)]}'
{"version": 3, "sources": ["/web/static/src/views/graph/graph_model.js", "/web/static/src/views/pivot/pivot_model.js", "/web/static/src/polyfills/clipboard.js", "/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js", "/spreadsheet_account/static/src/account_group_auto_complete.js", "/spreadsheet_account/static/src/accounting_functions.js", "/spreadsheet_account/static/src/index.js", "/spreadsheet_account/static/src/plugins/accounting_plugin.js", "/spreadsheet_account/static/src/utils.js", "/spreadsheet/static/src/actions/helpers.js", "/spreadsheet/static/src/actions/spreadsheet_component.js", "/spreadsheet/static/src/actions/spreadsheet_download_action.js", "/spreadsheet/static/src/chart/data_source/chart_data_source.js", "/spreadsheet/static/src/chart/index.js", "/spreadsheet/static/src/chart/odoo_chart/odoo_bar_chart.js", "/spreadsheet/static/src/chart/odoo_chart/odoo_chart.js", "/spreadsheet/static/src/chart/odoo_chart/odoo_line_chart.js", "/spreadsheet/static/src/chart/odoo_chart/odoo_pie_chart.js", "/spreadsheet/static/src/chart/odoo_menu/figure_component.js", "/spreadsheet/static/src/chart/plugins/chart_odoo_menu_plugin.js", "/spreadsheet/static/src/chart/plugins/odoo_chart_core_plugin.js", "/spreadsheet/static/src/chart/plugins/odoo_chart_ui_plugin.js", "/spreadsheet/static/src/chart/plugins/operational_transform.js", "/spreadsheet/static/src/components/share_button/share_button.js", "/spreadsheet/static/src/currency/formulas.js", "/spreadsheet/static/src/currency/helpers.js", "/spreadsheet/static/src/currency/plugins/currency.js", "/spreadsheet/static/src/data_sources/data_source.js", "/spreadsheet/static/src/data_sources/odoo_data_provider.js", "/spreadsheet/static/src/data_sources/odoo_views_data_source.js", "/spreadsheet/static/src/data_sources/server_data.js", "/spreadsheet/static/src/global_filters/components/filter_date_from_to_value/filter_date_from_to_value.js", "/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js", "/spreadsheet/static/src/global_filters/components/filter_text_value/filter_text_value.js", "/spreadsheet/static/src/global_filters/components/filter_value/filter_value.js", "/spreadsheet/static/src/global_filters/helpers.js", "/spreadsheet/static/src/global_filters/index.js", "/spreadsheet/static/src/global_filters/plugins/global_filters_core_plugin.js", "/spreadsheet/static/src/global_filters/plugins/global_filters_ui_plugin.js", "/spreadsheet/static/src/helpers/constants.js", "/spreadsheet/static/src/helpers/helpers.js", "/spreadsheet/static/src/helpers/model.js", "/spreadsheet/static/src/helpers/odoo_functions_helpers.js", "/spreadsheet/static/src/hooks.js", "/spreadsheet/static/src/index.js", "/spreadsheet/static/src/ir_ui_menu/index.js", "/spreadsheet/static/src/ir_ui_menu/ir_ui_menu_plugin.js", "/spreadsheet/static/src/ir_ui_menu/odoo_menu_link_cell.js", "/spreadsheet/static/src/list/index.js", "/spreadsheet/static/src/list/list_actions.js", "/spreadsheet/static/src/list/list_data_source.js", "/spreadsheet/static/src/list/list_functions.js", "/spreadsheet/static/src/list/list_helpers.js", "/spreadsheet/static/src/list/plugins/list_core_plugin.js", "/spreadsheet/static/src/list/plugins/list_ui_plugin.js", "/spreadsheet/static/src/model.js", "/spreadsheet/static/src/o_spreadsheet/cancelled_reason.js", "/spreadsheet/static/src/o_spreadsheet/errors.js", "/spreadsheet/static/src/o_spreadsheet/init_callbacks.js", "/spreadsheet/static/src/o_spreadsheet/migration.js", "/spreadsheet/static/src/o_spreadsheet/odoo_module.js", "/spreadsheet/static/src/o_spreadsheet/translation.js", "/spreadsheet/static/src/pivot/index.js", "/spreadsheet/static/src/pivot/odoo_pivot.js", "/spreadsheet/static/src/pivot/odoo_pivot_loader.js", "/spreadsheet/static/src/pivot/pivot_actions.js", "/spreadsheet/static/src/pivot/pivot_functions.js", "/spreadsheet/static/src/pivot/pivot_helpers.js", "/spreadsheet/static/src/pivot/pivot_model.js", "/spreadsheet/static/src/pivot/pivot_time_adapters.js", "/spreadsheet/static/src/pivot/pivot_to_function_values.js", "/spreadsheet/static/src/pivot/pivot_values_normalizers.js", "/spreadsheet/static/src/pivot/plugins/pivot_core_global_filter_plugin.js", "/spreadsheet/static/src/pivot/plugins/pivot_odoo_core_plugin.js", "/spreadsheet/static/src/pivot/plugins/pivot_odoo_ui_plugin.js", "/spreadsheet/static/src/pivot/plugins/pivot_ui_global_filter_plugin.js", "/spreadsheet/static/src/plugins.js", "/spreadsheet_dashboard/static/src/bundle/dashboard_action/dashboard_action.js", "/spreadsheet_dashboard/static/src/bundle/dashboard_action/dashboard_loader.js", "/spreadsheet_dashboard/static/src/bundle/dashboard_action/mobile_figure_container/mobile_figure_container.js", "/spreadsheet_dashboard/static/src/bundle/dashboard_action/mobile_search_panel/mobile_search_panel.js", "/spreadsheet_dashboard/static/src/bundle/list/clickable_cell.js", "/spreadsheet_dashboard/static/src/bundle/pivot/clickable_cell.js", "/spreadsheet_edition/static/src/bundle/actions/abstract_spreadsheet_action.js", "/spreadsheet_edition/static/src/bundle/actions/control_panel/spreadsheet_name.js", "/spreadsheet_edition/static/src/bundle/actions/input_dialog/input_dialog.js", "/spreadsheet_edition/static/src/bundle/actions/version_history/version_history_action.js", "/spreadsheet_edition/static/src/bundle/chart/clipboard_handlers/field_matching_clipboard_handler.js", "/spreadsheet_edition/static/src/bundle/chart/clipboard_handlers/odoo_menu_link_clipboard_handler.js", "/spreadsheet_edition/static/src/bundle/chart/odoo_chart_insertion.js", "/spreadsheet_edition/static/src/bundle/chart/odoo_menu/chart_panel.js", "/spreadsheet_edition/static/src/bundle/chart/side_panels/common/config_panel.js", "/spreadsheet_edition/static/src/bundle/chart/side_panels/index.js", "/spreadsheet_edition/static/src/bundle/chart/side_panels/main_side_panel_chart.js", "/spreadsheet_edition/static/src/bundle/chart/side_panels/odoo_bar/odoo_bar_config_panel.js", "/spreadsheet_edition/static/src/bundle/chart/side_panels/odoo_chart_with_axis/design_panel.js", "/spreadsheet_edition/static/src/bundle/chart/side_panels/odoo_line/odoo_line_config_panel.js", "/spreadsheet_edition/static/src/bundle/comments/clipboard_handler.js", "/spreadsheet_edition/static/src/bundle/comments/comments_store.js", "/spreadsheet_edition/static/src/bundle/comments/components/cell_thread.js", "/spreadsheet_edition/static/src/bundle/comments/components/cell_thread_popover.js", "/spreadsheet_edition/static/src/bundle/comments/components/composer_patch.js", "/spreadsheet_edition/static/src/bundle/comments/components/spreadsheet_comment_composer.js", "/spreadsheet_edition/static/src/bundle/comments/index.js", "/spreadsheet_edition/static/src/bundle/comments/plugins/comments_core_plugin.js", "/spreadsheet_edition/static/src/bundle/comments/side_panel/comment_threads_side_panel.js", "/spreadsheet_edition/static/src/bundle/components/collaborative_status/collaborative_status.js", "/spreadsheet_edition/static/src/bundle/components/locale_status/locale_status.js", "/spreadsheet_edition/static/src/bundle/components/side_panel_domain/side_panel_domain.js", "/spreadsheet_edition/static/src/bundle/components/spreadsheet_navbar/spreadsheet_navbar.js", "/spreadsheet_edition/static/src/bundle/components/topbar_share_button/topbar_share_button.js", "/spreadsheet_edition/static/src/bundle/global_filters/components/filter_editor/date_filter_editor_side_panel.js", "/spreadsheet_edition/static/src/bundle/global_filters/components/filter_editor/filter_editor_field_matching.js", "/spreadsheet_edition/static/src/bundle/global_filters/components/filter_editor/filter_editor_label.js", "/spreadsheet_edition/static/src/bundle/global_filters/components/filter_editor/filter_editor_side_panel.js", "/spreadsheet_edition/static/src/bundle/global_filters/components/filter_editor/relation_filter_editor_side_panel.js", "/spreadsheet_edition/static/src/bundle/global_filters/components/filter_editor/text_filter_editor_side_panel.js", "/spreadsheet_edition/static/src/bundle/global_filters/components/filter_field_offset.js", "/spreadsheet_edition/static/src/bundle/global_filters/filter_component.js", "/spreadsheet_edition/static/src/bundle/global_filters/global_filters_auto_complete.js", "/spreadsheet_edition/static/src/bundle/global_filters/global_filters_side_panel.js", "/spreadsheet_edition/static/src/bundle/global_filters/index.js", "/spreadsheet_edition/static/src/bundle/global_filters/operational_transform.js", "/spreadsheet_edition/static/src/bundle/helpers/misc.js", "/spreadsheet_edition/static/src/bundle/hooks.js", "/spreadsheet_edition/static/src/bundle/image/record_file_store.js", "/spreadsheet_edition/static/src/bundle/ir_menu_selector/ir_menu_selector.js", "/spreadsheet_edition/static/src/bundle/ir_ui_menu/index.js", "/spreadsheet_edition/static/src/bundle/list/autofill.js", "/spreadsheet_edition/static/src/bundle/list/index.js", "/spreadsheet_edition/static/src/bundle/list/list_actions.js", "/spreadsheet_edition/static/src/bundle/list/list_auto_complete.js", "/spreadsheet_edition/static/src/bundle/list/list_highlight_helpers.js", "/spreadsheet_edition/static/src/bundle/list/list_init_callback.js", "/spreadsheet_edition/static/src/bundle/list/operational_transform.js", "/spreadsheet_edition/static/src/bundle/list/plugins/list_autofill_plugin.js", "/spreadsheet_edition/static/src/bundle/list/side_panels/edit_list_sorting_section/edit_list_sorting_section.js", "/spreadsheet_edition/static/src/bundle/list/side_panels/list_details_side_panel.js", "/spreadsheet_edition/static/src/bundle/o_spreadsheet/collaborative/spreadsheet_collaborative_channel.js", "/spreadsheet_edition/static/src/bundle/o_spreadsheet/collaborative/spreadsheet_collaborative_service.js", "/spreadsheet_edition/static/src/bundle/o_spreadsheet/menu_item_registry.js", "/spreadsheet_edition/static/src/bundle/pivot/autofill.js", "/spreadsheet_edition/static/src/bundle/pivot/index.js", "/spreadsheet_edition/static/src/bundle/pivot/odoo_pivot.js", "/spreadsheet_edition/static/src/bundle/pivot/pivot_actions.js", "/spreadsheet_edition/static/src/bundle/pivot/pivot_init_callback.js", "/spreadsheet_edition/static/src/bundle/pivot/plugins/pivot_autofill_plugin.js", "/spreadsheet_edition/static/src/bundle/pivot/side_panels/odoo_pivot_layout_configurator/odoo_pivot_layout_configurator.js", "/spreadsheet_edition/static/src/bundle/pivot/side_panels/pivot_details_side_panel.js", "/spreadsheet_edition/static/src/bundle/pivot/side_panels/pivot_measure_display_panel_store.js", "/spreadsheet_edition/static/src/bundle/pivot/side_panels/pivot_side_panel_store.js", "/spreadsheet_edition/static/src/bundle/version_history/index.js", "/spreadsheet_edition/static/src/bundle/version_history/restore_version_dialog/restore_version_dialog.js", "/spreadsheet_edition/static/src/bundle/version_history/side_panel/version_history_item.js", "/spreadsheet_edition/static/src/bundle/version_history/side_panel/version_history_side_panel.js", "/spreadsheet_edition/static/src/bundle/version_history/version_history_plugin.js", "/spreadsheet_dashboard_edition/static/src/bundle/action/dashboard_edit_action.js", "/spreadsheet_dashboard_edition/static/src/bundle/components/dashboard_edit/dashboard_edit.js", "/spreadsheet_dashboard_edition/static/src/bundle/components/dashboard_publish/dashboard_publish.js", "/spreadsheet_sale_management/static/src/bundle/field_sync/action/field_sync_action.js", "/spreadsheet_sale_management/static/src/bundle/field_sync/field_sync_extension_hook.js", "/spreadsheet_sale_management/static/src/bundle/field_sync/field_sync_highlight_store.js", "/spreadsheet_sale_management/static/src/bundle/field_sync/model/field_sync_clipboard_handler.js", "/spreadsheet_sale_management/static/src/bundle/field_sync/model/field_sync_core_plugin.js", "/spreadsheet_sale_management/static/src/bundle/field_sync/model/field_sync_ui_plugin.js", "/spreadsheet_sale_management/static/src/bundle/field_sync/side_panel/field_sync_side_panel.js"], "mappings": "AAAA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACznBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACt0tEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpUA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9VA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9mBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;AC7DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpaA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;;;;ACLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrYA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/kBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3oBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1TA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3NA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjaA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3SA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;AC9DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;AC/CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACtCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5yBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "sourcesContent": ["import { _t } from \"@web/core/l10n/translation\";\nimport { sortBy, groupBy } from \"@web/core/utils/arrays\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { rankInterval } from \"@web/search/utils/dates\";\nimport { getGroupBy } from \"@web/search/utils/group_by\";\nimport { GROUPABLE_TYPES } from \"@web/search/utils/misc\";\nimport { addPropertyFieldDefs, Model } from \"@web/model/model\";\nimport { computeReportMeasures, processMeasure } from \"@web/views/utils\";\nimport { Domain } from \"@web/core/domain\";\n\nexport const SEP = \" / \";\n\nexport const SEQUENTIAL_TYPES = [\"date\", \"datetime\"];\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\nclass DateClasses {\n    // We view the param \"array\" as a matrix of values and undefined.\n    // An equivalence class is formed of defined values of a column.\n    // So nothing has to do with dates but we only use Dateclasses to manage\n    // identification of dates.\n    /**\n     * @param {(any[])[]} array\n     */\n    constructor(array) {\n        this.__referenceIndex = null;\n        this.__array = array;\n        for (let i = 0; i < this.__array.length; i++) {\n            const arr = this.__array[i];\n            if (arr.length && this.__referenceIndex === null) {\n                this.__referenceIndex = i;\n            }\n        }\n    }\n\n    /**\n     * @param {number} index\n     * @param {any} o\n     * @returns {string}\n     */\n    classLabel(index, o) {\n        return `${this.__array[index].indexOf(o)}`;\n    }\n\n    /**\n     * @param {string} classLabel\n     * @returns {any[]}\n     */\n    classMembers(classLabel) {\n        const classNumber = Number(classLabel);\n        const classMembers = new Set();\n        for (const arr of this.__array) {\n            if (arr[classNumber] !== undefined) {\n                classMembers.add(arr[classNumber]);\n            }\n        }\n        return [...classMembers];\n    }\n\n    /**\n     * @param {string} classLabel\n     * @param {number} [index]\n     * @returns {any}\n     */\n    representative(classLabel, index) {\n        const classNumber = Number(classLabel);\n        const i = index === undefined ? this.__referenceIndex : index;\n        if (i === null) {\n            return null;\n        }\n        return this.__array[i][classNumber];\n    }\n\n    /**\n     * @param {number} index\n     * @returns {number}\n     */\n    arrayLength(index) {\n        return this.__array[index].length;\n    }\n}\n\nexport class GraphModel extends Model {\n    /**\n     * @override\n     */\n    setup(params) {\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.race = new Race();\n        const _fetchDataPoints = this._fetchDataPoints.bind(this);\n        this._fetchDataPoints = (...args) => {\n            return this.race.add(_fetchDataPoints(...args));\n        };\n\n        this.initialGroupBy = null;\n\n        this.metaData = params;\n        this.data = null;\n        this.searchParams = null;\n        // This dataset will be added as a line plot on top of stacked bar chart.\n        this.lineOverlayDataset = null;\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    async load(searchParams) {\n        this.searchParams = searchParams;\n        if (!this.initialGroupBy) {\n            this.initialGroupBy = searchParams.context.graph_groupbys || this.metaData.groupBy; // = arch groupBy --> change that\n        }\n        const metaData = this._buildMetaData();\n        await addPropertyFieldDefs(\n            this.orm,\n            metaData.resModel,\n            searchParams.context,\n            metaData.fields,\n            metaData.groupBy.map((gb) => gb.fieldName)\n        );\n        await this._fetchDataPoints(metaData);\n    }\n\n    /**\n     * @override\n     */\n    hasData() {\n        return this.dataPoints.length > 0;\n    }\n\n    /**\n     * Only supposed to be called to change one or several parameters among\n     * \"measure\", \"mode\", \"order\", \"stacked\" and \"cumulated\".\n     * @param {Object} params\n     */\n    async updateMetaData(params) {\n        if (\"measure\" in params) {\n            const metaData = this._buildMetaData(params);\n            await this._fetchDataPoints(metaData);\n            this.useSampleModel = false;\n        } else {\n            await this.race.getCurrentProm();\n            this.metaData = Object.assign({}, this.metaData, params);\n            this._prepareData();\n        }\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     * @param {Object} [params={}]\n     * @returns {Object}\n     */\n    _buildMetaData(params) {\n        const { comparison, domain, context, groupBy } = this.searchParams;\n\n        const metaData = Object.assign({}, this.metaData, { context });\n        if (comparison) {\n            metaData.domains = comparison.domains;\n            metaData.comparisonField = comparison.fieldName;\n        } else {\n            metaData.domains = [{ arrayRepr: domain, description: null }];\n        }\n        metaData.measure = context.graph_measure || metaData.measure;\n        metaData.mode = context.graph_mode || metaData.mode;\n        metaData.groupBy = groupBy.length ? groupBy : this.initialGroupBy;\n        if (metaData.mode !== \"pie\") {\n            metaData.order = \"graph_order\" in context ? context.graph_order : metaData.order;\n            if (comparison) {\n                metaData.stacked = false;\n            } else if (\"graph_stacked\" in context) {\n                metaData.stacked = context.graph_stacked;\n            }\n            if (metaData.mode === \"line\") {\n                metaData.cumulated =\n                    \"graph_cumulated\" in context ? context.graph_cumulated : metaData.cumulated;\n            }\n        }\n\n        this._normalize(metaData);\n\n        metaData.measures = computeReportMeasures(metaData.fields, metaData.fieldAttrs, [\n            ...(metaData.viewMeasures || []),\n            metaData.measure,\n        ]);\n\n        return Object.assign(metaData, params);\n    }\n\n    /**\n     * Fetch the data points determined by the metaData. This function has\n     * several side effects. It can alter this.metaData and set this.dataPoints.\n     * @protected\n     * @param {Object} metaData\n     */\n    async _fetchDataPoints(metaData) {\n        this.dataPoints = await this.keepLast.add(this._loadDataPoints(metaData));\n        this.metaData = metaData;\n        this._prepareData();\n    }\n\n    /**\n     * Separates dataPoints coming from the read_group(s) into different\n     * datasets. This function returns the parameters data and labels used\n     * to produce the charts.\n     * @protected\n     * @param {Object[]}\n     * @returns {Object}\n     */\n    _getData(dataPoints) {\n        const { comparisonField, groupBy, mode } = this.metaData;\n\n        let identify = false;\n        if (comparisonField && groupBy.length && groupBy[0].fieldName === comparisonField) {\n            identify = true;\n        }\n        const dateClasses = identify ? this._getDateClasses(dataPoints) : null;\n\n        // dataPoints --> labels\n        let labels = [];\n        const labelMap = {};\n        for (const dataPt of dataPoints) {\n            const x = dataPt.labels.slice(0, mode === \"pie\" ? undefined : 1);\n            const trueLabel = x.length ? x.join(SEP) : _t(\"Total\");\n            if (dateClasses) {\n                x[0] = dateClasses.classLabel(dataPt.originIndex, x[0]);\n            }\n            const key = JSON.stringify(x);\n            if (labelMap[key] === undefined) {\n                labelMap[key] = labels.length;\n                if (dateClasses) {\n                    if (mode === \"pie\") {\n                        x[0] = dateClasses.classMembers(x[0]).join(\", \");\n                    } else {\n                        x[0] = dateClasses.representative(x[0]);\n                    }\n                }\n                const label = x.length ? x.join(SEP) : _t(\"Total\");\n                labels.push(label);\n            }\n            dataPt.labelIndex = labelMap[key];\n            dataPt.trueLabel = trueLabel;\n        }\n\n        // dataPoints + labels --> datasetsTmp --> datasets\n        const datasetsTmp = {};\n        for (const dataPt of dataPoints) {\n            const {\n                domain,\n                labelIndex,\n                originIndex,\n                trueLabel,\n                value,\n                identifier,\n                cumulatedStart,\n            } = dataPt;\n            const datasetLabel = this._getDatasetLabel(dataPt);\n            if (!(datasetLabel in datasetsTmp)) {\n                let dataLength = labels.length;\n                if (mode !== \"pie\" && dateClasses) {\n                    dataLength = dateClasses.arrayLength(originIndex);\n                }\n                datasetsTmp[datasetLabel] = {\n                    data: new Array(dataLength).fill(0),\n                    cumulatedStart,\n                    trueLabels: labels.slice(0, dataLength), // should be good // check this in case identify = true\n                    domains: new Array(dataLength).fill([]),\n                    label: datasetLabel,\n                    originIndex: originIndex,\n                    identifiers: new Set(),\n                };\n            }\n            datasetsTmp[datasetLabel].data[labelIndex] = value;\n            datasetsTmp[datasetLabel].domains[labelIndex] = domain;\n            datasetsTmp[datasetLabel].trueLabels[labelIndex] = trueLabel;\n            datasetsTmp[datasetLabel].identifiers.add(identifier);\n        }\n        // sort by origin\n        let datasets = sortBy(Object.values(datasetsTmp), \"originIndex\");\n\n        if (mode === \"pie\") {\n            // We kinda have a matrix. We remove the zero columns and rows. This is a global operation.\n            // That's why it cannot be done before.\n            datasets = datasets.filter((dataset) => dataset.data.some((v) => Boolean(v)));\n            const labelsToKeepIndexes = {};\n            labels.forEach((_, index) => {\n                if (datasets.some((dataset) => Boolean(dataset.data[index]))) {\n                    labelsToKeepIndexes[index] = true;\n                }\n            });\n            labels = labels.filter((_, index) => labelsToKeepIndexes[index]);\n            for (const dataset of datasets) {\n                dataset.data = dataset.data.filter((_, index) => labelsToKeepIndexes[index]);\n                dataset.domains = dataset.domains.filter((_, index) => labelsToKeepIndexes[index]);\n                dataset.trueLabels = dataset.trueLabels.filter(\n                    (_, index) => labelsToKeepIndexes[index]\n                );\n            }\n        }\n\n        return { datasets, labels };\n    }\n\n    _getLabel(description) {\n        if (!description) {\n            return _t(\"Sum\");\n        } else {\n            return _t(\"Sum (%s)\", description);\n        }\n    }\n\n    _getLineOverlayDataset() {\n        const { domains, stacked } = this.metaData;\n        const data = this.data;\n        let lineOverlayDataset = null;\n        if (stacked) {\n            const stacks = groupBy(data.datasets, (dataset) => dataset.originIndex);\n            if (Object.keys(stacks).length == 1) {\n                const [[originIndex, datasets]] = Object.entries(stacks);\n                if (datasets.length > 1) {\n                    const data = [];\n                    for (const dataset of datasets) {\n                        for (let i = 0; i < dataset.data.length; i++) {\n                            data[i] = (data[i] || 0) + dataset.data[i];\n                        }\n                    }\n                    lineOverlayDataset = {\n                        label: this._getLabel(domains[originIndex].description),\n                        data,\n                        trueLabels: datasets[0].trueLabels,\n                    };\n                }\n            }\n        }\n        return lineOverlayDataset;\n    }\n\n    /**\n     * Determines the dataset to which the data point belongs.\n     * @protected\n     * @param {Object} dataPoint\n     * @returns {string}\n     */\n    _getDatasetLabel(dataPoint) {\n        const { measure, measures, domains, mode } = this.metaData;\n        const { labels, originIndex } = dataPoint;\n        if (mode === \"pie\") {\n            return domains[originIndex].description || \"\";\n        }\n        // ([origin] + second to last groupBys) or measure\n        let datasetLabel = labels.slice(1).join(SEP);\n        if (domains.length > 1) {\n            datasetLabel =\n                domains[originIndex].description + (datasetLabel ? SEP + datasetLabel : \"\");\n        }\n        datasetLabel = datasetLabel || measures[measure].string;\n        return datasetLabel;\n    }\n\n    /**\n     * @protected\n     * @param {Object[]} dataPoints\n     * @returns {DateClasses}\n     */\n    _getDateClasses(dataPoints) {\n        const { domains } = this.metaData;\n        const dateSets = domains.map(() => new Set());\n        for (const { labels, originIndex } of dataPoints) {\n            const date = labels[0];\n            dateSets[originIndex].add(date);\n        }\n        const arrays = dateSets.map((dateSet) => [...dateSet]);\n        return new DateClasses(arrays);\n    }\n\n    /**\n     * @protected\n     * @returns {string}\n     */\n    _getDefaultFilterLabel(field) {\n        return _t(\"None\");\n    }\n\n    /**\n     * Eventually filters and sort data points.\n     * @protected\n     * @returns {Object[]}\n     */\n    _getProcessedDataPoints() {\n        const { domains, groupBy, mode, order } = this.metaData;\n        let processedDataPoints = [];\n        if (mode === \"line\") {\n            processedDataPoints = this.dataPoints.filter(\n                (dataPoint) => dataPoint.labels[0] !== this._getDefaultFilterLabel(groupBy[0])\n            );\n        } else if (mode === \"pie\") {\n            processedDataPoints = this.dataPoints.filter(\n                (dataPoint) => dataPoint.value > 0 && dataPoint.count !== 0\n            );\n        } else {\n            processedDataPoints = this.dataPoints.filter((dataPoint) => dataPoint.count !== 0);\n        }\n\n        if (order !== null && mode !== \"pie\" && domains.length === 1 && groupBy.length > 0) {\n            // group data by their x-axis value, and then sort datapoints\n            // based on the sum of values by group in ascending/descending order\n            const groupedDataPoints = {};\n            for (const dataPt of processedDataPoints) {\n                const key = dataPt.labels[0]; // = x-axis value under the current assumptions\n                if (!groupedDataPoints[key]) {\n                    groupedDataPoints[key] = [];\n                }\n                groupedDataPoints[key].push(dataPt);\n            }\n            const groups = Object.values(groupedDataPoints);\n            const groupTotal = (group) => group.reduce((sum, dataPt) => sum + dataPt.value, 0);\n            processedDataPoints = sortBy(groups, groupTotal, order.toLowerCase()).flat();\n        }\n\n        return processedDataPoints;\n    }\n\n    /**\n     * Fetch and process graph data.  It is basically a(some) read_group(s)\n     * with correct fields for each domain.  We have to do some light processing\n     * to separate date groups in the field list, because they can be defined\n     * with an aggregation function, such as my_date:week.\n     * @protected\n     * @param {Object} metaData\n     * @returns {Object[]}\n     */\n    async _loadDataPoints(metaData) {\n        const { measure, domains, fields, groupBy, resModel, cumulatedStart } = metaData;\n        const fieldName = groupBy[0]?.fieldName;\n        const sequential_field =\n            cumulatedStart && SEQUENTIAL_TYPES.includes(fields[fieldName]?.type) ? fieldName : null;\n        const sequential_spec = sequential_field && groupBy[0].spec;\n        const measures = [\"__count\"];\n        if (measure !== \"__count\") {\n            let { aggregator, type } = fields[measure];\n            if (type === \"many2one\") {\n                aggregator = \"count_distinct\";\n            }\n            if (aggregator === undefined) {\n                throw new Error(\n                    `No aggregate function has been provided for the measure '${measure}'`\n                );\n            }\n            measures.push(`${measure}:${aggregator}`);\n        }\n\n        const numbering = {}; // used to avoid ambiguity with many2one with values with same labels:\n        // for instance [1, \"ABC\"] [3, \"ABC\"] should be distinguished.\n\n        const proms = domains.map(async (domain, originIndex) => {\n            const data = await this.orm.webReadGroup(\n                resModel,\n                domain.arrayRepr,\n                measures,\n                groupBy.map((gb) => gb.spec),\n                {\n                    lazy: false, // what is this thing???\n                    context: { fill_temporal: true, ...this.searchParams.context },\n                }\n            );\n            let start = false;\n            if (\n                cumulatedStart &&\n                sequential_field &&\n                data.groups.length &&\n                domain.arrayRepr.some((leaf) => leaf.length === 3 && leaf[0] == sequential_field)\n            ) {\n                const first_date = data.groups[0].__range[sequential_spec].from;\n                const new_domain = Domain.combine(\n                    [\n                        new Domain([[sequential_field, \"<\", first_date]]),\n                        Domain.removeDomainLeaves(domain.arrayRepr, [sequential_field]),\n                    ],\n                    \"AND\"\n                ).toList();\n                start = await this.orm.webReadGroup(\n                    resModel,\n                    new_domain,\n                    measures,\n                    groupBy.filter((gb) => gb.fieldName != sequential_field).map((gb) => gb.spec),\n                    {\n                        lazy: false, // what is this thing???\n                        context: { ...this.searchParams.context },\n                    }\n                );\n            }\n            const dataPoints = [];\n            const cumulatedStartValue = {};\n            if (start) {\n                for (const group of start.groups) {\n                    const rawValues = [];\n                    for (const gb of groupBy.filter((gb) => gb.fieldName != sequential_field)) {\n                        rawValues.push({ [gb.spec]: group[gb.spec] });\n                    }\n                    cumulatedStartValue[JSON.stringify(rawValues)] = group[measure];\n                }\n            }\n            for (const group of data.groups) {\n                const { __domain, __count } = group;\n                const labels = [];\n                const rawValues = [];\n                for (const gb of groupBy) {\n                    let label;\n                    const val = group[gb.spec];\n                    rawValues.push({ [gb.spec]: val });\n                    const fieldName = gb.fieldName;\n                    const { type } = fields[fieldName];\n                    if (type === \"boolean\") {\n                        label = `${val}`; // toUpperCase?\n                    } else if (val === false) {\n                        label = this._getDefaultFilterLabel(gb);\n                    } else if ([\"many2many\", \"many2one\"].includes(type)) {\n                        const [id, name] = val;\n                        const key = JSON.stringify([fieldName, name]);\n                        if (!numbering[key]) {\n                            numbering[key] = {};\n                        }\n                        const numbers = numbering[key];\n                        if (!numbers[id]) {\n                            numbers[id] = Object.keys(numbers).length + 1;\n                        }\n                        const num = numbers[id];\n                        label = num === 1 ? name : `${name} (${num})`;\n                    } else if (type === \"selection\") {\n                        const selected = fields[fieldName].selection.find((s) => s[0] === val);\n                        label = selected[1];\n                    } else {\n                        label = val;\n                    }\n                    labels.push(label);\n                }\n\n                let value = group[measure];\n                if (value instanceof Array) {\n                    // case where measure is a many2one and is used as groupBy\n                    value = 1;\n                }\n                if (!Number.isInteger(value)) {\n                    metaData.allIntegers = false;\n                }\n                const group_id = JSON.stringify(rawValues.slice(1));\n                dataPoints.push({\n                    count: __count,\n                    domain: __domain,\n                    value,\n                    labels,\n                    originIndex,\n                    identifier: JSON.stringify(rawValues),\n                    cumulatedStart: cumulatedStartValue[group_id] || 0,\n                });\n            }\n            return dataPoints;\n        });\n        const promResults = await Promise.all(proms);\n        return promResults.flat();\n    }\n\n    /**\n     * Process metaData.groupBy in order to keep only the finest interval option for\n     * elements based on date/datetime field (e.g. 'date:year'). This means that\n     * 'week' is prefered to 'month'. The field stays at the place of its first occurence.\n     * For instance,\n     * ['foo', 'date:month', 'bar', 'date:week'] becomes ['foo', 'date:week', 'bar'].\n     * @protected\n     * @param {Object} metaData\n     */\n    _normalize(metaData) {\n        const { fields } = metaData;\n        const groupBy = [];\n        for (const gb of metaData.groupBy) {\n            let ngb = gb;\n            if (typeof gb === \"string\") {\n                ngb = getGroupBy(gb, fields);\n            }\n            groupBy.push(ngb);\n        }\n\n        const processedGroupBy = [];\n        for (const gb of groupBy) {\n            const { fieldName, interval } = gb;\n            if (!fieldName.includes(\".\")) {\n                const { groupable, type } = fields[fieldName];\n                if (\n                    // cf. _description_groupable in odoo/fields.py\n                    !groupable ||\n                    [\"id\", \"__count\"].includes(fieldName) ||\n                    !GROUPABLE_TYPES.includes(type)\n                ) {\n                    continue;\n                }\n            }\n            const index = processedGroupBy.findIndex((gb) => gb.fieldName === fieldName);\n            if (index === -1) {\n                processedGroupBy.push(gb);\n            } else if (interval) {\n                const registeredInterval = processedGroupBy[index].interval;\n                if (rankInterval(registeredInterval) < rankInterval(interval)) {\n                    processedGroupBy.splice(index, 1, gb);\n                }\n            }\n        }\n        metaData.groupBy = processedGroupBy;\n\n        metaData.measure = processMeasure(metaData.measure);\n    }\n\n    /**\n     * @protected\n     */\n    _prepareData() {\n        const processedDataPoints = this._getProcessedDataPoints();\n        this.data = this._getData(processedDataPoints);\n        this.lineOverlayDataset = null;\n        if (this.metaData.mode === \"bar\") {\n            this.lineOverlayDataset = this._getLineOverlayDataset();\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { cartesian, sections, sortBy, symmetricalDifference } from \"@web/core/utils/arrays\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { DEFAULT_INTERVAL } from \"@web/search/utils/dates\";\nimport { addPropertyFieldDefs, Model } from \"@web/model/model\";\nimport { computeReportMeasures, processMeasure } from \"@web/views/utils\";\n\n/**\n * @param {number} value\n * @param {number} comparisonValue\n * @returns {number}\n */\nfunction computeVariation(value, comparisonValue) {\n    if (isNaN(value) || isNaN(comparisonValue)) {\n        return NaN;\n    }\n    if (comparisonValue === 0) {\n        if (value === 0) {\n            return 0;\n        } else if (value > 0) {\n            return 1;\n        } else {\n            return -1;\n        }\n    }\n    return (value - comparisonValue) / Math.abs(comparisonValue);\n}\n\n/**\n * Pivot Model\n *\n * The pivot model keeps an in-memory representation of the pivot table that is\n * displayed on the screen.  The exact layout of this representation is not so\n * simple, because a pivot table is at its core a 2-dimensional object, but\n * with a 'list' component: some rows/cols can be expanded so we zoom into the\n * structure.\n *\n * However, we need to be able to manipulate the data in a somewhat efficient\n * way, and to transform it into a list of lines to be displayed by the renderer.\n *\n * Basicaly the pivot table presents aggregated values for various groups of records\n * in one domain. If a comparison is asked for, two domains are considered.\n *\n * Let us consider a simple example and let us fix the vocabulary (let us suppose we are in June 2020):\n * ___________________________________________________________________________________________________________________________________________\n * |                    |   Total                                                                                                             |\n * |                    |_____________________________________________________________________________________________________________________|\n * |                    |   Sale Team 1                         |  Sale Team 2                         |                                      |\n * |                    |_______________________________________|______________________________________|______________________________________|\n * |                    |   Sales total                         |  Sales total                         |  Sales total                         |\n * |                    |_______________________________________|______________________________________|______________________________________|\n * |                    |   May 2020   | June 2020  | Variation |  May 2020   | June 2020  | Variation |  May 2020   | June 2020  | Variation |\n * |____________________|______________|____________|___________|_____________|____________|___________|_____________|____________|___________|\n * | Total              |     85       |     110    |  29.4%    |     40      |    30      |   -25%    |    125      |    140     |     12%   |\n * |    Europe          |     25       |     35     |    40%    |     40      |    30      |   -25%    |     65      |     65     |      0%   |\n * |        Brussels    |      0       |     15     |   100%    |     30      |    30      |     0%    |     30      |     45     |     50%   |\n * |        Paris       |     25       |     20     |   -20%    |     10      |     0      |  -100%    |     35      |     20     |  -42.8%   |\n * |    North America   |     60       |     75     |    25%    |             |            |           |     60      |     75     |     25%   |\n * |        Washington  |     60       |     75     |    25%    |             |            |           |     60      |     75     |     25%   |\n * |____________________|______________|____________|___________|_____________|____________|___________|_____________|____________|___________|\n *\n *\n * META DATA:\n *\n * In the above pivot table, the records have been grouped using the fields\n *\n *      continent_id, city_id\n *\n * for rows and\n *\n *      sale_team_id\n *\n * for columns.\n *\n * The measure is the field 'sales_total'.\n *\n * Two domains are considered: 'May 2020' and 'June 2020'.\n *\n * In the model,\n *\n *      - rowGroupBys is the list [continent_id, city_id]\n *      - colGroupBys is the list [sale_team_id]\n *      - measures is the list [sales_total]\n *      - domains is the list [d1, d2] with d1 and d2 domain expressions\n *          for say sale_date in May 2020 and June 2020, for instance\n *          d1 = [['sale_date', >=, 2020-05-01], ['sale_date', '<=', 2020-05-31]]\n *      - origins is the list ['May 2020', 'June 2020']\n *\n * DATA:\n *\n * Recall that a group is constituted by records (in a given domain)\n * that have the same (raw) values for a list of fields.\n * Thus the group itself is identified by this list and the domain.\n * In comparison mode, the same group (forgetting the domain part or 'originIndex')\n * can be eventually found in the two domains.\n * This defines the way in which the groups are identified or not.\n *\n * In the above table, (forgetting the domain) the following groups are found:\n *\n *      the 'row groups'\n *      - Total\n *      - Europe\n *      - America\n *      - Europe, Brussels\n *      - Europe, Paris\n *      - America, Washington\n *\n *      the 'col groups'\n *\n *      - Total\n *      - Sale Team 1\n *      - Sale Team 2\n *\n *      and all non trivial combinations of row groups and col groups\n *\n *      - Europe, Sale Team 1\n *      - Europe, Brussels, Sale Team 2\n *      - America, Washington, Sale Team 1\n *      - ...\n *\n * The list of fields is created from the concatenation of two lists of fields, the first in\n *\n * [], [f1], [f1, f2], ... [f1, f2, ..., fn]  for [f1, f2, ..., fn] the full list of groupbys\n * (called rowGroupBys) used to create row groups\n *\n * In the example: [], [continent_id], [continent_id, city_id].\n *\n * and the second in\n * [], [g1], [g1, g2], ... [g1, g2, ..., gm]  for [g1, g2, ..., gm] the full list of groupbys\n * (called colGroupBys) used to create col groups.\n *\n * In the example: [], [sale_team_id].\n *\n * Thus there are (n+1)*(m+1) lists of fields possible.\n *\n * In the example: 6 lists possible, namely [],\n *                                          [continent_id], [sale_team_id],\n *                                          [continent_id, sale_team_id], [continent_id, city_id],\n *                                          [continent_id, city_id, sale_team_id]\n *\n * A given list is thus of the form [f1,..., fi, g1,..., gj] or better [[f1,...,fi], [g1,...,gj]]\n *\n * For each list of fields possible and each domain considered, one read_group is done\n * and gives results of the form (an exception for list [])\n *\n * g = {\n *  f1: v1, ..., fi: vi,\n *  g1: w1, ..., gj: wj,\n *  m1: x1, ..., mk: xk,\n *  __count: c,\n *  __domain: d\n * }\n *\n * where v1,...,vi,w1,...,Wj are 'values' for the corresponding fields and\n * m1,...,mk are the fields selected as measures.\n *\n * For example, g = {\n *      continent_id: [1, 'Europe']\n *      sale_team_id: [1, 'Sale Team 1']\n *      sales_count: 25,\n *      __count: 4\n *      __domain: [\n *                  ['sale_date', >=, 2020-05-01], ['sale_date', '<=', 2020-05-31],\n *                  ['continent_id', '=', 1],\n *                  ['sale_team_id', '=', 1]\n *                ]\n * }\n *\n * Thus the above group g is fully determined by [[v1,...,vi], [w1,...,wj]] and the base domain\n * or the corresponding 'originIndex'.\n *\n * When j=0, g corresponds to a row group (or also row header) and is of the form [[v1,...,vi], []] or more simply [v1,...vi]\n * (not forgetting the list [v1,...vi] comes from left).\n * When i=0, g corresponds to a col group (or col header) and is of the form [[], [w1,...,wj]] or more simply [w1,...,wj].\n *\n * A generic group g as above [[v1,...,vi], [w1,...,wj]] corresponds to the two headers [[v1,...,vi], []]\n * and [[], [w1,...,wj]].\n *\n * Here is a description of the data structure manipulated by the pivot model.\n *\n * Five objects contain all the data from the read_groups\n *\n *      - rowGroupTree: contains information on row headers\n *             the nodes correspond to the groups of the form [[v1,...,vi], []]\n *             The root is [[], []].\n *             A node [[v1,...,vl], []] has as direct children the nodes of the form [[v1,...,vl,v], []],\n *             this means that a direct child is obtained by grouping records using the single field fi+1\n *\n *             The structure at each level is of the form\n *\n *             {\n *                  root: {\n *                      values: [v1,...,vl],\n *                      labels: [la1,...,lal]\n *                  },\n *                  directSubTrees: {\n *                      v => {\n *                              root: {\n *                                  values: [v1,...,vl,v]\n *                                  labels: [label1,...,labell,label]\n *                              },\n *                              directSubTrees: {...}\n *                          },\n *                      v' => {...},\n *                      ...\n *                  }\n *             }\n *\n *             (directSubTrees is a Map instance)\n *\n *             In the example, the rowGroupTree is:\n *\n *             {\n *                  root: {\n *                      values: [],\n *                      labels: []\n *                  },\n *                  directSubTrees: {\n *                      1 => {\n *                              root: {\n *                                  values: [1],\n *                                  labels: ['Europe'],\n *                              },\n *                              directSubTrees: {\n *                                  1 => {\n *                                          root: {\n *                                              values: [1, 1],\n *                                              labels: ['Europe', 'Brussels'],\n *                                          },\n *                                          directSubTrees: new Map(),\n *                                  },\n *                                  2 => {\n *                                          root: {\n *                                              values: [1, 2],\n *                                              labels: ['Europe', 'Paris'],\n *                                          },\n *                                          directSubTrees: new Map(),\n *                                  },\n *                              },\n *                          },\n *                      2 => {\n *                              root: {\n *                                  values: [2],\n *                                  labels: ['America'],\n *                              },\n *                              directSubTrees: {\n *                                  3 => {\n *                                          root: {\n *                                              values: [2, 3],\n *                                              labels: ['America', 'Washington'],\n *                                          }\n *                                          directSubTrees: new Map(),\n *                                  },\n *                              },\n *                      },\n *                  },\n *             }\n *\n *      - colGroupTree: contains information on col headers\n *              The same as above with right instead of left\n *\n *      - measurements: contains information on measure values for all the groups\n *\n *              the object keys are of the form JSON.stringify([[v1,...,vi], [w1,...,wj]])\n *              and values are arrays of length equal to number of origins containing objects of the form\n *                  {m1: x1,...,mk: xk}\n *              The structure looks like\n *\n *              {\n *                  JSON.stringify([[], []]): [{m1: x1,...,mk: xk}, {m1: x1',...,mk: xk'},...]\n *                  ....\n *                  JSON.stringify([[v1,...,vi], [w1,...,wj]]): [{m1: y1',...,mk: yk'}, {m1: y1',...,mk: yk'},...],\n *                  ....\n *                  JSON.stringify([[v1,...,vn], [w1,...,wm]]): [{m1: z1',...,mk: zk'}, {m1: z1',...,mk: zk'},...],\n *              }\n *              Thus the structure contains all information for all groups and all origins on measure values.\n *\n *\n *              this.measurments[\"[[], []]\"][0]['foo'] gives the value of the measure 'foo' for the group 'Total' and the\n *              first domain (origin).\n *\n *              In the example:\n *                  {\n *                      \"[[], []]\": [{'sales_total': 125}, {'sales_total': 140}]                      (total/total)\n *                      ...\n *                      \"[[1, 2], [2]]\": [{'sales_total': 10}, {'sales_total': 0}]                   (Europe/Paris/Sale Team 2)\n *                      ...\n *                  }\n *\n *      - counts: contains information on the number of records in each groups\n *              The structure is similar to the above but the arrays contains numbers (counts)\n *      - groupDomains:\n *              The structure is similar to the above but the arrays contains domains\n *\n *      With this light data structures, all manipulation done by the model are eased and redundancies are limited.\n *      Each time a rendering or an export of the data has to be done, the pivot table is generated by the getTable function.\n */\n\n/**\n * @typedef Meta\n * @property {string[]} activeMeasures\n * @property {string[]} colGroupBys\n * @property {boolean} disableLinking\n * @property {Object} fields\n * @property {Object} measures\n * @property {string} resModel\n * @property {string[]} rowGroupBys\n * @property {string} title\n * @property {boolean} useSampleModel\n * @property {Object} widgets\n * @property {Map} customGroupBys\n * @property {string[]} expandedRowGroupBys\n * @property {string[]} expandedColGroupBys\n * @property {Object} sortedColumn\n * @property {Array[]} domains\n * @property {string[]} origins\n */\n\n/**\n * @typedef Data\n * @property {Object} colGroupTree\n * @property {Object} rowGroupTree\n * @property {Object} groupDomains\n * @property {Object} measurements\n * @property {Object} counts\n * @property {Object} numbering\n */\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\n/**\n * @typedef Config\n * @property {MetaData} metaData\n * @property {Data} data\n */\n\nexport class PivotModel extends Model {\n    /**\n     * @override\n     * @param {Object} params.metaData\n     * @param {string[]} params.metaData.activeMeasures\n     * @param {string[]} params.metaData.colGroupBys\n     * @param {Object} params.metaData.fields\n     * @param {Object[]} params.metaData.measures\n     * @param {string} params.metaData.resModel\n     * @param {string[]} params.metaData.rowGroupBys\n     * @param {string|null} params.metaData.defaultOrder\n     * @param {boolean} params.metaData.disableLinking\n     * @param {boolean} params.metaData.useSampleModel\n     * @param {Map} [params.metaData.customGroupBys={}]\n     * @param {string[]} [params.metaData.expandedColGroupBys=[]]\n     * @param {string[]} [params.metaData.expandedRowGroupBys=[]]\n     * @param {Object|null} [params.metaData.sortedColumn=null]\n     * @param {Object} [params.data] previously exported data\n     */\n    setup(params) {\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.race = new Race();\n        const _loadData = this._loadData.bind(this);\n        this._loadData = (...args) => {\n            return this.race.add(_loadData(...args));\n        };\n\n        let sortedColumn = params.metaData.sortedColumn || null;\n        if (!sortedColumn && params.metaData.defaultOrder) {\n            const defaultOrder = params.metaData.defaultOrder.split(\" \");\n            sortedColumn = {\n                groupId: [[], []],\n                measure: defaultOrder[0],\n                order: defaultOrder[1] ? defaultOrder[1] : \"asc\",\n            };\n        }\n\n        this.searchParams = {\n            context: {},\n            domain: [],\n            domains: [],\n            groupBy: [],\n        };\n        this.data = params.data || {\n            colGroupTree: null,\n            rowGroupTree: null,\n            groupDomains: {},\n            measurements: {},\n            counts: {},\n            numbering: {},\n        };\n        const metaData = Object.assign({}, params.metaData, {\n            customGroupBys: params.metaData.customGroupBys || new Map(),\n            expandedRowGroupBys: params.metaData.expandedRowGroupBys || [],\n            expandedColGroupBys: params.metaData.expandedColGroupBys || [],\n            sortedColumn,\n        });\n        this.metaData = this._buildMetaData(metaData);\n\n        this.reload = false; // used to discriminate between the first load and subsequent reloads\n        this.nextActiveMeasures = null; // allows to toggle several measures consecutively\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add a groupBy to rowGroupBys or colGroupBys according to provided type.\n     *\n     * @param {Object} params\n     * @param {Array[]} params.groupId\n     * @param {string} params.fieldName\n     * @param {'row'|'col'} params.type\n     * @param {boolean} [params.custom=false]\n     * @param {string} [params.interval]\n     */\n    async addGroupBy(params) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const { groupId, fieldName, type, custom } = params;\n        let { interval } = params;\n        const metaData = this._buildMetaData();\n        if (custom && !metaData.customGroupBys.has(fieldName)) {\n            const field = metaData.fields[fieldName];\n            if (!interval && [\"date\", \"datetime\"].includes(field.type)) {\n                interval = DEFAULT_INTERVAL;\n            }\n            metaData.customGroupBys.set(fieldName, {\n                ...field,\n                id: fieldName,\n            });\n        }\n\n        let groupBy = fieldName;\n        if (interval) {\n            groupBy = `${groupBy}:${interval}`;\n        }\n        if (type === \"row\") {\n            metaData.expandedRowGroupBys.push(groupBy);\n        } else {\n            metaData.expandedColGroupBys.push(groupBy);\n        }\n        const config = { metaData, data: this.data };\n        await this._expandGroup(groupId, type, config);\n        this.metaData = metaData;\n        this.notify();\n    }\n    /**\n     * Close the group with id given by groupId. A type must be specified\n     * in case groupId is [[], []] (the id of the group 'Total') because this\n     * group is present in both colGroupTree and rowGroupTree.\n     *\n     * @param {Array[]} groupId\n     * @param {'row'|'col'} type\n     */\n    closeGroup(groupId, type) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloading the table\n        }\n\n        let groupBys;\n        let expandedGroupBys;\n        let keyPart;\n        let group;\n        let tree;\n        if (type === \"row\") {\n            groupBys = this.metaData.rowGroupBys;\n            expandedGroupBys = this.metaData.expandedRowGroupBys;\n            tree = this.data.rowGroupTree;\n            group = this._findGroup(this.data.rowGroupTree, groupId[0]);\n            keyPart = 0;\n        } else {\n            groupBys = this.metaData.colGroupBys;\n            expandedGroupBys = this.metaData.expandedColGroupBys;\n            tree = this.data.colGroupTree;\n            group = this._findGroup(this.data.colGroupTree, groupId[1]);\n            keyPart = 1;\n        }\n\n        const groupIdPart = groupId[keyPart];\n        const range = groupIdPart.map((_, index) => index);\n        function keep(key) {\n            const idPart = JSON.parse(key)[keyPart];\n            return (\n                range.some((index) => groupIdPart[index] !== idPart[index]) ||\n                idPart.length === groupIdPart.length\n            );\n        }\n        function omitKeys(object) {\n            const newObject = {};\n            for (const key in object) {\n                if (keep(key)) {\n                    newObject[key] = object[key];\n                }\n            }\n            return newObject;\n        }\n        this.data.measurements = omitKeys(this.data.measurements);\n        this.data.counts = omitKeys(this.data.counts);\n        this.data.groupDomains = omitKeys(this.data.groupDomains);\n\n        group.directSubTrees.clear();\n        delete group.sortedKeys;\n        var newGroupBysLength = this._getTreeHeight(tree) - 1;\n        if (newGroupBysLength <= groupBys.length) {\n            expandedGroupBys.splice(0);\n            groupBys.splice(newGroupBysLength);\n        } else {\n            expandedGroupBys.splice(newGroupBysLength - groupBys.length);\n        }\n        this.notify();\n    }\n    /**\n     * Reload the view with the current rowGroupBys and colGroupBys\n     * This is the easiest way to expand all the groups that are not expanded\n     */\n    async expandAll() {\n        const config = { metaData: this.metaData, data: this.data };\n        await this._loadData(config, false);\n        this.notify();\n    }\n    /**\n     * Expand a group by using groupBy to split it and trigger a re-rendering.\n     *\n     * @param {Object} group\n     * @param {'row'|'col'} type\n     */\n    async expandGroup(groupId, type) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const config = { metaData: this.metaData, data: this.data };\n        await this._expandGroup(groupId, type, config);\n        this.notify();\n    }\n    /**\n     * Export model data in a form suitable for an easy encoding of the pivot\n     * table in excell.\n     *\n     * @returns {Object}\n     */\n    exportData() {\n        const measureCount = this.metaData.activeMeasures.length;\n        const originCount = this.metaData.origins.length;\n\n        const table = this.getTable();\n\n        // process headers\n        const headers = table.headers;\n        let colGroupHeaderRows;\n        let measureRow = [];\n        let originRow = [];\n\n        function processHeader(header) {\n            const inTotalColumn = header.groupId[1].length === 0;\n            return {\n                title: header.title,\n                width: header.width,\n                height: header.height,\n                is_bold: !!header.measure && inTotalColumn,\n            };\n        }\n\n        if (originCount > 1) {\n            colGroupHeaderRows = headers.slice(0, headers.length - 2);\n            measureRow = headers[headers.length - 2].map(processHeader);\n            originRow = headers[headers.length - 1].map(processHeader);\n        } else {\n            colGroupHeaderRows = headers.slice(0, headers.length - 1);\n            measureRow = headers[headers.length - 1].map(processHeader);\n        }\n\n        // remove the empty headers on left side\n        colGroupHeaderRows[0].splice(0, 1);\n\n        colGroupHeaderRows = colGroupHeaderRows.map((headerRow) => {\n            return headerRow.map(processHeader);\n        });\n\n        // process rows\n        const tableRows = table.rows.map((row) => {\n            return {\n                title: row.title,\n                indent: row.indent,\n                values: row.subGroupMeasurements.map((measurement) => {\n                    let value = measurement.value;\n                    if (value === undefined) {\n                        value = \"\";\n                    } else if (measurement.originIndexes.length > 1) {\n                        // in that case the value is a variation and a\n                        // number between 0 and 1\n                        value = value * 100;\n                    }\n                    return {\n                        is_bold: measurement.isBold,\n                        value: value,\n                    };\n                }),\n            };\n        });\n\n        return {\n            model: this.metaData.resModel,\n            title: this.metaData.title,\n            col_group_headers: colGroupHeaderRows,\n            measure_headers: measureRow,\n            origin_headers: originRow,\n            rows: tableRows,\n            measure_count: measureCount,\n            origin_count: originCount,\n        };\n    }\n    /**\n     * Swap the pivot columns and the rows. The flip operation is synchronous.\n     * However, we must wait for a potential pending reload to complete before\n     * flipping the axes. This method is thus async.\n     */\n    async flip() {\n        await this.race.getCurrentProm();\n\n        // swap the data: the main column and the main row\n        let temp = this.data.rowGroupTree;\n        this.data.rowGroupTree = this.data.colGroupTree;\n        this.data.colGroupTree = temp;\n\n        // we need to update the record metaData: (expanded) row and col groupBys\n        temp = this.metaData.rowGroupBys;\n        this.metaData.rowGroupBys = this.metaData.colGroupBys;\n        this.metaData.colGroupBys = temp;\n        temp = this.metaData.expandedColGroupBys;\n        this.metaData.expandedColGroupBys = this.metaData.expandedRowGroupBys;\n        this.metaData.expandedRowGroupBys = temp;\n\n        function twistKey(key) {\n            return JSON.stringify(JSON.parse(key).reverse());\n        }\n\n        function twist(object) {\n            const newObject = {};\n            Object.keys(object).forEach((key) => {\n                const value = object[key];\n                newObject[twistKey(key)] = value;\n            });\n            return newObject;\n        }\n\n        this.data.measurements = twist(this.data.measurements);\n        this.data.counts = twist(this.data.counts);\n        this.data.groupDomains = twist(this.data.groupDomains);\n\n        this.notify();\n    }\n    /**\n     * Returns a domain representation of a group\n     *\n     * @param {Object} group\n     * @param {Array} group.colValues\n     * @param {Array} group.rowValues\n     * @param {number} group.originIndex\n     * @returns {Array[]}\n     */\n    getGroupDomain(group) {\n        const config = { metaData: this.metaData, data: this.data };\n        return this._getGroupDomain(group, config);\n    }\n    /**\n     * Returns a description of the pivot table.\n     *\n     * @returns {Object}\n     */\n    getTable() {\n        const headers = this._getTableHeaders();\n        return {\n            headers: headers,\n            rows: this._getTableRows(this.data.rowGroupTree, headers[headers.length - 1]),\n        };\n    }\n    /**\n     * Returns the total number of columns of the pivot table.\n     *\n     * @returns {integer}\n     */\n    getTableWidth() {\n        var leafCounts = this._getLeafCounts(this.data.colGroupTree);\n        return leafCounts[JSON.stringify(this.data.colGroupTree.root.values)] + 2;\n    }\n    /**\n     * @returns {boolean} true iff there's no data in the table\n     */\n    hasData() {\n        return this._hasData(this.data);\n    }\n    /**\n     * @override\n     * @param {SearchParams} searchParams\n     */\n    async load(searchParams) {\n        this.searchParams = searchParams;\n        const processedMeasures = processMeasure(searchParams.context.pivot_measures);\n        const activeMeasures = processedMeasures || this.metaData.activeMeasures;\n        const metaData = this._buildMetaData({ activeMeasures });\n        if (!this.reload) {\n            metaData.rowGroupBys =\n                searchParams.context.pivot_row_groupby ||\n                (searchParams.groupBy.length ? searchParams.groupBy : metaData.rowGroupBys);\n            this.reload = true;\n        } else {\n            metaData.rowGroupBys = searchParams.groupBy.length\n                ? searchParams.groupBy\n                : searchParams.context.pivot_row_groupby || metaData.rowGroupBys;\n        }\n        metaData.colGroupBys =\n            searchParams.context.pivot_column_groupby || this.metaData.colGroupBys;\n\n        if (JSON.stringify(metaData.rowGroupBys) !== JSON.stringify(this.metaData.rowGroupBys)) {\n            metaData.expandedRowGroupBys = [];\n        }\n        if (JSON.stringify(metaData.colGroupBys) !== JSON.stringify(this.metaData.colGroupBys)) {\n            metaData.expandedColGroupBys = [];\n        }\n\n        const allActivesMeasures = new Set(this.metaData.activeMeasures);\n        if (processedMeasures) {\n            processedMeasures.forEach((e) => allActivesMeasures.add(e));\n        }\n\n        metaData.measures = computeReportMeasures(metaData.fields, metaData.fieldAttrs, [\n            ...allActivesMeasures,\n        ]);\n        const config = { metaData, data: this.data };\n        await addPropertyFieldDefs(\n            this.orm,\n            metaData.resModel,\n            searchParams.context,\n            metaData.fields,\n            new Set([...metaData.rowGroupBys, ...metaData.colGroupBys])\n        );\n        return this._loadData(config);\n    }\n    /**\n     * Sort the rows, depending on the values of a given column.  This is an\n     * in-memory sort.\n     *\n     * @param {Object} sortedColumn\n     * @param {number[]} sortedColumn.groupId\n     */\n    sortRows(sortedColumn) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const config = { metaData: this.metaData, data: this.data };\n        this._sortRows(sortedColumn, config);\n\n        this.notify();\n    }\n    /**\n     * Toggle the active state for a given measure, then reload the data\n     * if this turns out to be necessary.\n     *\n     * @param {string} fieldName\n     * @returns {Promise}\n     */\n    async toggleMeasure(fieldName) {\n        const metaData = this._buildMetaData();\n        this.nextActiveMeasures = this.nextActiveMeasures || metaData.activeMeasures;\n        metaData.activeMeasures = this.nextActiveMeasures;\n        const index = metaData.activeMeasures.indexOf(fieldName);\n        if (index !== -1) {\n            // in this case, we already have all data in memory, no need to\n            // actually reload a lesser amount of information (but still, we need\n            // to wait in case there is a pending load)\n            metaData.activeMeasures.splice(index, 1);\n            await Promise.resolve(this.race.getCurrentProm());\n            this.metaData = metaData;\n        } else {\n            metaData.activeMeasures.push(fieldName);\n            const config = { metaData, data: this.data };\n            await this._loadData(config);\n            this.useSampleModel = false;\n        }\n        this.nextActiveMeasures = null;\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add labels/values in the provided groupTree. A new leaf is created in\n     * the groupTree with a root object corresponding to the group with given\n     * labels/values.\n     *\n     * @protected\n     * @param {Object} groupTree, either this.data.rowGroupTree or this.data.colGroupTree\n     * @param {string[]} labels\n     * @param {Array} values\n     */\n    _addGroup(groupTree, labels, values) {\n        let tree = groupTree;\n        // we assume here that the group with value value.slice(value.length - 2) has already been added.\n        values.slice(0, values.length - 1).forEach(function (value) {\n            tree = tree.directSubTrees.get(value);\n        });\n        const value = values[values.length - 1];\n        if (tree.directSubTrees.has(value)) {\n            return;\n        }\n        tree.directSubTrees.set(value, {\n            root: {\n                labels: labels,\n                values: values,\n            },\n            directSubTrees: new Map(),\n        });\n    }\n    /**\n     * Return a copy of this.metaData, extended with optional params. This is useful\n     * for async methods that need to modify this.metaData, but it can't be done in\n     * place directly for the model to be concurrency proof (so they work on a\n     * copy and commit it at the end).\n     *\n     * @protected\n     * @param {Object} params\n     * @returns {Object}\n     */\n    _buildMetaData(params) {\n        const metaData = Object.assign({}, this.metaData, params);\n        metaData.activeMeasures = [...metaData.activeMeasures];\n        metaData.colGroupBys = [...metaData.colGroupBys];\n        metaData.rowGroupBys = [...metaData.rowGroupBys];\n        metaData.expandedColGroupBys = [...metaData.expandedColGroupBys];\n        metaData.expandedRowGroupBys = [...metaData.expandedRowGroupBys];\n        metaData.customGroupBys = new Map([...metaData.customGroupBys]);\n        // shallow copy sortedColumn because we never modify groupId in place\n        metaData.sortedColumn = metaData.sortedColumn ? { ...metaData.sortedColumn } : null;\n        if (this.searchParams.comparison) {\n            const domains = this.searchParams.comparison.domains.slice().reverse();\n            metaData.domains = domains.map((d) => d.arrayRepr);\n            metaData.origins = domains.map((d) => d.description);\n        } else {\n            metaData.domains = [this.searchParams.domain];\n            metaData.origins = [\"\"];\n        }\n        Object.defineProperty(metaData, \"fullColGroupBys\", {\n            get() {\n                return metaData.colGroupBys.concat(metaData.expandedColGroupBys);\n            },\n        });\n        Object.defineProperty(metaData, \"fullRowGroupBys\", {\n            get() {\n                return metaData.rowGroupBys.concat(metaData.expandedRowGroupBys);\n            },\n        });\n        return metaData;\n    }\n    /**\n     * Expand a group by using groupBy to split it.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {'row'|'col'} type\n     * @param {Config} config\n     */\n    async _expandGroup(groupId, type, config) {\n        const { metaData } = config;\n        const group = {\n            rowValues: groupId[0],\n            colValues: groupId[1],\n            type: type,\n        };\n        const groupValues = type === \"row\" ? groupId[0] : groupId[1];\n        const groupBys = type === \"row\" ? metaData.fullRowGroupBys : metaData.fullColGroupBys;\n        if (groupValues.length >= groupBys.length) {\n            throw new Error(\"Cannot expand group\");\n        }\n        const groupBy = groupBys[groupValues.length];\n        let leftDivisors;\n        let rightDivisors;\n        if (group.type === \"row\") {\n            leftDivisors = [[groupBy]];\n            rightDivisors = sections(metaData.fullColGroupBys);\n        } else {\n            leftDivisors = sections(metaData.fullRowGroupBys);\n            rightDivisors = [[groupBy]];\n        }\n        const divisors = cartesian(leftDivisors, rightDivisors);\n        delete group.type;\n        await this._subdivideGroup(group, divisors, config);\n    }\n    /**\n     * Find a group with given values in the provided groupTree, either\n     * this.rowGrouptree or this.data.colGroupTree.\n     *\n     * @protected\n     * @param {Object} groupTree\n     * @param {Array} values\n     * @returns {Object}\n     */\n    _findGroup(groupTree, values) {\n        let tree = groupTree;\n        values.slice(0, values.length).forEach((value) => {\n            tree = tree.directSubTrees.get(value);\n        });\n        return tree;\n    }\n    /**\n     * In case originIndex is an array of length 1, thus a single origin\n     * index, returns the given measure for a group determined by the id\n     * groupId and the origin index.\n     * If originIndexes is an array of length 2, we compute the variation\n     * of the measure values for the groups determined by groupId and the\n     * different origin indexes.\n     *\n     * @protected\n     * @param {Array[]} groupId\n     * @param {string} measure\n     * @param {number[]} originIndexes\n     * @param {Config} config\n     * @returns {number}\n     */\n    _getCellValue(groupId, measure, originIndexes, config) {\n        var key = JSON.stringify(groupId);\n        if (!config.data.measurements[key]) {\n            return;\n        }\n        var values = originIndexes.map((originIndex) => {\n            return config.data.measurements[key][originIndex][measure];\n        });\n        if (originIndexes.length > 1) {\n            return computeVariation(values[1], values[0]);\n        } else {\n            return values[0];\n        }\n    }\n    /**\n     * @protected\n     * @param {string[]} rowGroupBy\n     * @param {string[]} colGroupBy\n     * @returns {string[]}\n     */\n    _getGroupBySpecs(rowGroupBy, colGroupBy) {\n        const set = rowGroupBy.concat(colGroupBy).reduce((acc, gb) => {\n            acc.add(this._normalize(gb));\n            return acc;\n        }, new Set());\n        return [...set];\n    }\n    /**\n     * Returns a domain representation of a group\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Array} group.colValues\n     * @param {Array} group.rowValues\n     * @param {number} group.originIndex\n     * @param {Config} config\n     * @returns {Array[]}\n     */\n    _getGroupDomain(group, config) {\n        const { data } = config;\n        var key = JSON.stringify([group.rowValues, group.colValues]);\n        return data.groupDomains[key][group.originIndex];\n    }\n    /**\n     * Returns the group sanitized labels.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} groupBys\n     * @param {Config} config\n     * @returns {string[]}\n     */\n    _getGroupLabels(group, groupBys, config) {\n        return groupBys.map((gb) => {\n            const groupBy = this._normalize(gb);\n            return this._sanitizeLabel(group[groupBy], groupBy, config);\n        });\n    }\n    /**\n     * Returns a promise that returns the annotated read_group results\n     * corresponding to a partition of the given group obtained using the given\n     * rowGroupBy and colGroupBy.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} rowGroupBy\n     * @param {string[]} colGroupBy\n     * @param {Object} params\n     */\n    async _getGroupSubdivision(group, rowGroupBy, colGroupBy, params) {\n        const groupBy = this._getGroupBySpecs(rowGroupBy, colGroupBy);\n        const subGroups = await this._getSubGroups(groupBy, params);\n        return {\n            group,\n            subGroups,\n            rowGroupBy: rowGroupBy,\n            colGroupBy: colGroupBy,\n        };\n    }\n\n    /**\n     * Returns the group sanitized values.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} groupBys\n     * @returns {Array}\n     */\n    _getGroupValues(group, groupBys) {\n        return groupBys.map((gb) => {\n            const groupBy = this._normalize(gb);\n            return this._sanitizeValue(group[groupBy]);\n        });\n    }\n    /**\n     * Returns the leaf counts of each group inside the given tree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @returns {Object} keys are group ids\n     */\n    _getLeafCounts(tree) {\n        const leafCounts = {};\n        let leafCount;\n        if (!tree.directSubTrees.size) {\n            leafCount = 1;\n        } else {\n            leafCount = [...tree.directSubTrees.values()].reduce((acc, subTree) => {\n                const subLeafCounts = this._getLeafCounts(subTree);\n                Object.assign(leafCounts, subLeafCounts);\n                return acc + leafCounts[JSON.stringify(subTree.root.values)];\n            }, 0);\n        }\n\n        leafCounts[JSON.stringify(tree.root.values)] = leafCount;\n        return leafCounts;\n    }\n    /**\n     * Returns the group sanitized measure values for the measures in\n     * this.metaData.activeMeasures (that migth contain '__count', not really a fieldName).\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Config} config\n     * @returns {Array}\n     */\n    _getMeasurements(group, config) {\n        const { metaData } = config;\n        return metaData.activeMeasures.reduce((measurements, measureName) => {\n            var measurement = group[measureName];\n            if (measurement instanceof Array) {\n                // case field is many2one and used as measure and groupBy simultaneously\n                measurement = 1;\n            }\n            if (\n                metaData.measures[measureName].type === \"boolean\" &&\n                measurement instanceof Boolean\n            ) {\n                measurement = measurement ? 1 : 0;\n            }\n            if (metaData.origins.length > 1 && !measurement) {\n                measurement = 0;\n            }\n            measurements[measureName] = measurement;\n            return measurements;\n        }, {});\n    }\n    /**\n     * Returns a description of the measures row of the pivot table\n     *\n     * @protected\n     * @param {Object[]} columns for which measure cells must be generated\n     * @returns {Object[]}\n     */\n    _getMeasuresRow(columns) {\n        const sortedColumn = this.metaData.sortedColumn || {};\n        const measureRow = [];\n\n        columns.forEach((column) => {\n            this.metaData.activeMeasures.forEach((measureName) => {\n                const measureCell = {\n                    groupId: column.groupId,\n                    height: 1,\n                    measure: measureName,\n                    title: this.metaData.measures[measureName].string,\n                    width: 2 * this.metaData.origins.length - 1,\n                };\n                if (\n                    sortedColumn.measure === measureName &&\n                    JSON.stringify(sortedColumn.groupId) === JSON.stringify(column.groupId) // FIXME\n                ) {\n                    measureCell.order = sortedColumn.order;\n                }\n                measureRow.push(measureCell);\n            });\n        });\n\n        return measureRow;\n    }\n    /**\n     * Returns the list of measure specs associated with metaData.activeMeasures, i.e.\n     * a measure 'fieldName' becomes 'fieldName:aggregator' where\n     * aggregator is the value specified on the field 'fieldName' for\n     * the key aggregator.\n     *\n     * @protected\n     * @param {Config} config\n     * @return {string[]}\n     */\n    _getMeasureSpecs(config) {\n        const { metaData } = config;\n        return metaData.activeMeasures.reduce((acc, measure) => {\n            if (measure === \"__count\") {\n                acc.push(measure);\n                return acc;\n            }\n            const field = this.metaData.fields[measure];\n            if (field.type === \"many2one\") {\n                field.aggregator = \"count_distinct\";\n            }\n            if (field.aggregator === undefined) {\n                throw new Error(\n                    \"No aggregate function has been provided for the measure '\" + measure + \"'\"\n                );\n            }\n            acc.push(measure + \":\" + field.aggregator);\n            return acc;\n        }, []);\n    }\n    /**\n     * Make sure that the labels of different many2one values are distinguished\n     * by numbering them if necessary.\n     *\n     * @protected\n     * @param {Array} label\n     * @param {string} fieldName\n     * @param {Config} config\n     * @returns {string}\n     */\n    _getNumberedLabel(label, fieldName, config) {\n        const { data } = config;\n        const id = label[0];\n        const name = label[1];\n        data.numbering[fieldName] = data.numbering[fieldName] || {};\n        data.numbering[fieldName][name] = data.numbering[fieldName][name] || {};\n        const numbers = data.numbering[fieldName][name];\n        numbers[id] = numbers[id] || Object.keys(numbers).length + 1;\n        return name + (numbers[id] > 1 ? \"  (\" + numbers[id] + \")\" : \"\");\n    }\n    /**\n     * Returns a description of the origins row of the pivot table\n     *\n     * @protected\n     * @param {Object[]} columns for which origin cells must be generated\n     * @returns {Object[]}\n     */\n    _getOriginsRow(columns) {\n        const sortedColumn = this.metaData.sortedColumn || {};\n        const originRow = [];\n\n        columns.forEach((column) => {\n            const groupId = column.groupId;\n            const measure = column.measure;\n            const isSorted =\n                sortedColumn.measure === measure &&\n                JSON.stringify(sortedColumn.groupId) === JSON.stringify(groupId); // FIXME\n            const isSortedByOrigin = isSorted && !sortedColumn.originIndexes[1];\n            const isSortedByVariation = isSorted && sortedColumn.originIndexes[1];\n\n            this.metaData.origins.forEach((origin, originIndex) => {\n                const originCell = {\n                    groupId: groupId,\n                    height: 1,\n                    measure: measure,\n                    originIndexes: [originIndex],\n                    title: origin,\n                    width: 1,\n                };\n                if (isSortedByOrigin && sortedColumn.originIndexes[0] === originIndex) {\n                    originCell.order = sortedColumn.order;\n                }\n                originRow.push(originCell);\n\n                if (originIndex > 0) {\n                    const variationCell = {\n                        groupId: groupId,\n                        height: 1,\n                        measure: measure,\n                        originIndexes: [originIndex - 1, originIndex],\n                        title: _t(\"Variation\"),\n                        width: 1,\n                    };\n                    if (isSortedByVariation && sortedColumn.originIndexes[1] === originIndex) {\n                        variationCell.order = sortedColumn.order;\n                    }\n                    originRow.push(variationCell);\n                }\n            });\n        });\n\n        return originRow;\n    }\n    /**\n     * @protected\n     * @param {string[]} groupBy\n     * @param {Object} params\n     * @returns {Promise<Object[]>}\n     */\n    async _getSubGroups(groupBy, params) {\n        const { resModel, groupDomain, measureSpecs, kwargs, mapping } = params;\n        const key = JSON.stringify(groupBy);\n        if (!mapping[key]) {\n            mapping[key] = this.orm.readGroup(resModel, groupDomain, measureSpecs, groupBy, kwargs);\n        }\n        return mapping[key];\n    }\n    /**\n     * Returns the list of header rows of the pivot table: the col group rows\n     * (depending on the col groupbys), the measures row and optionnaly the\n     * origins row (if there are more than one origins).\n     *\n     * @protected\n     * @returns {Object[]}\n     */\n    _getTableHeaders() {\n        const colGroupBys = this.metaData.fullColGroupBys;\n        const height = colGroupBys.length + 1;\n        const measureCount = this.metaData.activeMeasures.length;\n        const originCount = this.metaData.origins.length;\n        const leafCounts = this._getLeafCounts(this.data.colGroupTree);\n        let headers = [];\n        const measureColumns = []; // used to generate the measure cells\n\n        // 1) generate col group rows (total row + one row for each col groupby)\n        const colGroupRows = new Array(height).fill(0).map(() => []);\n        // blank top left cell\n        colGroupRows[0].push({\n            height: height + 1 + (originCount > 1 ? 1 : 0), // + measures rows [+ origins row]\n            title: \"\",\n            width: 1,\n        });\n\n        // col groupby cells with group values\n        /**\n         * Recursive function that generates the header cells corresponding to\n         * the groups of a given tree.\n         *\n         * @param {Object} tree\n         */\n        function generateTreeHeaders(tree, fields) {\n            const group = tree.root;\n            const rowIndex = group.values.length;\n            const row = colGroupRows[rowIndex];\n            const groupId = [[], group.values];\n            const isLeaf = !tree.directSubTrees.size;\n            const leafCount = leafCounts[JSON.stringify(tree.root.values)];\n            const cell = {\n                groupId: groupId,\n                height: isLeaf ? colGroupBys.length + 1 - rowIndex : 1,\n                isLeaf: isLeaf,\n                isFolded: isLeaf && colGroupBys.length > group.values.length,\n                label:\n                    rowIndex === 0\n                        ? undefined\n                        : fields[colGroupBys[rowIndex - 1].split(\":\")[0]].string,\n                title: group.labels.length ? group.labels[group.labels.length - 1] : _t(\"Total\"),\n                width: leafCount * measureCount * (2 * originCount - 1),\n            };\n            row.push(cell);\n            if (isLeaf) {\n                measureColumns.push(cell);\n            }\n\n            [...tree.directSubTrees.values()].forEach((subTree) => {\n                generateTreeHeaders(subTree, fields);\n            });\n        }\n\n        generateTreeHeaders(this.data.colGroupTree, this.metaData.fields);\n        // blank top right cell for 'Total' group (if there is more that one leaf)\n        if (leafCounts[JSON.stringify(this.data.colGroupTree.root.values)] > 1) {\n            var groupId = [[], []];\n            var totalTopRightCell = {\n                groupId: groupId,\n                height: height,\n                title: \"\",\n                width: measureCount * (2 * originCount - 1),\n            };\n            colGroupRows[0].push(totalTopRightCell);\n            measureColumns.push(totalTopRightCell);\n        }\n        headers = headers.concat(colGroupRows);\n\n        // 2) generate measures row\n        var measuresRow = this._getMeasuresRow(measureColumns);\n        headers.push(measuresRow);\n\n        // 3) generate origins row if more than one origin\n        if (originCount > 1) {\n            headers.push(this._getOriginsRow(measuresRow));\n        }\n\n        return headers;\n    }\n    /**\n     * Returns the list of body rows of the pivot table for a given tree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @param {Object[]} columns\n     * @returns {Object[]}\n     */\n    _getTableRows(tree, columns) {\n        let rows = [];\n        const group = tree.root;\n        const rowGroupId = [group.values, []];\n        const title = group.labels.length ? group.labels[group.labels.length - 1] : _t(\"Total\");\n        const indent = group.labels.length;\n        const isLeaf = !tree.directSubTrees.size;\n        const rowGroupBys = this.metaData.fullRowGroupBys;\n\n        const subGroupMeasurements = columns.map((column) => {\n            const colGroupId = column.groupId;\n            const groupIntersectionId = [rowGroupId[0], colGroupId[1]];\n            const measure = column.measure;\n            const originIndexes = column.originIndexes || [0];\n\n            const value = this._getCellValue(groupIntersectionId, measure, originIndexes, {\n                data: this.data,\n            });\n\n            const measurement = {\n                groupId: groupIntersectionId,\n                originIndexes: originIndexes,\n                measure: measure,\n                value: value,\n                isBold: !groupIntersectionId[0].length || !groupIntersectionId[1].length,\n            };\n            return measurement;\n        });\n\n        rows.push({\n            title: title,\n            label:\n                indent === 0\n                    ? undefined\n                    : this.metaData.fields[rowGroupBys[indent - 1].split(\":\")[0]].string,\n            groupId: rowGroupId,\n            indent: indent,\n            isLeaf: isLeaf,\n            isFolded: isLeaf && rowGroupBys.length > group.values.length,\n            subGroupMeasurements: subGroupMeasurements,\n        });\n\n        const subTreeKeys = tree.sortedKeys || [...tree.directSubTrees.keys()];\n        subTreeKeys.forEach((subTreeKey) => {\n            const subTree = tree.directSubTrees.get(subTreeKey);\n            rows = rows.concat(this._getTableRows(subTree, columns));\n        });\n\n        return rows;\n    }\n    /**\n     * returns the height of a given groupTree\n     *\n     * @protected\n     * @param {Object} tree, a groupTree\n     * @returns {number}\n     */\n    _getTreeHeight(tree) {\n        const subTreeHeights = [...tree.directSubTrees.values()].map(\n            this._getTreeHeight.bind(this)\n        );\n        return Math.max(0, Math.max.apply(null, subTreeHeights)) + 1;\n    }\n    /**\n     * @protected\n     * @param {Data} data\n     * @returns {boolean} true iff there's no data in the table\n     */\n    _hasData(data) {\n        return (data.counts[JSON.stringify([[], []])] || []).some((count) => {\n            return count > 0;\n        });\n    }\n    /**\n     * Initialize/Reinitialize data.rowGroupTree, colGroupTree, measurements,\n     * counts and subdivide the group 'Total' as many times it is necessary.\n     * A first subdivision with no groupBy (divisors.slice(0, 1)) is made in\n     * order to see if there is data in the intersection of the group 'Total'\n     * and the various origins. In case there is none, non supplementary rpc\n     * will be done (see the code of subdivideGroup).\n     *\n     * @protected\n     * @param {Config} config\n     */\n    async _loadData(config, prune = true) {\n        config.data = {}; // data will be completely recomputed\n        const { data, metaData } = config;\n        data.rowGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() };\n        data.colGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() };\n        data.measurements = {};\n        data.counts = {};\n        data.groupDomains = {};\n        data.numbering = {};\n        const key = JSON.stringify([[], []]);\n        data.groupDomains[key] = metaData.domains.slice(0);\n\n        const group = { rowValues: [], colValues: [] };\n        const leftDivisors = sections(metaData.fullRowGroupBys);\n        const rightDivisors = sections(metaData.fullColGroupBys);\n        const divisors = cartesian(leftDivisors, rightDivisors);\n\n        await this._subdivideGroup(group, divisors.slice(0, 1), config);\n        await this._subdivideGroup(group, divisors.slice(1), config);\n\n        // keep folded groups folded after the reload if the structure of the table is the same\n        if (prune && this._hasData(data) && this._hasData(this.data)) {\n            if (\n                symmetricalDifference(metaData.rowGroupBys, this.metaData.rowGroupBys).length === 0\n            ) {\n                this._pruneTree(data.rowGroupTree, this.data.rowGroupTree);\n            }\n            if (\n                symmetricalDifference(metaData.colGroupBys, this.metaData.colGroupBys).length === 0\n            ) {\n                this._pruneTree(data.colGroupTree, this.data.colGroupTree);\n            }\n        }\n\n        this.data = config.data;\n        this.metaData = config.metaData;\n    }\n    /**\n     * @protected\n     * @param {string} gb\n     * @returns {string}\n     */\n    _normalize(gb) {\n        const [fieldName, interval] = gb.split(\":\");\n        const field = this.metaData.fields[fieldName];\n        if ([\"date\", \"datetime\"].includes(field.type)) {\n            return `${fieldName}:${interval || \"month\"}`;\n        } else {\n            return fieldName;\n        }\n    }\n    /**\n     * Extract the information in the read_group results (groupSubdivisions)\n     * and develop this.data.rowGroupTree, colGroupTree, measurements, counts, and\n     * groupDomains.\n     * If a column needs to be sorted, the rowGroupTree corresponding to the\n     * group is sorted.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Object[]} groupSubdivisions\n     * @param {Config} config\n     */\n    _prepareData(group, groupSubdivisions, config) {\n        const { data, metaData } = config;\n        const groupRowValues = group.rowValues;\n        let groupRowLabels = [];\n        let rowSubTree = data.rowGroupTree;\n        let root;\n        if (groupRowValues.length) {\n            // we should have labels information on hand! regretful!\n            rowSubTree = this._findGroup(data.rowGroupTree, groupRowValues);\n            root = rowSubTree.root;\n            groupRowLabels = root.labels;\n        }\n\n        const groupColValues = group.colValues;\n        let groupColLabels = [];\n        if (groupColValues.length) {\n            root = this._findGroup(data.colGroupTree, groupColValues).root;\n            groupColLabels = root.labels;\n        }\n\n        groupSubdivisions.forEach((groupSubdivision) => {\n            groupSubdivision.subGroups.forEach((subGroup) => {\n                const rowValues = groupRowValues.concat(\n                    this._getGroupValues(subGroup, groupSubdivision.rowGroupBy)\n                );\n                const rowLabels = groupRowLabels.concat(\n                    this._getGroupLabels(subGroup, groupSubdivision.rowGroupBy, config)\n                );\n\n                const colValues = groupColValues.concat(\n                    this._getGroupValues(subGroup, groupSubdivision.colGroupBy)\n                );\n                const colLabels = groupColLabels.concat(\n                    this._getGroupLabels(subGroup, groupSubdivision.colGroupBy, config)\n                );\n\n                if (!colValues.length && rowValues.length) {\n                    this._addGroup(data.rowGroupTree, rowLabels, rowValues);\n                }\n                if (colValues.length && !rowValues.length) {\n                    this._addGroup(data.colGroupTree, colLabels, colValues);\n                }\n\n                const key = JSON.stringify([rowValues, colValues]);\n                const originIndex = groupSubdivision.group.originIndex;\n\n                if (!(key in data.measurements)) {\n                    data.measurements[key] = metaData.origins.map(() => {\n                        return this._getMeasurements({}, config);\n                    });\n                }\n                data.measurements[key][originIndex] = this._getMeasurements(subGroup, config);\n\n                if (!(key in data.counts)) {\n                    data.counts[key] = metaData.origins.map(function () {\n                        return 0;\n                    });\n                }\n                data.counts[key][originIndex] = subGroup.__count;\n\n                if (!(key in data.groupDomains)) {\n                    data.groupDomains[key] = metaData.origins.map(function () {\n                        return Domain.FALSE.toList();\n                    });\n                }\n                // if __domain is not defined this means that we are in the\n                // case where\n                // groupSubdivision.rowGroupBy = groupSubdivision.rowGroupBy = []\n                if (subGroup.__domain) {\n                    data.groupDomains[key][originIndex] = subGroup.__domain;\n                }\n            });\n        });\n\n        if (metaData.sortedColumn) {\n            this._sortRows(metaData.sortedColumn, config);\n        }\n    }\n    /**\n     * Make any group in tree a leaf if it was a leaf in oldTree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @param {Object} oldTree\n     */\n    _pruneTree(tree, oldTree) {\n        if (!oldTree.directSubTrees.size) {\n            tree.directSubTrees.clear();\n            delete tree.sortedKeys;\n            return;\n        }\n        [...tree.directSubTrees.keys()].forEach((subTreeKey) => {\n            const subTree = tree.directSubTrees.get(subTreeKey);\n            if (!oldTree.directSubTrees.has(subTreeKey)) {\n                subTree.directSubTrees.clear();\n                delete subTree.sortedKeys;\n            } else {\n                const oldSubTree = oldTree.directSubTrees.get(subTreeKey);\n                this._pruneTree(subTree, oldSubTree);\n            }\n        });\n    }\n\n    _getEmptyGroupLabel(fieldName) {\n        return _t(\"None\");\n    }\n\n    /**\n     * Extract from a groupBy value a label.\n     *\n     * @protected\n     * @param {any} value\n     * @param {string} groupBy\n     * @param {Config} config\n     * @returns {string}\n     */\n    _sanitizeLabel(value, groupBy, config) {\n        const { metaData } = config;\n        const fieldName = groupBy.split(\":\")[0];\n        if (\n            fieldName &&\n            metaData.fields[fieldName] &&\n            metaData.fields[fieldName].type === \"boolean\"\n        ) {\n            return value === undefined ? _t(\"None\") : value ? _t(\"Yes\") : _t(\"No\");\n        }\n        if (value === false) {\n            return this._getEmptyGroupLabel(fieldName);\n        }\n        if (value instanceof Array) {\n            return this._getNumberedLabel(value, fieldName, config);\n        }\n        if (\n            fieldName &&\n            metaData.fields[fieldName] &&\n            metaData.fields[fieldName].type === \"selection\"\n        ) {\n            const selected = metaData.fields[fieldName].selection.find((o) => o[0] === value);\n            return selected ? selected[1] : value; // selected should be truthy normally ?!\n        }\n        return value;\n    }\n    /**\n     * Extract from a groupBy value the raw value of that groupBy (discarding\n     * a label if any)\n     *\n     * @protected\n     * @param {any} value\n     * @returns {any}\n     */\n    _sanitizeValue(value) {\n        if (value instanceof Array) {\n            return value[0];\n        }\n        return value;\n    }\n    /**\n     * Get all partitions of a given group using the provided list of divisors\n     * and enrich the objects of this.data.rowGroupTree, colGroupTree,\n     * measurements, counts.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Array[]} divisors\n     * @param {Config} config\n     */\n    async _subdivideGroup(group, divisors, config) {\n        const { data, metaData } = config;\n        const key = JSON.stringify([group.rowValues, group.colValues]);\n\n        const proms = metaData.origins.reduce((acc, origin, originIndex) => {\n            // if no information on group content is available, we fetch data.\n            // if group is known to be empty for the given origin,\n            // we don't need to fetch data for that origin.\n            if (!data.counts[key] || data.counts[key][originIndex] > 0) {\n                const subGroup = {\n                    rowValues: group.rowValues,\n                    colValues: group.colValues,\n                    originIndex: originIndex,\n                };\n                const groupDomain = this._getGroupDomain(subGroup, config);\n                const measureSpecs = this._getMeasureSpecs(config);\n                const resModel = config.metaData.resModel;\n                const kwargs = { lazy: false, context: this.searchParams.context };\n                const mapping = {};\n                divisors.forEach((divisor) => {\n                    acc.push(\n                        this._getGroupSubdivision(subGroup, divisor[0], divisor[1], {\n                            resModel,\n                            groupDomain,\n                            measureSpecs,\n                            kwargs,\n                            mapping,\n                        })\n                    );\n                });\n            }\n            return acc;\n        }, []);\n        const groupSubdivisions = await this.keepLast.add(Promise.all(proms));\n        if (groupSubdivisions.length) {\n            this._prepareData(group, groupSubdivisions, config);\n        }\n    }\n    /**\n     * Sort the rows, depending on the values of a given column.  This is an\n     * in-memory sort.\n     *\n     * @protected\n     * @param {Object} sortedColumn\n     * @param {number[]} sortedColumn.groupId\n     * @param {Config} config\n     */\n    _sortRows(sortedColumn, config) {\n        const metaData = config.metaData || this.metaData;\n        const data = config.data || this.data;\n        const colGroupValues = sortedColumn.groupId[1];\n        sortedColumn.originIndexes = sortedColumn.originIndexes || [0];\n        metaData.sortedColumn = sortedColumn;\n\n        const sortFunction = (tree) => {\n            return (subTreeKey) => {\n                const subTree = tree.directSubTrees.get(subTreeKey);\n                const groupIntersectionId = [subTree.root.values, colGroupValues];\n                const value =\n                    this._getCellValue(\n                        groupIntersectionId,\n                        sortedColumn.measure,\n                        sortedColumn.originIndexes,\n                        { data }\n                    ) || 0;\n                return sortedColumn.order === \"asc\" ? value : -value;\n            };\n        };\n\n        this._sortTree(sortFunction, data.rowGroupTree);\n    }\n    /**\n     * Sort recursively the subTrees of tree using sortFunction.\n     * In the end each node of the tree has its direct children sorted\n     * according to the criterion reprensented by sortFunction.\n     *\n     * @protected\n     * @param {Function} sortFunction\n     * @param {Object} tree\n     */\n    _sortTree(sortFunction, tree) {\n        tree.sortedKeys = sortBy([...tree.directSubTrees.keys()], sortFunction(tree));\n        [...tree.directSubTrees.values()].forEach((subTree) => {\n            this._sortTree(sortFunction, subTree);\n        });\n    }\n}\n", "class ClipboardItemImpl {\n    constructor(items, options = {}) {\n        this.items = items;\n        this.options = options;\n    }\n    get presentationStyle() {\n        return this.options.presentationStyle;\n    }\n    get types() {\n        return Object.keys(this.items);\n    }\n    getType(type) {\n        return this.items[type];\n    }\n}\n\nfunction blobToStr(blob) {\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n        reader.addEventListener(\"load\", () => {\n            const { result } = reader;\n            if (typeof result === \"string\") {\n                resolve(result);\n            } else {\n                reject(\"Cannot read Blob as String\");\n            }\n        });\n        reader.addEventListener(\"error\", () => {\n            reject(\"Cannot read Blob\");\n        });\n        reader.readAsText(blob);\n    });\n}\n\nasync function stringify(item) {\n    const strItem = {};\n    for (const type of item.types) {\n        strItem[type] = await blobToStr(item.getType(type));\n    }\n    return strItem;\n}\n\nasync function write(items) {\n    if (!items[0].getType(\"text/plain\")) {\n        throw new Error(\n            `Calling clipboard.write() without a \"text/plain\" type may result in an empty clipboard on some platforms.`\n        );\n    }\n    const strItem = await stringify(items[0]);\n\n    const stubContainer = document.createElement(\"div\");\n    const shadowContainer = stubContainer.attachShadow({ mode: \"open\" });\n    const stub = document.createElement(\"span\");\n    stub.innerText = strItem[\"text/plain\"];\n    shadowContainer.appendChild(stub);\n    document.body.appendChild(stubContainer);\n\n    const selection = document.getSelection();\n    const range = document.createRange();\n    range.selectNodeContents(stub);\n    selection.removeAllRanges();\n    selection.addRange(range);\n\n    const onCopy = (ev) => {\n        for (const type in strItem) {\n            ev.clipboardData.setData(type, strItem[type]);\n        }\n        ev.preventDefault();\n    };\n    document.addEventListener(\"copy\", onCopy);\n    let result;\n    try {\n        result = document.execCommand(\"copy\");\n    } finally {\n        document.removeEventListener(\"copy\", onCopy);\n    }\n\n    selection.removeAllRanges();\n    document.body.removeChild(stubContainer);\n\n    return result;\n}\n\n/**\n * Only attempt to polyfill browsers that partially implement\n * the Clipboard API (aka. Firefox with `clipboard.write()` and\n * `ClipboardItem` behind a feature flag)\n *\n * Spec: https://w3c.github.io/clipboard-apis/\n */\nif (window.navigator.clipboard) {\n    if (!window.navigator.clipboard.write) {\n        window.navigator.clipboard.write = write.bind(window);\n    }\n    if (!window.ClipboardItem) {\n        window.ClipboardItem = ClipboardItemImpl;\n    }\n}\n", "\n/**\n * This file is generated by o-spreadsheet build tools. Do not edit it.\n * @see https://github.com/odoo/o-spreadsheet\n * @version 18.0.8\n * @date 2024-12-19T07:55:19.099Z\n * @hash 7cf34a618\n */\n\nimport { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, useState, onPatched, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';\n\nfunction createActions(menuItems) {\n    return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);\n}\nlet nextItemId = 1;\nfunction createAction(item) {\n    const name = item.name;\n    const children = item.children;\n    const description = item.description;\n    const icon = item.icon;\n    const secondaryIcon = item.secondaryIcon;\n    const itemId = item.id || nextItemId++;\n    return {\n        id: itemId.toString(),\n        name: typeof name === \"function\" ? name : () => name,\n        isVisible: item.isVisible ? item.isVisible : () => true,\n        isEnabled: item.isEnabled ? item.isEnabled : () => true,\n        isActive: item.isActive,\n        execute: item.execute,\n        children: children\n            ? (env) => {\n                return children\n                    .map((child) => (typeof child === \"function\" ? child(env) : child))\n                    .flat()\n                    .map(createAction);\n            }\n            : () => [],\n        isReadonlyAllowed: item.isReadonlyAllowed || false,\n        separator: item.separator || false,\n        icon: typeof icon === \"function\" ? icon : () => icon || \"\",\n        iconColor: item.iconColor,\n        secondaryIcon: typeof secondaryIcon === \"function\" ? secondaryIcon : () => secondaryIcon || \"\",\n        description: typeof description === \"function\" ? description : () => description || \"\",\n        textColor: item.textColor,\n        sequence: item.sequence || 0,\n        onStartHover: item.onStartHover,\n        onStopHover: item.onStopHover,\n    };\n}\n\n/**\n * Registry\n *\n * The Registry class is basically just a mapping from a string key to an object.\n * It is really not much more than an object. It is however useful for the\n * following reasons:\n *\n * 1. it let us react and execute code when someone add something to the registry\n *   (for example, the FunctionRegistry subclass this for this purpose)\n * 2. it throws an error when the get operation fails\n * 3. it provides a chained API to add items to the registry.\n */\nclass Registry {\n    content = {};\n    /**\n     * Add an item to the registry\n     *\n     * Note that this also returns the registry, so another add method call can\n     * be chained\n     */\n    add(key, value) {\n        this.content[key] = value;\n        return this;\n    }\n    /**\n     * Get an item from the registry\n     */\n    get(key) {\n        /**\n         * Note: key in {} is ~12 times slower than {}[key].\n         * So, we check the absence of key only when the direct access returns\n         * a falsy value. It's done to ensure that the registry can contains falsy values\n         */\n        const content = this.content[key];\n        if (!content) {\n            if (!(key in this.content)) {\n                throw new Error(`Cannot find ${key} in this registry!`);\n            }\n        }\n        return content;\n    }\n    /**\n     * Check if the key is already in the registry\n     */\n    contains(key) {\n        return key in this.content;\n    }\n    /**\n     * Get a list of all elements in the registry\n     */\n    getAll() {\n        return Object.values(this.content);\n    }\n    /**\n     * Get a list of all keys in the registry\n     */\n    getKeys() {\n        return Object.keys(this.content);\n    }\n    /**\n     * Remove an item from the registry\n     */\n    remove(key) {\n        delete this.content[key];\n    }\n}\n\nconst CANVAS_SHIFT = 0.5;\n// Colors\nconst HIGHLIGHT_COLOR = \"#37A850\";\nconst BACKGROUND_GRAY_COLOR = \"#f5f5f5\";\nconst BACKGROUND_HEADER_COLOR = \"#F8F9FA\";\nconst BACKGROUND_HEADER_SELECTED_COLOR = \"#E8EAED\";\nconst BACKGROUND_HEADER_ACTIVE_COLOR = \"#595959\";\nconst TEXT_HEADER_COLOR = \"#666666\";\nconst FIGURE_BORDER_COLOR = \"#c9ccd2\";\nconst SELECTION_BORDER_COLOR = \"#3266ca\";\nconst HEADER_BORDER_COLOR = \"#C0C0C0\";\nconst CELL_BORDER_COLOR = \"#E2E3E3\";\nconst BACKGROUND_CHART_COLOR = \"#FFFFFF\";\nconst DISABLED_TEXT_COLOR = \"#CACACA\";\nconst DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8;\nconst LINK_COLOR = \"#017E84\";\nconst FILTERS_COLOR = \"#188038\";\nconst SEPARATOR_COLOR = \"#E0E2E4\";\nconst ICONS_COLOR = \"#4A4F59\";\nconst HEADER_GROUPING_BACKGROUND_COLOR = \"#F5F5F5\";\nconst HEADER_GROUPING_BORDER_COLOR = \"#999\";\nconst GRID_BORDER_COLOR = \"#E2E3E3\";\nconst FROZEN_PANE_HEADER_BORDER_COLOR = \"#BCBCBC\";\nconst FROZEN_PANE_BORDER_COLOR = \"#DADFE8\";\nconst COMPOSER_ASSISTANT_COLOR = \"#9B359B\";\nconst CHART_WATERFALL_POSITIVE_COLOR = \"#4EA7F2\";\nconst CHART_WATERFALL_NEGATIVE_COLOR = \"#EA6175\";\nconst CHART_WATERFALL_SUBTOTAL_COLOR = \"#AAAAAA\";\nconst GRAY_900 = \"#111827\";\nconst GRAY_300 = \"#D8DADD\";\nconst GRAY_200 = \"#E7E9ED\";\nconst GRAY_100 = \"#F9FAFB\";\nconst TEXT_BODY = \"#374151\";\nconst TEXT_BODY_MUTED = TEXT_BODY + \"C2\";\nconst TEXT_HEADING = \"#111827\";\nconst PRIMARY_BUTTON_BG = \"#714B67\";\nconst PRIMARY_BUTTON_HOVER_BG = \"#624159\";\nconst PRIMARY_BUTTON_ACTIVE_BG = \"#f1edf0\";\nconst BUTTON_BG = GRAY_200;\nconst BUTTON_HOVER_BG = GRAY_300;\nconst BUTTON_HOVER_TEXT_COLOR = \"#111827\";\nconst BUTTON_ACTIVE_BG = \"#e6f2f3\";\nconst BUTTON_ACTIVE_TEXT_COLOR = \"#111827\";\nconst ACTION_COLOR = \"#017E84\";\nconst ACTION_COLOR_HOVER = \"#01585c\";\nconst ALERT_WARNING_BG = \"#FBEBCC\";\nconst ALERT_WARNING_BORDER = \"#F8E2B3\";\nconst ALERT_WARNING_TEXT_COLOR = \"#946D23\";\nconst ALERT_DANGER_BG = \"#D44C591A\";\nconst ALERT_DANGER_BORDER = \"#C34A41\";\nconst ALERT_DANGER_TEXT_COLOR = \"#C34A41\";\nconst ALERT_INFO_BG = \"#CDEDF1\";\nconst ALERT_INFO_BORDER = \"#98DBE2\";\nconst ALERT_INFO_TEXT_COLOR = \"#09414A\";\nconst BADGE_SELECTED_COLOR = \"#E6F2F3\";\nconst DEFAULT_CHART_PADDING = 20;\nconst DEFAULT_CHART_FONT_SIZE = 22;\nconst SCORECARD_GAUGE_CHART_PADDING = 10;\nconst SCORECARD_GAUGE_CHART_FONT_SIZE = 14;\n// Color picker defaults as upper case HEX to match `toHex`helper\nconst COLOR_PICKER_DEFAULTS = [\n    \"#000000\",\n    \"#434343\",\n    \"#666666\",\n    \"#999999\",\n    \"#B7B7B7\",\n    \"#CCCCCC\",\n    \"#D9D9D9\",\n    \"#EFEFEF\",\n    \"#F3F3F3\",\n    \"#FFFFFF\",\n    \"#980000\",\n    \"#FF0000\",\n    \"#FF9900\",\n    \"#FFFF00\",\n    \"#00FF00\",\n    \"#00FFFF\",\n    \"#4A86E8\",\n    \"#0000FF\",\n    \"#9900FF\",\n    \"#FF00FF\",\n    \"#E6B8AF\",\n    \"#F4CCCC\",\n    \"#FCE5CD\",\n    \"#FFF2CC\",\n    \"#D9EAD3\",\n    \"#D0E0E3\",\n    \"#C9DAF8\",\n    \"#CFE2F3\",\n    \"#D9D2E9\",\n    \"#EAD1DC\",\n    \"#DD7E6B\",\n    \"#EA9999\",\n    \"#F9CB9C\",\n    \"#FFE599\",\n    \"#B6D7A8\",\n    \"#A2C4C9\",\n    \"#A4C2F4\",\n    \"#9FC5E8\",\n    \"#B4A7D6\",\n    \"#D5A6BD\",\n    \"#CC4125\",\n    \"#E06666\",\n    \"#F6B26B\",\n    \"#FFD966\",\n    \"#93C47D\",\n    \"#76A5AF\",\n    \"#6D9EEB\",\n    \"#6FA8DC\",\n    \"#8E7CC3\",\n    \"#C27BA0\",\n    \"#A61C00\",\n    \"#CC0000\",\n    \"#E69138\",\n    \"#F1C232\",\n    \"#6AA84F\",\n    \"#45818E\",\n    \"#3C78D8\",\n    \"#3D85C6\",\n    \"#674EA7\",\n    \"#A64D79\",\n    \"#85200C\",\n    \"#990000\",\n    \"#B45F06\",\n    \"#BF9000\",\n    \"#38761D\",\n    \"#134F5C\",\n    \"#1155CC\",\n    \"#0B5394\",\n    \"#351C75\",\n    \"#741B47\",\n    \"#5B0F00\",\n    \"#660000\",\n    \"#783F04\",\n    \"#7F6000\",\n    \"#274E13\",\n    \"#0C343D\",\n    \"#1C4587\",\n    \"#073763\",\n    \"#20124D\",\n    \"#4C1130\",\n];\n// Dimensions\nconst MIN_ROW_HEIGHT = 10;\nconst MIN_COL_WIDTH = 5;\nconst HEADER_HEIGHT = 26;\nconst HEADER_WIDTH = 48;\nconst TOPBAR_HEIGHT = 63;\nconst TOPBAR_TOOLBAR_HEIGHT = 34;\nconst BOTTOMBAR_HEIGHT = 36;\nconst DEFAULT_CELL_WIDTH = 96;\nconst DEFAULT_CELL_HEIGHT = 23;\nconst SCROLLBAR_WIDTH = 15;\nconst AUTOFILL_EDGE_LENGTH = 8;\nconst ICON_EDGE_LENGTH = 18;\nconst MIN_CF_ICON_MARGIN = 4;\nconst MIN_CELL_TEXT_MARGIN = 4;\nconst CF_ICON_EDGE_LENGTH = 15;\nconst PADDING_AUTORESIZE_VERTICAL = 3;\nconst PADDING_AUTORESIZE_HORIZONTAL = MIN_CELL_TEXT_MARGIN;\nconst GROUP_LAYER_WIDTH = 21;\nconst GRID_ICON_MARGIN = 2;\nconst GRID_ICON_EDGE_LENGTH = 17;\nconst FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT;\n// Menus\nconst MENU_WIDTH = 250;\nconst MENU_VERTICAL_PADDING = 6;\nconst MENU_ITEM_HEIGHT = 26;\nconst MENU_ITEM_PADDING_HORIZONTAL = 11;\nconst MENU_ITEM_PADDING_VERTICAL = 4;\nconst MENU_SEPARATOR_BORDER_WIDTH = 1;\nconst MENU_SEPARATOR_PADDING = 5;\n// Style\nconst DEFAULT_STYLE = {\n    align: \"left\",\n    verticalAlign: \"bottom\",\n    wrapping: \"overflow\",\n    bold: false,\n    italic: false,\n    strikethrough: false,\n    underline: false,\n    fontSize: 10,\n    fillColor: \"\",\n    textColor: \"\",\n};\nconst DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign;\nconst DEFAULT_WRAPPING_MODE = DEFAULT_STYLE.wrapping;\n// Fonts\nconst DEFAULT_FONT_WEIGHT = \"400\";\nconst DEFAULT_FONT_SIZE = DEFAULT_STYLE.fontSize;\nconst HEADER_FONT_SIZE = 11;\nconst DEFAULT_FONT = \"'Roboto', arial\";\n// Borders\nconst DEFAULT_BORDER_DESC = { style: \"thin\", color: \"#000000\" };\n// Max Number of history steps kept in memory\nconst MAX_HISTORY_STEPS = 99;\n// Id of the first revision\nconst DEFAULT_REVISION_ID = \"START_REVISION\";\n// Figure\nconst DEFAULT_FIGURE_HEIGHT = 335;\nconst DEFAULT_FIGURE_WIDTH = 536;\nconst FIGURE_BORDER_WIDTH = 1;\nconst MIN_FIG_SIZE = 80;\n// Chart\nconst MAX_CHAR_LABEL = 20;\nconst FIGURE_ID_SPLITTER = \"??\";\nconst DEFAULT_GAUGE_LOWER_COLOR = \"#EA6175\";\nconst DEFAULT_GAUGE_MIDDLE_COLOR = \"#FFD86D\";\nconst DEFAULT_GAUGE_UPPER_COLOR = \"#43C5B1\";\nconst DEFAULT_SCORECARD_BASELINE_MODE = \"difference\";\nconst DEFAULT_SCORECARD_BASELINE_COLOR_UP = \"#43C5B1\";\nconst DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = \"#EA6175\";\nconst LINE_FILL_TRANSPARENCY = 0.4;\n// session\nconst DEBOUNCE_TIME = 200;\nconst MESSAGE_VERSION = 1;\n// Sheets\nconst FORBIDDEN_SHEETNAME_CHARS = [\"'\", \"*\", \"?\", \"/\", \"\\\\\", \"[\", \"]\"];\nconst FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX = /'|\\*|\\?|\\/|\\\\|\\[|\\]/;\n// Cells\nconst FORMULA_REF_IDENTIFIER = \"|\";\n// Components\nvar ComponentsImportance;\n(function (ComponentsImportance) {\n    ComponentsImportance[ComponentsImportance[\"Grid\"] = 0] = \"Grid\";\n    ComponentsImportance[ComponentsImportance[\"Highlight\"] = 5] = \"Highlight\";\n    ComponentsImportance[ComponentsImportance[\"HeaderGroupingButton\"] = 6] = \"HeaderGroupingButton\";\n    ComponentsImportance[ComponentsImportance[\"Figure\"] = 10] = \"Figure\";\n    ComponentsImportance[ComponentsImportance[\"ScrollBar\"] = 15] = \"ScrollBar\";\n    ComponentsImportance[ComponentsImportance[\"GridPopover\"] = 19] = \"GridPopover\";\n    ComponentsImportance[ComponentsImportance[\"GridComposer\"] = 20] = \"GridComposer\";\n    ComponentsImportance[ComponentsImportance[\"Dropdown\"] = 21] = \"Dropdown\";\n    ComponentsImportance[ComponentsImportance[\"IconPicker\"] = 25] = \"IconPicker\";\n    ComponentsImportance[ComponentsImportance[\"TopBarComposer\"] = 30] = \"TopBarComposer\";\n    ComponentsImportance[ComponentsImportance[\"Popover\"] = 35] = \"Popover\";\n    ComponentsImportance[ComponentsImportance[\"FigureAnchor\"] = 1000] = \"FigureAnchor\";\n    ComponentsImportance[ComponentsImportance[\"FigureSnapLine\"] = 1001] = \"FigureSnapLine\";\n})(ComponentsImportance || (ComponentsImportance = {}));\nlet DEFAULT_SHEETVIEW_SIZE = 0;\nfunction getDefaultSheetViewSize() {\n    return DEFAULT_SHEETVIEW_SIZE;\n}\nfunction setDefaultSheetViewSize(size) {\n    DEFAULT_SHEETVIEW_SIZE = size;\n}\nconst MAXIMAL_FREEZABLE_RATIO = 0.85;\nconst NEWLINE = \"\\n\";\nconst FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];\n// Pivot\nconst PIVOT_TABLE_CONFIG = {\n    hasFilters: false,\n    totalRow: false,\n    firstColumn: true,\n    lastColumn: false,\n    numberOfHeaders: 1,\n    bandedRows: true,\n    bandedColumns: false,\n    styleId: \"TableStyleMedium5\",\n    automaticAutofill: false,\n};\nconst DEFAULT_CURRENCY = {\n    symbol: \"$\",\n    position: \"before\",\n    decimalPlaces: 2,\n    code: \"\",\n    name: \"Dollar\",\n};\n\n//------------------------------------------------------------------------------\n// Miscellaneous\n//------------------------------------------------------------------------------\nconst sanitizeSheetNameRegex = new RegExp(FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX, \"g\");\n/**\n * Remove quotes from a quoted string\n * ```js\n * removeStringQuotes('\"Hello\"')\n * > 'Hello'\n * ```\n */\nfunction removeStringQuotes(str) {\n    if (str[0] === '\"') {\n        str = str.slice(1);\n    }\n    if (str[str.length - 1] === '\"' && str[str.length - 2] !== \"\\\\\") {\n        return str.slice(0, str.length - 1);\n    }\n    return str;\n}\nfunction isCloneable(obj) {\n    return \"clone\" in obj && obj.clone instanceof Function;\n}\n/**\n * Escapes a string to use as a literal string in a RegExp.\n * @url https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping\n */\nfunction escapeRegExp(str) {\n    return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n/**\n * Deep copy arrays, plain objects and primitive values.\n * Throws an error for other types such as class instances.\n * Sparse arrays remain sparse.\n */\nfunction deepCopy(obj) {\n    const result = Array.isArray(obj) ? [] : {};\n    switch (typeof obj) {\n        case \"object\": {\n            if (obj === null) {\n                return obj;\n            }\n            else if (isCloneable(obj)) {\n                return obj.clone();\n            }\n            else if (!(isPlainObject(obj) || obj instanceof Array)) {\n                throw new Error(\"Unsupported type: only objects and arrays are supported\");\n            }\n            for (const key in obj) {\n                result[key] = deepCopy(obj[key]);\n            }\n            return result;\n        }\n        case \"number\":\n        case \"string\":\n        case \"boolean\":\n        case \"function\":\n        case \"undefined\":\n            return obj;\n        default:\n            throw new Error(`Unsupported type: ${typeof obj}`);\n    }\n}\n/**\n * Check if the object is a plain old javascript object.\n */\nfunction isPlainObject(obj) {\n    return (typeof obj === \"object\" &&\n        obj !== null &&\n        // obj.constructor can be undefined when there's no prototype (`Object.create(null, {})`)\n        (obj?.constructor === Object || obj?.constructor === undefined));\n}\n/**\n * Sanitize the name of a sheet, by eventually removing quotes\n * @param sheetName name of the sheet, potentially quoted with single quotes\n */\nfunction getUnquotedSheetName(sheetName) {\n    return unquote(sheetName, \"'\");\n}\nfunction unquote(string, quoteChar = '\"') {\n    if (string.startsWith(quoteChar)) {\n        string = string.slice(1);\n    }\n    if (string.endsWith(quoteChar)) {\n        string = string.slice(0, -1);\n    }\n    return string;\n}\n/**\n * Add quotes around the sheet name or any symbol name if it contains at least one non alphanumeric character\n * '\\w' captures [0-9][a-z][A-Z] and _.\n * @param symbolName Name of the sheet or symbol\n */\nfunction getCanonicalSymbolName(symbolName) {\n    if (symbolName.match(/\\w/g)?.length !== symbolName.length) {\n        symbolName = `'${symbolName}'`;\n    }\n    return symbolName;\n}\n/** Replace the excel-excluded characters of a sheetName */\nfunction sanitizeSheetName(sheetName, replacementChar = \" \") {\n    return sheetName.replace(sanitizeSheetNameRegex, replacementChar);\n}\nfunction clip(val, min, max) {\n    return val < min ? min : val > max ? max : val;\n}\n/**\n * Create a range from start (included) to end (excluded).\n * range(10, 13) => [10, 11, 12]\n * range(2, 8, 2) => [2, 4, 6]\n */\nfunction range(start, end, step = 1) {\n    if (end <= start && step > 0) {\n        return [];\n    }\n    if (step === 0) {\n        throw new Error(\"range() step must not be zero\");\n    }\n    const length = Math.ceil(Math.abs((end - start) / step));\n    const array = Array(length);\n    for (let i = 0; i < length; i++) {\n        array[i] = start + i * step;\n    }\n    return array;\n}\n/**\n * Groups consecutive numbers.\n * The input array is assumed to be sorted\n * @param numbers\n */\nfunction groupConsecutive(numbers) {\n    return numbers.reduce((groups, currentRow, index, rows) => {\n        if (Math.abs(currentRow - rows[index - 1]) === 1) {\n            const lastGroup = groups[groups.length - 1];\n            lastGroup.push(currentRow);\n        }\n        else {\n            groups.push([currentRow]);\n        }\n        return groups;\n    }, []);\n}\n/**\n * Create one generator from two generators by linking\n * each item of the first generator to the next item of\n * the second generator.\n *\n * Let's say generator G1 yields A, B, C and generator G2 yields X, Y, Z.\n * The resulting generator of `linkNext(G1, G2)` will yield A', B', C'\n * where `A' = A & {next: Y}`, `B' = B & {next: Z}` and `C' = C & {next: undefined}`\n * @param generator\n * @param nextGenerator\n */\nfunction* linkNext(generator, nextGenerator) {\n    nextGenerator.next();\n    for (const item of generator) {\n        const nextItem = nextGenerator.next();\n        yield {\n            ...item,\n            next: nextItem.done ? undefined : nextItem.value,\n        };\n    }\n}\nfunction isBoolean(str) {\n    const upperCased = str.toUpperCase();\n    return upperCased === \"TRUE\" || upperCased === \"FALSE\";\n}\nconst MARKDOWN_LINK_REGEX = /^\\[(.+)\\]\\((.+)\\)$/;\n//link must start with http or https\n//https://stackoverflow.com/a/3809435/4760614\nconst WEB_LINK_REGEX = /^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$/;\nfunction isMarkdownLink(str) {\n    return MARKDOWN_LINK_REGEX.test(str);\n}\n/**\n * Check if the string is a web link.\n * e.g. http://odoo.com\n */\nfunction isWebLink(str) {\n    return WEB_LINK_REGEX.test(str);\n}\n/**\n * Build a markdown link from a label and an url\n */\nfunction markdownLink(label, url) {\n    return `[${label}](${url})`;\n}\nfunction parseMarkdownLink(str) {\n    const matches = str.match(MARKDOWN_LINK_REGEX) || [];\n    const label = matches[1];\n    const url = matches[2];\n    if (!label || !url) {\n        throw new Error(`Could not parse markdown link ${str}.`);\n    }\n    return {\n        label,\n        url,\n    };\n}\nconst O_SPREADSHEET_LINK_PREFIX = \"o-spreadsheet://\";\nfunction isSheetUrl(url) {\n    return url.startsWith(O_SPREADSHEET_LINK_PREFIX);\n}\nfunction buildSheetLink(sheetId) {\n    return `${O_SPREADSHEET_LINK_PREFIX}${sheetId}`;\n}\n/**\n * Parse a sheet link and return the sheet id\n */\nfunction parseSheetUrl(sheetLink) {\n    if (sheetLink.startsWith(O_SPREADSHEET_LINK_PREFIX)) {\n        return sheetLink.substr(O_SPREADSHEET_LINK_PREFIX.length);\n    }\n    throw new Error(`${sheetLink} is not a valid sheet link`);\n}\n/**\n * This helper function can be used as a type guard when filtering arrays.\n * const foo: number[] = [1, 2, undefined, 4].filter(isDefined)\n */\nfunction isDefined(argument) {\n    return argument !== undefined;\n}\nfunction isNotNull(argument) {\n    return argument !== null;\n}\n/**\n * Check if all the values of an object, and all the values of the objects inside of it, are undefined.\n */\nfunction isObjectEmptyRecursive(argument) {\n    if (argument === undefined)\n        return true;\n    return Object.values(argument).every((value) => typeof value === \"object\" ? isObjectEmptyRecursive(value) : !value);\n}\n/**\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing.\n *\n * Also decorate the argument function with two methods: stopDebounce and isDebouncePending.\n *\n * Inspired by https://davidwalsh.name/javascript-debounce-function\n */\nfunction debounce(func, wait, immediate) {\n    let timeout = undefined;\n    const debounced = function () {\n        const context = this;\n        const args = Array.from(arguments);\n        function later() {\n            timeout = undefined;\n            if (!immediate) {\n                func.apply(context, args);\n            }\n        }\n        const callNow = immediate && !timeout;\n        clearTimeout(timeout);\n        timeout = setTimeout(later, wait);\n        if (callNow) {\n            func.apply(context, args);\n        }\n    };\n    debounced.isDebouncePending = () => timeout !== undefined;\n    debounced.stopDebounce = () => {\n        clearTimeout(timeout);\n    };\n    return debounced;\n}\n/**\n * Creates a batched version of a callback so that all calls to it in the same\n * microtick will only call the original callback once.\n *\n * @param callback the callback to batch\n * @returns a batched version of the original callback\n *\n * Copied from odoo/owl repo.\n */\nfunction batched(callback) {\n    let scheduled = false;\n    return async (...args) => {\n        if (!scheduled) {\n            scheduled = true;\n            await Promise.resolve();\n            scheduled = false;\n            callback(...args);\n        }\n    };\n}\n/*\n * Concatenate an array of strings.\n */\nfunction concat(chars) {\n    // ~40% faster than chars.join(\"\")\n    let output = \"\";\n    for (let i = 0, len = chars.length; i < len; i++) {\n        output += chars[i];\n    }\n    return output;\n}\n/**\n * Lazy value computed by the provided function.\n */\nfunction lazy(fn) {\n    let isMemoized = false;\n    let memo;\n    const lazyValue = () => {\n        if (!isMemoized) {\n            memo = fn instanceof Function ? fn() : fn;\n            isMemoized = true;\n        }\n        return memo;\n    };\n    lazyValue.map = (callback) => lazy(() => callback(lazyValue()));\n    return lazyValue;\n}\n/**\n * Find the next defined value after the given index in an array of strings. If there is no defined value\n * after the index, return the closest defined value before the index. Return an empty string if no\n * defined value was found.\n *\n */\nfunction findNextDefinedValue(arr, index) {\n    let value = arr.slice(index).find((val) => val);\n    if (!value) {\n        value = arr\n            .slice(0, index)\n            .reverse()\n            .find((val) => val);\n    }\n    return value || \"\";\n}\n/** Get index of first header added by an ADD_COLUMNS_ROWS command */\nfunction getAddHeaderStartIndex(position, base) {\n    return position === \"after\" ? base + 1 : base;\n}\n/**\n * Compares two objects.\n */\nfunction deepEquals(o1, o2) {\n    if (o1 === o2)\n        return true;\n    if ((o1 && !o2) || (o2 && !o1))\n        return false;\n    if (typeof o1 !== typeof o2)\n        return false;\n    if (typeof o1 !== \"object\")\n        return false;\n    // Objects can have different keys if the values are undefined\n    for (const key in o2) {\n        if (!(key in o1) && o2[key] !== undefined) {\n            return false;\n        }\n    }\n    for (const key in o1) {\n        if (typeof o1[key] !== typeof o2[key])\n            return false;\n        if (typeof o1[key] === \"object\") {\n            if (!deepEquals(o1[key], o2[key]))\n                return false;\n        }\n        else {\n            if (o1[key] !== o2[key])\n                return false;\n        }\n    }\n    return true;\n}\n/**\n * Compares two arrays.\n * For performance reasons, this function is to be preferred\n * to 'deepEquals' in the case we know that the inputs are arrays.\n */\nfunction deepEqualsArray(arr1, arr2) {\n    if (arr1.length !== arr2.length) {\n        return false;\n    }\n    for (let i = 0; i < arr1.length; i++) {\n        if (!deepEquals(arr1[i], arr2[i])) {\n            return false;\n        }\n    }\n    return true;\n}\n/** Check if the given array contains all the values of the other array. */\nfunction includesAll(arr, values) {\n    return values.every((value) => arr.includes(value));\n}\n/**\n * Return an object with all the keys in the object that have a falsy value removed.\n */\nfunction removeFalsyAttributes(obj) {\n    if (!obj)\n        return obj;\n    const cleanObject = { ...obj };\n    Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]);\n    return cleanObject;\n}\n/**\n * Equivalent to \"\\s\" in regexp, minus the new lines characters\n *\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes\n */\nconst whiteSpaceSpecialCharacters = [\n    \" \",\n    \"\\t\",\n    \"\\f\",\n    \"\\v\",\n    String.fromCharCode(parseInt(\"00a0\", 16)),\n    String.fromCharCode(parseInt(\"1680\", 16)),\n    String.fromCharCode(parseInt(\"2000\", 16)),\n    String.fromCharCode(parseInt(\"200a\", 16)),\n    String.fromCharCode(parseInt(\"2028\", 16)),\n    String.fromCharCode(parseInt(\"2029\", 16)),\n    String.fromCharCode(parseInt(\"202f\", 16)),\n    String.fromCharCode(parseInt(\"205f\", 16)),\n    String.fromCharCode(parseInt(\"3000\", 16)),\n    String.fromCharCode(parseInt(\"feff\", 16)),\n];\nconst whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join(\"|\"), \"g\");\nconst newLineRegexp = /(\\r\\n|\\r)/g;\n/**\n * Replace all different newlines characters by \\n\n */\nfunction replaceNewLines(text) {\n    if (!text)\n        return \"\";\n    return text.replace(newLineRegexp, NEWLINE);\n}\n/**\n * Determine if the numbers are consecutive.\n */\nfunction isConsecutive(iterable) {\n    const array = Array.from(iterable).sort((a, b) => a - b); // sort numerically rather than lexicographically\n    for (let i = 1; i < array.length; i++) {\n        if (array[i] - array[i - 1] !== 1) {\n            return false;\n        }\n    }\n    return true;\n}\n/**\n * Creates a version of the function that's memoized on the value of its first\n * argument, if any.\n */\nfunction memoize(func) {\n    const cache = new Map();\n    const funcName = func.name ? func.name + \" (memoized)\" : \"memoized\";\n    return {\n        [funcName](...args) {\n            if (!cache.has(args[0])) {\n                cache.set(args[0], func(...args));\n            }\n            return cache.get(args[0]);\n        },\n    }[funcName];\n}\nfunction removeIndexesFromArray(array, indexes) {\n    return array.filter((_, index) => !indexes.includes(index));\n}\nfunction insertItemsAtIndex(array, items, index) {\n    const newArray = [...array];\n    newArray.splice(index, 0, ...items);\n    return newArray;\n}\nfunction replaceItemAtIndex(array, newItem, index) {\n    const newArray = [...array];\n    newArray.splice(index, 1, newItem);\n    return newArray;\n}\nfunction trimContent(content) {\n    const contentLines = content.split(\"\\n\");\n    return contentLines.map((line) => line.replace(/\\s+/g, \" \").trim()).join(\"\\n\");\n}\nfunction isNumberBetween(value, min, max) {\n    if (min > max) {\n        return isNumberBetween(value, max, min);\n    }\n    return value >= min && value <= max;\n}\n/**\n * Get a Regex for the find & replace that matches the given search string and options.\n */\nfunction getSearchRegex(searchStr, searchOptions) {\n    let searchValue = escapeRegExp(searchStr);\n    const flags = !searchOptions.matchCase ? \"i\" : \"\";\n    if (searchOptions.exactMatch) {\n        searchValue = `^${searchValue}$`;\n    }\n    return RegExp(searchValue, flags);\n}\n/**\n * Alternative to Math.max that works with large arrays.\n * Typically useful for arrays bigger than 100k elements.\n */\nfunction largeMax(array) {\n    let len = array.length;\n    if (len < 100_000)\n        return Math.max(...array);\n    let max = -Infinity;\n    while (len--) {\n        max = array[len] > max ? array[len] : max;\n    }\n    return max;\n}\n/**\n * Alternative to Math.min that works with large arrays.\n * Typically useful for arrays bigger than 100k elements.\n */\nfunction largeMin(array) {\n    let len = array.length;\n    if (len < 100_000)\n        return Math.min(...array);\n    let min = +Infinity;\n    while (len--) {\n        min = array[len] < min ? array[len] : min;\n    }\n    return min;\n}\nclass TokenizingChars {\n    text;\n    currentIndex = 0;\n    current;\n    constructor(text) {\n        this.text = text;\n        this.current = text[0];\n    }\n    shift() {\n        const current = this.current;\n        const next = this.text[++this.currentIndex];\n        this.current = next;\n        return current;\n    }\n    advanceBy(length) {\n        this.currentIndex += length;\n        this.current = this.text[this.currentIndex];\n    }\n    isOver() {\n        return this.currentIndex >= this.text.length;\n    }\n    remaining() {\n        return this.text.substring(this.currentIndex);\n    }\n    currentStartsWith(str) {\n        if (this.current !== str[0]) {\n            return false;\n        }\n        for (let j = 1; j < str.length; j++) {\n            if (this.text[this.currentIndex + j] !== str[j]) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n/**\n * Remove duplicates from an array.\n *\n * @param array The array to remove duplicates from.\n * @param cb A callback to get an element value.\n */\nfunction removeDuplicates$1(array, cb = (a) => a) {\n    const set = new Set();\n    return array.filter((item) => {\n        const key = cb(item);\n        if (set.has(key)) {\n            return false;\n        }\n        set.add(key);\n        return true;\n    });\n}\n/**\n * Similar to transposing and array, but with POJOs instead of arrays. Useful, for example, when manipulating\n * a POJO grid[col][row] and you want to transpose it to grid[row][col].\n *\n * The resulting object is created such as result[key1][key2] = pojo[key2][key1]\n */\nfunction transpose2dPOJO(pojo) {\n    const result = {};\n    for (const key in pojo) {\n        for (const subKey in pojo[key]) {\n            if (!result[subKey]) {\n                result[subKey] = {};\n            }\n            result[subKey][key] = pojo[key][subKey];\n        }\n    }\n    return result;\n}\n\nconst RBA_REGEX = /rgba?\\(|\\s+|\\)/gi;\nconst HEX_MATCH = /^#([A-F\\d]{2}){3,4}$/;\nconst colors$1 = [\n    \"#eb6d00\",\n    \"#0074d9\",\n    \"#ad8e00\",\n    \"#169ed4\",\n    \"#b10dc9\",\n    \"#00a82d\",\n    \"#00a3a3\",\n    \"#f012be\",\n    \"#3d9970\",\n    \"#111111\",\n    \"#62A300\",\n    \"#ff4136\",\n    \"#949494\",\n    \"#85144b\",\n    \"#001f3f\",\n];\n/*\n * transform a color number (R * 256^2 + G * 256 + B) into classic hex6 value\n * */\nfunction colorNumberString(color) {\n    return toHex(color.toString(16).padStart(6, \"0\"));\n}\n/**\n * Converts any CSS color value to a standardized hex6 value.\n * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].\n *\n * [1] under the form rgb(r, g, b, a?) or rgba(r, g, b, a?)\n * with r,g,b \u2208 [0, 255] and a \u2208 [0, 1]\n *\n * toHex(\"#ABC\")\n * >> \"#AABBCC\"\n *\n * toHex(\"#AAAFFF\")\n * >> \"#AAAFFF\"\n *\n * toHex(\"rgb(30, 80, 16)\")\n * >> \"#1E5010\"\n *\n *  * toHex(\"rgb(30, 80, 16, 0.5)\")\n * >> \"#1E501080\"\n *\n */\nfunction toHex(color) {\n    let hexColor = color;\n    if (color.startsWith(\"rgb\")) {\n        hexColor = rgbaStringToHex(color);\n    }\n    else {\n        hexColor = color.replace(\"#\", \"\").toUpperCase();\n        if (hexColor.length === 3 || hexColor.length === 4) {\n            hexColor = hexColor.split(\"\").reduce((acc, h) => acc + h + h, \"\");\n        }\n        hexColor = `#${hexColor}`;\n    }\n    if (!HEX_MATCH.test(hexColor)) {\n        throw new Error(`invalid color input: ${color}`);\n    }\n    return hexColor;\n}\nfunction isColorValid(color) {\n    try {\n        toHex(color);\n        return true;\n    }\n    catch (error) {\n        return false;\n    }\n}\nfunction isHSLAValid(color) {\n    try {\n        hslaToHex(color);\n        return true;\n    }\n    catch (error) {\n        return false;\n    }\n}\nconst isColorValueValid = (v) => v >= 0 && v <= 255;\nfunction rgba(r, g, b, a = 1) {\n    const isInvalid = !isColorValueValid(r) || !isColorValueValid(g) || !isColorValueValid(b) || a < 0 || a > 1;\n    if (isInvalid) {\n        throw new Error(`Invalid RGBA values ${[r, g, b, a]}`);\n    }\n    return { a, b, g, r };\n}\n/**\n * The relative brightness of a point in the colorspace, normalized to 0 for\n * darkest black and 1 for lightest white.\n * https://www.w3.org/TR/WCAG20/#relativeluminancedef\n */\nfunction relativeLuminance(color) {\n    let { r, g, b } = colorToRGBA(color);\n    r /= 255;\n    g /= 255;\n    b /= 255;\n    const toLinearValue = (c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);\n    const R = toLinearValue(r);\n    const G = toLinearValue(g);\n    const B = toLinearValue(b);\n    return 0.2126 * R + 0.7152 * G + 0.0722 * B;\n}\n/**\n * Convert a CSS rgb color string to a standardized hex6 color value.\n *\n * rgbaStringToHex(\"rgb(30, 80, 16)\")\n * >> \"#1E5010\"\n *\n * rgbaStringToHex(\"rgba(30, 80, 16, 0.5)\")\n * >> \"#1E501080\"\n *\n * DOES NOT SUPPORT NON INTEGER RGB VALUES\n */\nfunction rgbaStringToHex(color) {\n    const stringVals = color.replace(RBA_REGEX, \"\").split(\",\");\n    let alphaHex = 255;\n    if (stringVals.length !== 3 && stringVals.length !== 4) {\n        throw new Error(\"invalid color\");\n    }\n    else if (stringVals.length === 4) {\n        const alpha = parseFloat(stringVals.pop() || \"1\");\n        alphaHex = Math.round((alpha || 1) * 255);\n    }\n    const vals = stringVals.map((val) => parseInt(val, 10));\n    if (alphaHex !== 255) {\n        vals.push(alphaHex);\n    }\n    return \"#\" + concat(vals.map((value) => value.toString(16).padStart(2, \"0\"))).toUpperCase();\n}\n/**\n * RGBA to HEX representation (#RRGGBBAA).\n *\n * https://css-tricks.com/converting-color-spaces-in-javascript/\n */\nfunction rgbaToHex(rgba) {\n    let r = rgba.r.toString(16);\n    let g = rgba.g.toString(16);\n    let b = rgba.b.toString(16);\n    let a = Math.round(rgba.a * 255).toString(16);\n    if (r.length === 1)\n        r = \"0\" + r;\n    if (g.length === 1)\n        g = \"0\" + g;\n    if (b.length === 1)\n        b = \"0\" + b;\n    if (a.length === 1)\n        a = \"0\" + a;\n    if (a === \"ff\")\n        a = \"\";\n    return (\"#\" + r + g + b + a).toUpperCase();\n}\n/**\n * Color string to RGBA representation\n */\nfunction colorToRGBA(color) {\n    color = toHex(color);\n    let r;\n    let g;\n    let b;\n    let a;\n    if (color.length === 7) {\n        r = parseInt(color[1] + color[2], 16);\n        g = parseInt(color[3] + color[4], 16);\n        b = parseInt(color[5] + color[6], 16);\n        a = 255;\n    }\n    else if (color.length === 9) {\n        r = parseInt(color[1] + color[2], 16);\n        g = parseInt(color[3] + color[4], 16);\n        b = parseInt(color[5] + color[6], 16);\n        a = parseInt(color[7] + color[8], 16);\n    }\n    else {\n        throw new Error(\"Invalid color\");\n    }\n    a = +(a / 255).toFixed(3);\n    return { a, r, g, b };\n}\n/**\n * HSLA to RGBA.\n *\n * https://css-tricks.com/converting-color-spaces-in-javascript/\n */\nfunction hslaToRGBA(hsla) {\n    hsla = { ...hsla };\n    // Must be fractions of 1\n    hsla.s /= 100;\n    hsla.l /= 100;\n    let c = (1 - Math.abs(2 * hsla.l - 1)) * hsla.s;\n    let x = c * (1 - Math.abs(((hsla.h / 60) % 2) - 1));\n    let m = hsla.l - c / 2;\n    let r = 0;\n    let g = 0;\n    let b = 0;\n    if (0 <= hsla.h && hsla.h < 60) {\n        r = c;\n        g = x;\n        b = 0;\n    }\n    else if (60 <= hsla.h && hsla.h < 120) {\n        r = x;\n        g = c;\n        b = 0;\n    }\n    else if (120 <= hsla.h && hsla.h < 180) {\n        r = 0;\n        g = c;\n        b = x;\n    }\n    else if (180 <= hsla.h && hsla.h < 240) {\n        r = 0;\n        g = x;\n        b = c;\n    }\n    else if (240 <= hsla.h && hsla.h < 300) {\n        r = x;\n        g = 0;\n        b = c;\n    }\n    else if (300 <= hsla.h && hsla.h < 360) {\n        r = c;\n        g = 0;\n        b = x;\n    }\n    r = Math.round((r + m) * 255);\n    g = Math.round((g + m) * 255);\n    b = Math.round((b + m) * 255);\n    return { a: hsla.a, r, g, b };\n}\n/**\n * HSLA to RGBA.\n *\n * https://css-tricks.com/converting-color-spaces-in-javascript/\n */\nfunction rgbaToHSLA(rgba) {\n    // Make r, g, and b fractions of 1\n    const r = rgba.r / 255;\n    const g = rgba.g / 255;\n    const b = rgba.b / 255;\n    // Find greatest and smallest channel values\n    let cMin = Math.min(r, g, b);\n    let cMax = Math.max(r, g, b);\n    let delta = cMax - cMin;\n    let h = 0;\n    let s = 0;\n    let l = 0;\n    // Calculate hue\n    // No difference\n    if (delta === 0)\n        h = 0;\n    // Red is max\n    else if (cMax === r)\n        h = ((g - b) / delta) % 6;\n    // Green is max\n    else if (cMax === g)\n        h = (b - r) / delta + 2;\n    // Blue is max\n    else\n        h = (r - g) / delta + 4;\n    h = Math.round(h * 60);\n    // Make negative hues positive behind 360\u00b0\n    if (h < 0)\n        h += 360;\n    l = (cMax + cMin) / 2;\n    // Calculate saturation\n    s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));\n    // Multiply l and s by 100\n    s = +(s * 100).toFixed(1);\n    l = +(l * 100).toFixed(1);\n    return { a: rgba.a, h, s, l };\n}\nfunction hslaToHex(hsla) {\n    return rgbaToHex(hslaToRGBA(hsla));\n}\nfunction hexToHSLA(hex) {\n    return rgbaToHSLA(colorToRGBA(hex));\n}\n/**\n * Will compare two color strings\n * A tolerance can be provided to account for small differences that could\n * be introduced by non-bijective transformations between color spaces.\n *\n * E.g. HSV <-> RGB is not a bijection\n *\n * Note that the tolerance is applied on the euclidean distance between\n * the two **normalized** color values.\n */\nfunction isSameColor(color1, color2, tolerance = 0) {\n    if (!(isColorValid(color1) && isColorValid(color2))) {\n        return false;\n    }\n    const rgb1 = colorToRGBA(color1);\n    const rgb2 = colorToRGBA(color2);\n    // alpha cannot differ as it is not impacted by transformations\n    if (rgb1.a !== rgb2.a) {\n        return false;\n    }\n    const diff = Math.sqrt(((rgb1.r - rgb2.r) / 255) ** 2 + ((rgb1.g - rgb2.g) / 255) ** 2 + ((rgb1.b - rgb2.b) / 255) ** 2);\n    return diff <= tolerance;\n}\nfunction setColorAlpha(color, alpha) {\n    return alpha === 1 ? toHex(color).slice(0, 7) : rgbaToHex({ ...colorToRGBA(color), a: alpha });\n}\nfunction lightenColor(color, percentage) {\n    const hsla = hexToHSLA(color);\n    if (percentage === 1) {\n        return \"#fff\";\n    }\n    hsla.l = percentage * (100 - hsla.l) + hsla.l;\n    return hslaToHex(hsla);\n}\nfunction darkenColor(color, percentage) {\n    const hsla = hexToHSLA(color);\n    if (percentage === 1) {\n        return \"#000\";\n    }\n    hsla.l = hsla.l - percentage * hsla.l;\n    return hslaToHex(hsla);\n}\nconst COLORS_SM = [\n    \"#4EA7F2\", // Blue\n    \"#EA6175\", // Red\n    \"#43C5B1\", // Teal\n    \"#F4A261\", // Orange\n    \"#8481DD\", // Purple\n    \"#FFD86D\", // Yellow\n];\nconst COLORS_MD = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n];\nconst COLORS_LG = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#056BD9\", // Blue #3\n    \"#A76DBC\", // Violet #1\n    \"#7F4295\", // Violet #2\n    \"#6D2387\", // Violet #3\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#982738\", // Red #3\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#0E8270\", // Teal #3\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#BE5D10\", // Orange #3\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#3A3580\", // Purple #3\n    \"#A4A8B6\", // Gray #1\n    \"#7E8290\", // Gray #2\n    \"#545B70\", // Gray #3\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n    \"#C08A16\", // Yellow #3\n];\nconst COLORS_XL = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#056BD9\", // Blue #3\n    \"#155193\", // Blue #4\n    \"#A76DBC\", // Violet #1\n    \"#7F4295\", // Violet #1\n    \"#6D2387\", // Violet #1\n    \"#4F1565\", // Violet #1\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#982738\", // Red #3\n    \"#791B29\", // Red #4\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#0E8270\", // Teal #3\n    \"#105F53\", // Teal #4\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#BE5D10\", // Orange #3\n    \"#7D380D\", // Orange #4\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#3A3580\", // Purple #3\n    \"#26235F\", // Purple #4\n    \"#A4A8B6\", // Grey #1\n    \"#7E8290\", // Grey #2\n    \"#545B70\", // Grey #3\n    \"#3F4250\", // Grey #4\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n    \"#C08A16\", // Yellow #3\n    \"#936A12\", // Yellow #4\n];\nfunction getNthColor(index, palette) {\n    return palette[index % palette.length];\n}\nfunction getColorsPalette(quantity) {\n    if (quantity <= 6) {\n        return COLORS_SM;\n    }\n    else if (quantity <= 12) {\n        return COLORS_MD;\n    }\n    else if (quantity <= 24) {\n        return COLORS_LG;\n    }\n    else {\n        return COLORS_XL;\n    }\n}\nclass ColorGenerator {\n    preferredColors;\n    currentColorIndex = 0;\n    palette;\n    constructor(paletteSize, preferredColors = []) {\n        this.preferredColors = preferredColors;\n        this.palette = getColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));\n    }\n    next() {\n        return this.preferredColors?.[this.currentColorIndex]\n            ? this.preferredColors[this.currentColorIndex++]\n            : getNthColor(this.currentColorIndex++, this.palette);\n    }\n}\n\n//------------------------------------------------------------------------------\n// Coordinate\n//------------------------------------------------------------------------------\n/**\n * Convert a (col) number to the corresponding letter.\n *\n * Examples:\n *     0 => 'A'\n *     25 => 'Z'\n *     26 => 'AA'\n *     27 => 'AB'\n */\nfunction numberToLetters(n) {\n    if (n < 0) {\n        throw new Error(`number must be positive. Got ${n}`);\n    }\n    if (n < 26) {\n        return String.fromCharCode(65 + n);\n    }\n    else {\n        return numberToLetters(Math.floor(n / 26) - 1) + numberToLetters(n % 26);\n    }\n}\nfunction lettersToNumber(letters) {\n    let result = 0;\n    const l = letters.length;\n    for (let i = 0; i < l; i++) {\n        const charCode = letters.charCodeAt(i);\n        const colIndex = charCode >= 65 && charCode <= 90 ? charCode - 64 : charCode - 96;\n        result = result * 26 + colIndex;\n    }\n    return result - 1;\n}\nfunction isCharALetter(char) {\n    return (char >= \"A\" && char <= \"Z\") || (char >= \"a\" && char <= \"z\");\n}\nfunction isCharADigit(char) {\n    return char >= \"0\" && char <= \"9\";\n}\n/**\n * Convert a \"XC\" coordinate to cartesian coordinates.\n *\n * Examples:\n *   A1 => [0,0]\n *   B3 => [1,2]\n *\n * Note: it also accepts lowercase coordinates, but not fixed references\n */\nfunction toCartesian(xc) {\n    xc = xc.trim();\n    let letterPart = \"\";\n    let numberPart = \"\";\n    let i = 0;\n    // Process letter part\n    if (xc[i] === \"$\")\n        i++;\n    while (i < xc.length && isCharALetter(xc[i])) {\n        letterPart += xc[i++];\n    }\n    if (letterPart.length === 0 || letterPart.length > 3) {\n        // limit to max 3 letters for performance reasons\n        throw new Error(`Invalid cell description: ${xc}`);\n    }\n    // Process number part\n    if (xc[i] === \"$\")\n        i++;\n    while (i < xc.length && isCharADigit(xc[i])) {\n        numberPart += xc[i++];\n    }\n    if (i !== xc.length || numberPart.length === 0 || numberPart.length > 7) {\n        // limit to max 7 numbers for performance reasons\n        throw new Error(`Invalid cell description: ${xc}`);\n    }\n    const col = lettersToNumber(letterPart);\n    const row = Number(numberPart) - 1;\n    if (isNaN(row)) {\n        throw new Error(`Invalid cell description: ${xc}`);\n    }\n    return { col, row };\n}\n/**\n * Convert from cartesian coordinate to the \"XC\" coordinate system.\n *\n * Examples:\n *   - 0,0 => A1\n *   - 1,2 => B3\n *   - 0,0, {colFixed: false, rowFixed: true} => A$1\n *   - 1,2, {colFixed: true, rowFixed: false} => $B3\n */\nfunction toXC(col, row, rangePart = { colFixed: false, rowFixed: false }) {\n    return ((rangePart.colFixed ? \"$\" : \"\") +\n        numberToLetters(col) +\n        (rangePart.rowFixed ? \"$\" : \"\") +\n        String(row + 1));\n}\n\n/**\n * ####################################################\n * # INTRODUCTION\n * ####################################################\n *\n * This file contain the function recomputeZones.\n * This function try to recompute in a performant way\n * an ensemble of zones possibly overlapping to avoid\n * overlapping and to reduce the number of zones.\n *\n * It also allows to remove some zones from the ensemble.\n *\n * In the following example, 2 zones are overlapping.\n * Applying recomputeZones will return zones without\n * overlapping:\n *\n * [\"B3:D4\", \"D2:E3\"]         [\"B3:C4\", \"D2:D4\", \"E2:E3\"]\n *\n *      A B C D E                    A B C D E\n *    1       ___                  1       ___\n *    2   ___|_  |                 2   ___| | |\n *    3  |   |_|_|      --->       3  |   | |_|\n *    4  |_____|                   4  |___|_|\n *    6                            6\n *    7                            7\n *\n *\n * In the following example, 2 zones are contiguous.\n * Applying recomputeZones will return only one zone:\n *\n *  [\"B2:B3\", \"C2:D3\"]               [\"B2:D3\"]\n *\n *       A B C D E                   A B C D E\n *     1   _ ___                   1   _____\n *     2  | |   |        --->      2  |     |\n *     3  |_|___|                  3  |_____|\n *     4                           4\n *\n *\n * In the following example, we want to remove a zone\n * from the ensemble. Applying recomputeZones will\n * return the ensemble without the zone to remove:\n *\n *    remove [\"C3:D3\"]           [\"B2:B4\", \"C2:D2\",\n *                                \"C4:D4\", \"E2:E4\"]\n *\n *       A B C D E F                 A B C D E F\n *     1   _______                 1   _______\n *     2  |       |       --->     2  | |___| |\n *     3  |  xxx  |                3  | |___| |\n *     4  |_______|                4  |_|___|_|\n *     5                           5\n *\n *\n * The exercise seems simple when we have only 2 zones.\n * But with n zones and in a performant way, we want to\n * avoid comparing each zone with all the others.\n *\n *\n * ####################################################\n * # Methodological approach\n * ####################################################\n *\n * The methodological approach to avoid comparing each\n * zone with all the others is to use a data structure\n * that allow to quickly find which zones are\n * overlapping with any other given zone.\n *\n * Here the idea is to profile the zones at the columns level.\n *\n * To do that, we propose to use a data structure\n * composed of 2 parts:\n * - profilesStartingPosition: a sorted number array\n * indicating on which columns a new profile begins.\n * - profiles: a map where the key is a column\n * position (from profilesStartingPosition) and the\n * value is a sorted number array representing a\n * profile.\n *\n *\n * See the following example:    here profileStartingPosition\n *                               corresponds to [A,C,E,G,K]\n *    A B C D E F G H I J K      so with number [0,2,4,6,10]\n *  1    '   '   '       '\n *  2    '   '   '_______'       here profile correspond\n *  3    '___'   |_______|       for A to []\n *  4    |   |                   for C to [3, 5]\n *  5    |___|                   for E to []\n *  6                            for G to [2, 3]\n *  7                            for K to []\n *\n *\n * Now we can easily find which zones are overlapping\n * with a given zone. Suppose we want to add a new zone\n * D5:H6 to the ensemble:\n *\n *                              With a binary search of left and right\n *    A B C D E F G H I J K     on profilesStartingPosition, we can\n *  1    '   '   '       '      find the indexes of the profiles on which\n *  2    '   '   '_______'      to apply a modification.\n *  3    '___'   |_______|\n *  4    |  _|_______           Here we will:\n *  5    |_|_|       |          - add a new profile in D   --> become [3, 6]\n *  6      |_________|          - modify the profile in E  --> become [4, 6]\n *  7                           - modify the profile in G  --> become [2, 3, 4, 6]\n *                              - add a new profile in I   --> become [8, 10]\n *\n *  See below the result:\n *\n *                              Note the particularity of the profile\n *    A B C D E F G H I J K     for G: it will correspond to [2, 3, 4, 6]\n *  1    ' ' '   '   '   '\n *  2    ' ' '   '___'___'      To know how to modify the profile (add a\n *  3    '_'_'   |___|___|      zone or remove it) we do a binary\n *  4    | | |___ ___           search of the top and bottom value on the\n *  5    |_| |   |   |          profile array. Depending on the result index\n *  6      |_|___|___|          parity (odd or even), because zone boundaries\n *  7                           go by pairs, we know if we are in a zone or\n *                              not and how operate.\n */\n/**\n * Recompute the zone without the cells in toRemoveZones and avoid overlapping.\n * This compute is particularly useful because after this function:\n * - you will find coordinate of a cell only once among all the zones\n * - the number of zones will be reduced to the minimum\n */\nfunction recomputeZones(zones, zonesToRemove = []) {\n    if (zones.length <= 1 && zonesToRemove.length === 0) {\n        return zones;\n    }\n    const profilesStartingPosition = [0];\n    const profiles = new Map([[0, []]]);\n    modifyProfiles(profilesStartingPosition, profiles, zones, false);\n    modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);\n    return constructZonesFromProfiles(profilesStartingPosition, profiles);\n}\nfunction modifyProfiles(// export for testing only\nprofilesStartingPosition, profiles, zones, toRemove = false) {\n    for (const zone of zones) {\n        const leftValue = zone.left;\n        const rightValue = zone.right === undefined ? undefined : zone.right + 1;\n        const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);\n        const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);\n        for (let i = leftIndex; i <= rightIndex; i++) {\n            const profile = profiles.get(profilesStartingPosition[i]);\n            modifyProfile(profile, zone, toRemove);\n        }\n        // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked\n        removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);\n    }\n}\nfunction findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {\n    if (value === undefined) {\n        // this is only the case when the value correspond to a bottom value that could be undefined\n        return profilesStartingPosition.length - 1;\n    }\n    const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);\n    if (value != profilesStartingPosition[predecessorIndex]) {\n        // mean that the value is not ending/starting at the same position as the previous/next profile\n        // --> it's a new profile\n        // --> we need to add it\n        profilesStartingPosition.splice(predecessorIndex + 1, 0, value);\n        // suppose the               we want to add the       for the left value\n        // following profile         following zone:          'C', the predecessor index\n        //   for B: [1, 3]                \"C3:D4\"             correspond to 'B'.\n        //                                                    The next line code will\n        //       A B C D                A B C D               copy the profile of 'B'\n        //     1  '___'               1  '___'                to 'C'. In the rest of the\n        //     2  |   |       --->    2  |  _|_               process the 'modifyProfile'\n        //     3  |___|               3  |_|_| |              function will adapt the waiting\n        //     4                      4    |___|              'C' profile [1, 3] to the\n        //                                                    correct 'C' profile [1, 4]\n        profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);\n        return searchLeft ? predecessorIndex + 1 : predecessorIndex;\n    }\n    return searchLeft ? predecessorIndex : predecessorIndex - 1;\n}\n/**\n *  Suppose the following        Suppose we want to add          We want to have the\n *  profile:                     the following zone:             following profile:\n *\n *       A B C D E F                  A B C D E F                     A B C D E F\n *     1    '___'                   1    '   '                      1    '___'\n *     2    |___|                   2    '___'                      2    |   |\n *     3    '   '                   3    |   |                      3    |   |\n *     4    '___'          -->      4    |   |            -->       4    |   |\n *     6    |   |                   6    |___|                      6    |   |\n *     7    |___|                   7                               7    |___|\n *     8                            8                               8\n *\n *  the profile for 'C'          the top zone correspond        Here [2, 3, 5, 8] with [3, 7]\n *  corresponds to:              to 3 and the bottom zone       would be merged into [2, 8]\n *   ____  ____                  correspond to 6\n *  [2, 3, 5, 8]                 would be the profile:          The difficulty of modify profile\n *                                ____                          is to know what must be deleted\n *  Note that the 'filled        [3, 7]                         and what must be added to the\n *  zone' are always between                                    existing profile.\n *  an even index and its\n *  next index\n *\n */\nfunction modifyProfile(profile, zone, toRemove = false) {\n    const topValue = zone.top;\n    const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;\n    const newPoints = [];\n    // Case we want to add a zone to the profile:\n    // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone\n    // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.\n    // - If it is odd, it means the top of the zone must be the beginning of a filled zone.\n    // so we can keep the index of the top position\n    // Case we want to remove a zone from the profile: it's the opposite of the previous case\n    const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);\n    if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {\n        newPoints.push(topValue);\n    }\n    if (bottomValue === undefined) {\n        // The following two code lines will not impact the final result,\n        // but they will impact the intermediate profile.\n        // We keep them for performance reason\n        profile.splice(topPredIndex + 1);\n        profile.push(...newPoints);\n        return;\n    }\n    // Case we want to add a zone to the profile:\n    // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone\n    // so we can keep the index of the bottom position.\n    // - If it is odd, it means the bottom of the zone is already positioned on a filled zone\n    // so we don't need to add it to the profile. we can keep in reference the index of the successor\n    // Case we want to remove a zone from the profile: it's the opposite of the previous case\n    const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);\n    if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {\n        newPoints.push(bottomValue);\n    }\n    // add the top and bottom value to the profile and\n    // remove all information between the top and bottom index\n    profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);\n}\nfunction removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {\n    const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;\n    const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;\n    for (let i = end; i > start; i--) {\n        if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {\n            profiles.delete(profilesStartingPosition[i]);\n            profilesStartingPosition.splice(i, 1);\n        }\n    }\n}\nfunction constructZonesFromProfiles(profilesStartingPosition, profiles) {\n    const mergedZone = [];\n    let pendingZones = [];\n    for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {\n        const left = profilesStartingPosition[colIndex];\n        const profile = profiles.get(left);\n        if (!profile || profile.length === 0) {\n            mergedZone.push(...pendingZones);\n            pendingZones = [];\n            continue;\n        }\n        let right = profilesStartingPosition[colIndex + 1];\n        if (right !== undefined) {\n            right--;\n        }\n        const nextPendingZones = [];\n        for (let i = 0; i < profile.length; i += 2) {\n            const top = profile[i];\n            let bottom = profile[i + 1];\n            if (bottom !== undefined) {\n                bottom--;\n            }\n            const profileZone = {\n                top,\n                left,\n                bottom,\n                right,\n                hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),\n            };\n            let findCorrespondingZone = false;\n            for (let j = pendingZones.length - 1; j >= 0; j--) {\n                const pendingZone = pendingZones[j];\n                if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {\n                    pendingZone.right = profileZone.right;\n                    pendingZones.splice(j, 1);\n                    nextPendingZones.push(pendingZone);\n                    findCorrespondingZone = true;\n                    break;\n                }\n            }\n            if (!findCorrespondingZone) {\n                nextPendingZones.push(profileZone);\n            }\n        }\n        mergedZone.push(...pendingZones);\n        pendingZones = nextPendingZones;\n    }\n    mergedZone.push(...pendingZones);\n    return mergedZone;\n}\nfunction binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {\n    let end = arr.length - 1;\n    let result = -1;\n    while (start <= end) {\n        const mid = Math.floor((start + end) / 2);\n        if (arr[mid] === val && matchEqual) {\n            return mid;\n        }\n        else if (arr[mid] < val) {\n            result = mid;\n            start = mid + 1;\n        }\n        else {\n            end = mid - 1;\n        }\n    }\n    return result;\n}\nfunction binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {\n    let end = arr.length - 1;\n    let result = arr.length;\n    while (start <= end) {\n        const mid = Math.floor((start + end) / 2);\n        if (arr[mid] === val && matchEqual) {\n            return mid;\n        }\n        else if (arr[mid] > val) {\n            result = mid;\n            end = mid - 1;\n        }\n        else {\n            start = mid + 1;\n        }\n    }\n    return result;\n}\n\nconst defaultTranslate = (s) => s;\nconst defaultLoaded = () => false;\nlet _translate = defaultTranslate;\nlet _loaded = defaultLoaded;\nfunction sprintf(s, ...values) {\n    if (values.length === 1 && typeof values[0] === \"object\" && !(values[0] instanceof String)) {\n        const valuesDict = values[0];\n        s = s.replace(/\\%\\(([^\\)]+)\\)s/g, (match, value) => valuesDict[value]);\n    }\n    else if (values.length > 0) {\n        s = s.replace(/\\%s/g, () => values.shift());\n    }\n    return s;\n}\n/***\n * Allow to inject a translation function from outside o-spreadsheet. This should be called before instantiating\n * a model.\n * @param tfn the function that will do the translation\n * @param loaded a function that returns true when the translation is loaded\n */\nfunction setTranslationMethod(tfn, loaded = () => true) {\n    _translate = tfn;\n    _loaded = loaded;\n}\n/**\n * If no translation function has been set, this will mark the translation are loaded.\n *\n * By default, the translations should not be set as loaded, otherwise top-level translated constants will never be\n * translated. But if by the time the model is instantiated no custom translation function has been set, we can set\n * the default translation function as loaded so o-spreadsheet can be run in standalone with no translations.\n */\nfunction setDefaultTranslationMethod() {\n    if (_translate === defaultTranslate && _loaded === defaultLoaded) {\n        _loaded = () => true;\n    }\n}\nconst _t = function (s, ...values) {\n    if (!_loaded()) {\n        return new LazyTranslatedString(s, values);\n    }\n    return sprintf(_translate(s), ...values);\n};\nclass LazyTranslatedString extends String {\n    values;\n    constructor(str, values) {\n        super(str);\n        this.values = values;\n    }\n    valueOf() {\n        const str = super.valueOf();\n        return _loaded() ? sprintf(_translate(str), ...this.values) : sprintf(str, ...this.values);\n    }\n    toString() {\n        return this.valueOf();\n    }\n}\n\n/** Reference of a cell (eg. A1, $B$5) */\nconst cellReference = new RegExp(/\\$?([A-Z]{1,3})\\$?([0-9]{1,7})/, \"i\");\n// Same as above, but matches the exact string (nothing before or after)\nconst singleCellReference = new RegExp(/^\\$?([A-Z]{1,3})\\$?([0-9]{1,7})$/, \"i\");\n/** Reference of a column header (eg. A, AB, $A) */\nconst colHeader = new RegExp(/^\\$?([A-Z]{1,3})+$/, \"i\");\n/** Reference of a row header (eg. 1, $1) */\nconst rowHeader = new RegExp(/^\\$?([0-9]{1,7})+$/, \"i\");\n/** Reference of a column (eg. A, $CA, Sheet1!B) */\nconst colReference = new RegExp(/^\\s*('.+'!|[^']+!)?\\$?([A-Z]{1,3})$/, \"i\");\n/** Reference of a row (eg. 1, 59, Sheet1!9) */\nconst rowReference = new RegExp(/^\\s*('.+'!|[^']+!)?\\$?([0-9]{1,7})$/, \"i\");\n/** Reference of a normal range or a full row range (eg. A1:B1, 1:$5, $A2:5) */\nconst fullRowXc = /(\\$?[A-Z]{1,3})?\\$?[0-9]{1,7}\\s*:\\s*(\\$?[A-Z]{1,3})?\\$?[0-9]{1,7}\\s*/i;\n/** Reference of a normal range or a column row range (eg. A1:B1, A:$B, $A1:C) */\nconst fullColXc = /\\$?[A-Z]{1,3}(\\$?[0-9]{1,7})?\\s*:\\s*\\$?[A-Z]{1,3}(\\$?[0-9]{1,7})?\\s*/i;\n/** Reference of a cell or a range, it can be a bounded range, a full row or a full column */\nconst rangeReference = new RegExp(/^\\s*('.+'!|[^']+!)?/.source +\n    \"(\" +\n    [cellReference.source, fullRowXc.source, fullColXc.source].join(\"|\") +\n    \")\" +\n    /$/.source, \"i\");\n/**\n * Return true if the given xc is the reference of a column (e.g. A or AC or Sheet1!A)\n */\nfunction isColReference(xc) {\n    return colReference.test(xc);\n}\n/**\n * Return true if the given xc is the reference of a column (e.g. 1 or Sheet1!1)\n */\nfunction isRowReference(xc) {\n    return rowReference.test(xc);\n}\nfunction isColHeader(str) {\n    return colHeader.test(str);\n}\nfunction isRowHeader(str) {\n    return rowHeader.test(str);\n}\n/**\n * Return true if the given xc is the reference of a single cell,\n * without any specified sheet (e.g. A1)\n */\nfunction isSingleCellReference(xc) {\n    return singleCellReference.test(xc);\n}\nfunction splitReference(ref) {\n    if (!ref.includes(\"!\")) {\n        return { xc: ref };\n    }\n    const parts = ref.split(\"!\");\n    const xc = parts.pop();\n    const sheetName = getUnquotedSheetName(parts.join(\"!\")) || undefined;\n    return { sheetName, xc };\n}\n/** Return a reference SheetName!xc from the given arguments */\nfunction getFullReference(sheetName, xc) {\n    return sheetName !== undefined ? `${getCanonicalSymbolName(sheetName)}!${xc}` : xc;\n}\n\n/**\n * Convert from a cartesian reference to a Zone\n * The range boundaries will be kept in the same order as the\n * ones in the text.\n * Examples:\n *    \"A1\" ==> Top 0, Bottom 0, Left: 0, Right: 0\n *    \"B1:B3\" ==> Top 0, Bottom 3, Left: 1, Right: 1\n *    \"Sheet1!A1\" ==> Top 0, Bottom 0, Left: 0, Right: 0\n *    \"Sheet1!B1:B3\" ==> Top 0, Bottom 3, Left: 1, Right: 1\n *    \"C3:A1\" ==> Top 2, Bottom 0, Left 2, Right 0\n *    \"A:A\" ==> Top 0, Bottom undefined, Left 0, Right 0\n *    \"A:B3\" or \"B3:A\" ==> Top 2, Bottom undefined, Left 0, Right 1\n *\n * @param xc the string reference to convert\n *\n */\nfunction toZoneWithoutBoundaryChanges(xc) {\n    if (xc.includes(\"!\")) {\n        xc = xc.split(\"!\").at(-1);\n    }\n    if (xc.includes(\"$\")) {\n        xc = xc.replaceAll(\"$\", \"\");\n    }\n    let firstRangePart = \"\";\n    let secondRangePart;\n    if (xc.includes(\":\")) {\n        [firstRangePart, secondRangePart] = xc.split(\":\");\n        firstRangePart = firstRangePart.trim();\n        secondRangePart = secondRangePart.trim();\n    }\n    else {\n        firstRangePart = xc.trim();\n    }\n    let top, bottom, left, right;\n    let fullCol = false;\n    let fullRow = false;\n    let hasHeader = false;\n    if (isColReference(firstRangePart)) {\n        left = right = lettersToNumber(firstRangePart);\n        top = bottom = 0;\n        fullCol = true;\n    }\n    else if (isRowReference(firstRangePart)) {\n        top = bottom = parseInt(firstRangePart, 10) - 1;\n        left = right = 0;\n        fullRow = true;\n    }\n    else {\n        const c = toCartesian(firstRangePart);\n        left = right = c.col;\n        top = bottom = c.row;\n        hasHeader = true;\n    }\n    if (secondRangePart) {\n        if (isColReference(secondRangePart)) {\n            right = lettersToNumber(secondRangePart);\n            fullCol = true;\n        }\n        else if (isRowReference(secondRangePart)) {\n            bottom = parseInt(secondRangePart, 10) - 1;\n            fullRow = true;\n        }\n        else {\n            const c = toCartesian(secondRangePart);\n            right = c.col;\n            bottom = c.row;\n            top = fullCol ? bottom : top;\n            left = fullRow ? right : left;\n            hasHeader = true;\n        }\n    }\n    if (fullCol && fullRow) {\n        throw new Error(\"Wrong zone xc. The zone cannot be at the same time a full column and a full row\");\n    }\n    const zone = {\n        top,\n        left,\n        bottom: fullCol ? undefined : bottom,\n        right: fullRow ? undefined : right,\n    };\n    hasHeader = hasHeader && (fullRow || fullCol);\n    if (hasHeader) {\n        zone.hasHeader = hasHeader;\n    }\n    return zone;\n}\n/**\n * Convert from a cartesian reference to a (possibly unbounded) Zone\n *\n * Examples:\n *    \"A1\" ==> Top 0, Bottom 0, Left: 0, Right: 0\n *    \"B1:B3\" ==> Top 0, Bottom 3, Left: 1, Right: 1\n *    \"B:B\" ==> Top 0, Bottom undefined, Left: 1, Right: 1\n *    \"B2:B\" ==> Top 1, Bottom undefined, Left: 1, Right: 1, hasHeader: 1\n *    \"Sheet1!A1\" ==> Top 0, Bottom 0, Left: 0, Right: 0\n *    \"Sheet1!B1:B3\" ==> Top 0, Bottom 3, Left: 1, Right: 1\n *\n * @param xc the string reference to convert\n *\n */\nfunction toUnboundedZone(xc) {\n    const zone = toZoneWithoutBoundaryChanges(xc);\n    if (zone.right !== undefined && zone.right < zone.left) {\n        const tmp = zone.left;\n        zone.left = zone.right;\n        zone.right = tmp;\n    }\n    if (zone.bottom !== undefined && zone.bottom < zone.top) {\n        const tmp = zone.top;\n        zone.top = zone.bottom;\n        zone.bottom = tmp;\n    }\n    return zone;\n}\n/**\n * Convert from a cartesian reference to a Zone.\n * Will return throw an error if given a unbounded zone (eg : A:A).\n *\n * Examples:\n *    \"A1\" ==> Top 0, Bottom 0, Left: 0, Right: 0\n *    \"B1:B3\" ==> Top 0, Bottom 2, Left: 1, Right: 1\n *    \"Sheet1!A1\" ==> Top 0, Bottom 0, Left: 0, Right: 0\n *    \"Sheet1!B1:B3\" ==> Top 0, Bottom 2, Left: 1, Right: 1\n *\n * @param xc the string reference to convert\n *\n */\nfunction toZone(xc) {\n    const zone = toUnboundedZone(xc);\n    if (zone.bottom === undefined || zone.right === undefined) {\n        throw new Error(\"This does not support unbounded ranges\");\n    }\n    return zone;\n}\n/**\n * Check that the zone has valid coordinates and in\n * the correct order.\n */\nfunction isZoneValid(zone) {\n    // Typescript *should* prevent this kind of errors but\n    // it's better to be on the safe side at runtime as well.\n    const { bottom, top, left, right } = zone;\n    if ((bottom !== undefined && isNaN(bottom)) ||\n        isNaN(top) ||\n        isNaN(left) ||\n        (right !== undefined && isNaN(right))) {\n        return false;\n    }\n    return isZoneOrdered(zone) && zone.top >= 0 && zone.left >= 0;\n}\n/**\n * Check that the zone properties are in the correct order.\n */\nfunction isZoneOrdered(zone) {\n    return ((zone.bottom === undefined || (zone.bottom >= zone.top && zone.bottom >= 0)) &&\n        (zone.right === undefined || (zone.right >= zone.left && zone.right >= 0)));\n}\n/**\n * Convert from zone to a cartesian reference\n *\n */\nfunction zoneToXc(zone) {\n    const { top, bottom, left, right } = zone;\n    const hasHeader = \"hasHeader\" in zone ? zone.hasHeader : false;\n    const isOneCell = top === bottom && left === right;\n    if (bottom === undefined && right !== undefined) {\n        return top === 0 && !hasHeader\n            ? `${numberToLetters(left)}:${numberToLetters(right)}`\n            : `${toXC(left, top)}:${numberToLetters(right)}`;\n    }\n    else if (right === undefined && bottom !== undefined) {\n        return left === 0 && !hasHeader\n            ? `${top + 1}:${bottom + 1}`\n            : `${toXC(left, top)}:${bottom + 1}`;\n    }\n    else if (bottom !== undefined && right !== undefined) {\n        return isOneCell ? toXC(left, top) : `${toXC(left, top)}:${toXC(right, bottom)}`;\n    }\n    throw new Error(_t(\"Bad zone format\"));\n}\n/**\n * Expand a zone after inserting columns or rows.\n *\n * Don't resize the zone if a col/row was added right before/after the row but only move the zone.\n */\nfunction expandZoneOnInsertion(zone, start, base, position, quantity) {\n    const dimension = start === \"left\" ? \"columns\" : \"rows\";\n    const baseElement = position === \"before\" ? base - 1 : base;\n    const end = start === \"left\" ? \"right\" : \"bottom\";\n    const zoneEnd = zone[end];\n    if (zone[start] <= baseElement && zoneEnd && zoneEnd > baseElement) {\n        return createAdaptedZone(zone, dimension, \"RESIZE\", quantity);\n    }\n    if (baseElement < zone[start]) {\n        return createAdaptedZone(zone, dimension, \"MOVE\", quantity);\n    }\n    return { ...zone };\n}\n/**\n * Update the selection after column/row addition\n */\nfunction updateSelectionOnInsertion(selection, start, base, position, quantity) {\n    const dimension = start === \"left\" ? \"columns\" : \"rows\";\n    const baseElement = position === \"before\" ? base - 1 : base;\n    const end = start === \"left\" ? \"right\" : \"bottom\";\n    if (selection[start] <= baseElement && selection[end] > baseElement) {\n        return createAdaptedZone(selection, dimension, \"RESIZE\", quantity);\n    }\n    if (baseElement < selection[start]) {\n        return createAdaptedZone(selection, dimension, \"MOVE\", quantity);\n    }\n    return { ...selection };\n}\n/**\n * Update the selection after column/row deletion\n */\nfunction updateSelectionOnDeletion(zone, start, elements) {\n    const end = start === \"left\" ? \"right\" : \"bottom\";\n    let newStart = zone[start];\n    let newEnd = zone[end];\n    for (let removedElement of elements.sort((a, b) => b - a)) {\n        if (zone[start] > removedElement) {\n            newStart--;\n            newEnd--;\n        }\n        if (zone[start] < removedElement && zone[end] >= removedElement) {\n            newEnd--;\n        }\n    }\n    return { ...zone, [start]: newStart, [end]: newEnd };\n}\n/**\n * Reduce a zone after deletion of elements\n */\nfunction reduceZoneOnDeletion(zone, start, elements) {\n    const end = start === \"left\" ? \"right\" : \"bottom\";\n    let newStart = zone[start];\n    let newEnd = zone[end];\n    const zoneEnd = zone[end];\n    for (let removedElement of elements.sort((a, b) => b - a)) {\n        if (zone[start] > removedElement) {\n            newStart--;\n            if (newEnd !== undefined)\n                newEnd--;\n        }\n        if (zoneEnd !== undefined &&\n            newEnd !== undefined &&\n            zone[start] <= removedElement &&\n            zoneEnd >= removedElement) {\n            newEnd--;\n        }\n    }\n    if (newEnd !== undefined && newStart > newEnd) {\n        return undefined;\n    }\n    return { ...zone, [start]: newStart, [end]: newEnd };\n}\n/**\n * Compute the union of multiple zones.\n */\nfunction union(...zones) {\n    return {\n        top: Math.min(...zones.map((zone) => zone.top)),\n        left: Math.min(...zones.map((zone) => zone.left)),\n        bottom: Math.max(...zones.map((zone) => zone.bottom)),\n        right: Math.max(...zones.map((zone) => zone.right)),\n    };\n}\n/**\n * Compute the union of multiple unbounded zones.\n */\nfunction unionUnboundedZones(...zones) {\n    return {\n        top: Math.min(...zones.map((zone) => zone.top)),\n        left: Math.min(...zones.map((zone) => zone.left)),\n        bottom: zones.some((zone) => zone.bottom === undefined)\n            ? undefined\n            : Math.max(...zones.map((zone) => zone.bottom)),\n        right: zones.some((zone) => zone.right === undefined)\n            ? undefined\n            : Math.max(...zones.map((zone) => zone.right)),\n    };\n}\n/**\n * Compute the intersection of two zones. Returns nothing if the two zones don't overlap\n */\nfunction intersection(z1, z2) {\n    if (!overlap(z1, z2)) {\n        return undefined;\n    }\n    return {\n        top: Math.max(z1.top, z2.top),\n        left: Math.max(z1.left, z2.left),\n        bottom: Math.min(z1.bottom, z2.bottom),\n        right: Math.min(z1.right, z2.right),\n    };\n}\n/**\n * Two zones are equal if they represent the same area, so we clearly cannot use\n * reference equality.\n */\nfunction isEqual(z1, z2) {\n    return (z1.left === z2.left && z1.right === z2.right && z1.top === z2.top && z1.bottom === z2.bottom);\n}\n/**\n * Return true if two zones overlap, false otherwise.\n */\nfunction overlap(z1, z2) {\n    if (z1.bottom < z2.top || z2.bottom < z1.top) {\n        return false;\n    }\n    if (z1.right < z2.left || z2.right < z1.left) {\n        return false;\n    }\n    return true;\n}\nfunction isInside(col, row, zone) {\n    const { left, right, top, bottom } = zone;\n    return col >= left && col <= right && row >= top && row <= bottom;\n}\n/**\n * Check if a zone is inside another\n */\nfunction isZoneInside(smallZone, biggerZone) {\n    return isEqual(union(biggerZone, smallZone), biggerZone);\n}\nfunction zoneToDimension(zone) {\n    return {\n        numberOfRows: zone.bottom - zone.top + 1,\n        numberOfCols: zone.right - zone.left + 1,\n    };\n}\nfunction isOneDimensional(zone) {\n    const { numberOfCols, numberOfRows } = zoneToDimension(zone);\n    return numberOfCols === 1 || numberOfRows === 1;\n}\nfunction excludeTopLeft(zone) {\n    const { top, left, bottom, right } = zone;\n    if (getZoneArea(zone) === 1) {\n        return [];\n    }\n    const leftColumnZone = {\n        top: top + 1,\n        bottom,\n        left,\n        right: left,\n    };\n    if (right === left) {\n        return [leftColumnZone];\n    }\n    const rightPartZone = {\n        top,\n        bottom,\n        left: left + 1,\n        right,\n    };\n    if (top === bottom) {\n        return [rightPartZone];\n    }\n    return [leftColumnZone, rightPartZone];\n}\nfunction aggregatePositionsToZones(positions) {\n    const result = {};\n    for (const position of positions) {\n        result[position.sheetId] ??= [];\n        result[position.sheetId].push(positionToZone(position));\n    }\n    for (const sheetId in result) {\n        result[sheetId] = recomputeZones(result[sheetId]);\n    }\n    return result;\n}\n/**\n * Array of all positions in the zone.\n */\nfunction positions(zone) {\n    const positions = [];\n    const { left, right, top, bottom } = reorderZone(zone);\n    for (const col of range(left, right + 1)) {\n        for (const row of range(top, bottom + 1)) {\n            positions.push({ col, row });\n        }\n    }\n    return positions;\n}\nfunction reorderZone(zone) {\n    if (zone.left > zone.right) {\n        zone = { left: zone.right, right: zone.left, top: zone.top, bottom: zone.bottom };\n    }\n    if (zone.top > zone.bottom) {\n        zone = { left: zone.left, right: zone.right, top: zone.bottom, bottom: zone.top };\n    }\n    return zone;\n}\n/**\n * This function returns a zone with coordinates modified according to the change\n * applied to the zone. It may be possible to change the zone by resizing or moving\n * it according to different dimensions.\n *\n * @param zone the zone to modify\n * @param dimension the direction to change the zone among \"columns\", \"rows\" and\n * \"both\"\n * @param operation how to change the zone, modify its size \"RESIZE\" or modify\n * its location \"MOVE\"\n * @param by a number of how many units the change should be made. This parameter\n * takes the form of a two-number array when the dimension is \"both\"\n */\nfunction createAdaptedZone(zone, dimension, operation, by) {\n    const offsetX = dimension === \"both\" ? by[0] : dimension === \"columns\" ? by : 0;\n    const offsetY = dimension === \"both\" ? by[1] : dimension === \"rows\" ? by : 0;\n    // For full columns/rows, we have to make the distinction between the one that have a header and\n    // whose start should be moved (eg. A2:A), and those who don't (eg. A:A)\n    // The only time we don't want to move the start of the zone is if the zone is a full column (or a full row)\n    // without header and that we are adding/removing a row (or a column)\n    const hasHeader = \"hasHeader\" in zone ? zone.hasHeader : false;\n    let shouldStartBeMoved;\n    if (isFullCol(zone) && !hasHeader) {\n        shouldStartBeMoved = dimension !== \"rows\";\n    }\n    else if (isFullRow(zone) && !hasHeader) {\n        shouldStartBeMoved = dimension !== \"columns\";\n    }\n    else {\n        shouldStartBeMoved = true;\n    }\n    const newZone = { ...zone };\n    if (shouldStartBeMoved && operation === \"MOVE\") {\n        newZone[\"left\"] += offsetX;\n        newZone[\"top\"] += offsetY;\n    }\n    if (newZone[\"right\"] !== undefined) {\n        newZone[\"right\"] += offsetX;\n    }\n    if (newZone[\"bottom\"] !== undefined) {\n        newZone[\"bottom\"] += offsetY;\n    }\n    return newZone;\n}\n/**\n * Returns a Zone array with unique occurrence of each zone.\n * For each multiple occurrence, the occurrence with the largest index is kept.\n * This allows to always have the last selection made in the last position.\n * */\nfunction uniqueZones(zones) {\n    return zones\n        .reverse()\n        .filter((zone, index, self) => index ===\n        self.findIndex((z) => z.top === zone.top &&\n            z.bottom === zone.bottom &&\n            z.left === zone.left &&\n            z.right === zone.right))\n        .reverse();\n}\n/**\n * This function will find all overlapping zones in an array and transform them\n * into an union of each one.\n * */\nfunction mergeOverlappingZones(zones) {\n    return zones.reduce((dissociatedZones, zone) => {\n        const nextIndex = dissociatedZones.length;\n        for (let i = 0; i < nextIndex; i++) {\n            if (overlap(dissociatedZones[i], zone)) {\n                dissociatedZones[i] = union(dissociatedZones[i], zone);\n                return dissociatedZones;\n            }\n        }\n        dissociatedZones[nextIndex] = zone;\n        return dissociatedZones;\n    }, []);\n}\n/**\n * This function will compare the modifications of selection to determine\n * a cell that is part of the new zone and not the previous one.\n */\nfunction findCellInNewZone(oldZone, currentZone) {\n    let col, row;\n    const { left: oldLeft, right: oldRight, top: oldTop, bottom: oldBottom } = oldZone;\n    const { left, right, top, bottom } = currentZone;\n    if (left != oldLeft) {\n        col = left;\n    }\n    else if (right != oldRight) {\n        col = right;\n    }\n    else {\n        // left and right don't change\n        col = left;\n    }\n    if (top != oldTop) {\n        row = top;\n    }\n    else if (bottom != oldBottom) {\n        row = bottom;\n    }\n    else {\n        // top and bottom don't change\n        row = top;\n    }\n    return { col, row };\n}\nfunction positionToZone(position) {\n    return { left: position.col, right: position.col, top: position.row, bottom: position.row };\n}\n/** Transform a zone into a zone with only its top-left position */\nfunction zoneToTopLeft(zone) {\n    return { ...zone, right: zone.left, bottom: zone.top };\n}\nfunction isFullRow(zone) {\n    return zone.right === undefined;\n}\nfunction isFullCol(zone) {\n    return zone.bottom === undefined;\n}\n/** Returns the area of a zone */\nfunction getZoneArea(zone) {\n    return (zone.bottom - zone.top + 1) * (zone.right - zone.left + 1);\n}\n/**\n * Check if the zones are continuous, ie. if they can be merged into a single zone without\n * including cells outside the zones\n * */\nfunction areZonesContinuous(zones) {\n    if (zones.length < 2)\n        return true;\n    return recomputeZones(zones).length === 1;\n}\n/** Return all the columns in the given list of zones */\nfunction getZonesCols(zones) {\n    const set = new Set();\n    for (let zone of recomputeZones(zones)) {\n        for (let col of range(zone.left, zone.right + 1)) {\n            set.add(col);\n        }\n    }\n    return set;\n}\n/** Return all the rows in the given list of zones */\nfunction getZonesRows(zones) {\n    const set = new Set();\n    for (let zone of recomputeZones(zones)) {\n        for (let row of range(zone.top, zone.bottom + 1)) {\n            set.add(row);\n        }\n    }\n    return set;\n}\n/**\n * Check if two zones are contiguous, ie. that they share a border\n */\nfunction areZoneContiguous(zone1, zone2) {\n    if (zone1.right + 1 === zone2.left || zone1.left === zone2.right + 1) {\n        return ((zone1.top <= zone2.bottom && zone1.top >= zone2.top) ||\n            (zone2.top <= zone1.bottom && zone2.top >= zone1.top));\n    }\n    if (zone1.bottom + 1 === zone2.top || zone1.top === zone2.bottom + 1) {\n        return ((zone1.left <= zone2.right && zone1.left >= zone2.left) ||\n            (zone2.left <= zone1.right && zone2.left >= zone1.left));\n    }\n    return false;\n}\n/**\n * Merge contiguous and overlapping zones that are in the array into bigger zones\n */\nfunction mergeContiguousZones(zones) {\n    const mergedZones = [...zones];\n    let hasMerged = true;\n    while (hasMerged) {\n        hasMerged = false;\n        for (let i = 0; i < mergedZones.length; i++) {\n            const zone = mergedZones[i];\n            const mergeableZoneIndex = mergedZones.findIndex((z, j) => i !== j && (areZoneContiguous(z, zone) || overlap(z, zone)));\n            if (mergeableZoneIndex !== -1) {\n                mergedZones[i] = union(mergedZones[mergeableZoneIndex], zone);\n                mergedZones.splice(mergeableZoneIndex, 1);\n                hasMerged = true;\n                break;\n            }\n        }\n    }\n    return mergedZones;\n}\n\n/**\n * Get the id of the given item (its key in the given dictionary).\n * If the given item does not exist in the dictionary, it creates one with a new id.\n */\nfunction getItemId(item, itemsDic) {\n    for (const key in itemsDic) {\n        if (deepEquals(itemsDic[key], item)) {\n            return parseInt(key, 10);\n        }\n    }\n    // Generate new Id if the item didn't exist in the dictionary\n    const ids = Object.keys(itemsDic);\n    const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));\n    itemsDic[maxId + 1] = item;\n    return maxId + 1;\n}\nfunction groupItemIdsByZones(positionsByItemId) {\n    const result = {};\n    for (const itemId in positionsByItemId) {\n        const zones = recomputeZones(positionsByItemId[itemId].map(positionToZone));\n        for (const zone of zones) {\n            result[zoneToXc(zone)] = Number(itemId);\n        }\n    }\n    return result;\n}\n\n// -----------------------------------------------------------------------------\n// Date Type\n// -----------------------------------------------------------------------------\n/**\n * A DateTime object that can be used to manipulate spreadsheet dates.\n * Conceptually, a spreadsheet date is simply a number with a date format,\n * and it is timezone-agnostic.\n * This DateTime object consistently uses UTC time to represent a naive date and time.\n */\nclass DateTime {\n    jsDate;\n    constructor(year, month, day, hours = 0, minutes = 0, seconds = 0) {\n        this.jsDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds, 0));\n    }\n    static fromTimestamp(timestamp) {\n        const date = new Date(timestamp);\n        return new DateTime(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());\n    }\n    static now() {\n        const now = new Date();\n        return new DateTime(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds());\n    }\n    toString() {\n        return this.jsDate.toString();\n    }\n    toLocaleDateString() {\n        return this.jsDate.toLocaleDateString();\n    }\n    getTime() {\n        return this.jsDate.getTime();\n    }\n    getFullYear() {\n        return this.jsDate.getUTCFullYear();\n    }\n    getMonth() {\n        return this.jsDate.getUTCMonth();\n    }\n    getQuarter() {\n        return Math.floor(this.getMonth() / 3) + 1;\n    }\n    getDate() {\n        return this.jsDate.getUTCDate();\n    }\n    getDay() {\n        return this.jsDate.getUTCDay();\n    }\n    getHours() {\n        return this.jsDate.getUTCHours();\n    }\n    getMinutes() {\n        return this.jsDate.getUTCMinutes();\n    }\n    getSeconds() {\n        return this.jsDate.getUTCSeconds();\n    }\n    getIsoWeek() {\n        const date = new Date(this.jsDate.getTime());\n        const dayNumber = date.getUTCDay() || 7;\n        date.setUTCDate(date.getUTCDate() + 4 - dayNumber);\n        const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));\n        return Math.ceil(((date.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);\n    }\n    setFullYear(year) {\n        return this.jsDate.setUTCFullYear(year);\n    }\n    setMonth(month) {\n        return this.jsDate.setUTCMonth(month);\n    }\n    setDate(date) {\n        return this.jsDate.setUTCDate(date);\n    }\n    setHours(hours) {\n        return this.jsDate.setUTCHours(hours);\n    }\n    setMinutes(minutes) {\n        return this.jsDate.setUTCMinutes(minutes);\n    }\n    setSeconds(seconds) {\n        return this.jsDate.setUTCSeconds(seconds);\n    }\n}\n// -----------------------------------------------------------------------------\n// Parsing\n// -----------------------------------------------------------------------------\nconst INITIAL_1900_DAY = new DateTime(1899, 11, 30);\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst CURRENT_MILLENIAL = 2000; // note: don't forget to update this in 2999\nconst CURRENT_YEAR = DateTime.now().getFullYear();\nconst CURRENT_MONTH = DateTime.now().getMonth();\nconst INITIAL_JS_DAY = DateTime.fromTimestamp(0);\nconst DATE_JS_1900_OFFSET = INITIAL_JS_DAY.getTime() - INITIAL_1900_DAY.getTime();\nconst mdyDateRegexp = /^\\d{1,2}(\\/|-|\\s)\\d{1,2}((\\/|-|\\s)\\d{1,4})?$/;\nconst ymdDateRegexp = /^\\d{3,4}(\\/|-|\\s)\\d{1,2}(\\/|-|\\s)\\d{1,2}$/;\nconst dateSeparatorsRegex = /\\/|-|\\s/;\nconst dateRegexp = /^(\\d{1,4})[\\/-\\s](\\d{1,4})([\\/-\\s](\\d{1,4}))?$/;\nconst timeRegexp = /((\\d+(:\\d+)?(:\\d+)?\\s*(AM|PM))|(\\d+:\\d+(:\\d+)?))$/;\n/** Convert a value number representing a date, or return undefined if it isn't possible */\nfunction valueToDateNumber(value, locale) {\n    switch (typeof value) {\n        case \"number\":\n            return value;\n        case \"string\":\n            if (isDateTime(value, locale)) {\n                return parseDateTime(value, locale)?.value;\n            }\n            return !value || isNaN(Number(value)) ? undefined : Number(value);\n        default:\n            return undefined;\n    }\n}\nfunction isDateTime(str, locale) {\n    return parseDateTime(str, locale) !== null;\n}\nconst CACHE = new Map();\nfunction parseDateTime(str, locale) {\n    if (!CACHE.has(locale)) {\n        CACHE.set(locale, new Map());\n    }\n    if (!CACHE.get(locale).has(str)) {\n        CACHE.get(locale).set(str, _parseDateTime(str, locale));\n    }\n    return CACHE.get(locale).get(str);\n}\nfunction _parseDateTime(str, locale) {\n    str = str.trim();\n    let time = null;\n    const timeMatch = str.match(timeRegexp);\n    if (timeMatch) {\n        time = parseTime(timeMatch[0]);\n        if (time === null) {\n            return null;\n        }\n        str = str.replace(timeMatch[0], \"\").trim();\n    }\n    let date = null;\n    const dateParts = getDateParts(str, locale);\n    if (dateParts) {\n        const separator = dateParts.dateString.match(dateSeparatorsRegex)[0];\n        date = parseDate(dateParts, separator);\n        if (date === null) {\n            return null;\n        }\n        str = str.replace(dateParts.dateString, \"\").trim();\n    }\n    if (str !== \"\" || !(date || time)) {\n        return null;\n    }\n    if (date && date.jsDate && time && time.jsDate) {\n        return {\n            value: date.value + time.value,\n            format: date.format + \" \" + (time.format === \"hhhh:mm:ss\" ? \"hh:mm:ss\" : time.format),\n            jsDate: new DateTime(date.jsDate.getFullYear() + time.jsDate.getFullYear() - 1899, date.jsDate.getMonth() + time.jsDate.getMonth() - 11, date.jsDate.getDate() + time.jsDate.getDate() - 30, date.jsDate.getHours() + time.jsDate.getHours(), date.jsDate.getMinutes() + time.jsDate.getMinutes(), date.jsDate.getSeconds() + time.jsDate.getSeconds()),\n        };\n    }\n    return date || time;\n}\n/**\n * Returns the parts (day/month/year) of a date string corresponding to the given locale.\n *\n * - A string \"xxxx-xx-xx\" will be parsed as \"y-m-d\" no matter the locale.\n * - A string \"xx-xx-xxxx\" will be parsed as \"m-d-y\" for mdy locale, and \"d-m-y\" for ymd and dmy locales.\n * - A string \"xx-xx-xx\" will be \"y-m-d\" for ymd locale, \"d-m-y\" for dmy locale, \"m-d-y\" for mdy locale.\n * - A string \"xxxx-xx\" will be parsed as \"y-m\" no matter the locale.\n * - A string \"xx-xx\" will be parsed as \"m-d\" for mdy and ymd locales, and \"d-m\" for dmy locale.\n */\nfunction getDateParts(dateString, locale) {\n    const match = dateString.match(dateRegexp);\n    if (!match) {\n        return null;\n    }\n    const [, part1, part2, , part3] = match;\n    if (part1.length > 2 && part3 && part3.length > 2) {\n        return null;\n    }\n    if (part1.length > 2) {\n        return { year: part1, month: part2, day: part3, dateString, type: \"ymd\" };\n    }\n    const localeDateType = getLocaleDateFormatType(locale);\n    if (!part3) {\n        if (part2.length > 2) {\n            // e.g. 11/2023\n            return { month: part1, year: part2, day: undefined, dateString, type: localeDateType };\n        }\n        if (localeDateType === \"dmy\") {\n            return { day: part1, month: part2, year: part3, dateString, type: \"dmy\" };\n        }\n        return { month: part1, day: part2, year: part3, dateString, type: \"mdy\" };\n    }\n    if (part3.length > 2) {\n        if (localeDateType === \"mdy\") {\n            return { month: part1, day: part2, year: part3, dateString, type: \"mdy\" };\n        }\n        return { day: part1, month: part2, year: part3, dateString, type: \"dmy\" };\n    }\n    if (localeDateType === \"mdy\") {\n        return { month: part1, day: part2, year: part3, dateString, type: \"mdy\" };\n    }\n    if (localeDateType === \"ymd\") {\n        return { year: part1, month: part2, day: part3, dateString, type: \"ymd\" };\n    }\n    if (localeDateType === \"dmy\") {\n        return { day: part1, month: part2, year: part3, dateString, type: \"dmy\" };\n    }\n    return null;\n}\nfunction getLocaleDateFormatType(locale) {\n    switch (locale.dateFormat[0]) {\n        case \"d\":\n            return \"dmy\";\n        case \"m\":\n            return \"mdy\";\n        case \"y\":\n            return \"ymd\";\n    }\n    throw new Error(\"Invalid date format in locale\");\n}\nfunction parseDate(parts, separator) {\n    let { year: yearStr, month: monthStr, day: dayStr } = parts;\n    const month = inferMonth(monthStr);\n    const day = inferDay(dayStr);\n    const year = inferYear(yearStr);\n    if (year === null || month === null || day === null) {\n        return null;\n    }\n    // month + 1: months are 0-indexed in JS\n    const leadingZero = (monthStr?.length === 2 && month + 1 < 10) || (dayStr?.length === 2 && day < 10);\n    const fullYear = yearStr?.length !== 2;\n    const jsDate = new DateTime(year, month, day);\n    if (jsDate.getMonth() !== month || jsDate.getDate() !== day) {\n        // invalid date\n        return null;\n    }\n    const delta = jsDate.getTime() - INITIAL_1900_DAY.getTime();\n    const format = getFormatFromDateParts(parts, separator, leadingZero, fullYear);\n    return {\n        value: Math.round(delta / MS_PER_DAY),\n        format: format,\n        jsDate,\n    };\n}\nfunction getFormatFromDateParts(parts, sep, leadingZero, fullYear) {\n    const yearFmt = parts.year ? (fullYear ? \"yyyy\" : \"yy\") : undefined;\n    const monthFmt = parts.month ? (leadingZero ? \"mm\" : \"m\") : undefined;\n    const dayFmt = parts.day ? (leadingZero ? \"dd\" : \"d\") : undefined;\n    switch (parts.type) {\n        case \"mdy\":\n            return [monthFmt, dayFmt, yearFmt].filter(isDefined).join(sep);\n        case \"ymd\":\n            return [yearFmt, monthFmt, dayFmt].filter(isDefined).join(sep);\n        case \"dmy\":\n            return [dayFmt, monthFmt, yearFmt].filter(isDefined).join(sep);\n    }\n}\nfunction inferYear(yearStr) {\n    if (!yearStr) {\n        return CURRENT_YEAR;\n    }\n    const nbr = Number(yearStr);\n    switch (yearStr.length) {\n        case 1:\n            return CURRENT_MILLENIAL + nbr;\n        case 2:\n            const offset = CURRENT_MILLENIAL + nbr > CURRENT_YEAR + 10 ? -100 : 0;\n            const base = CURRENT_MILLENIAL + offset;\n            return base + nbr;\n        case 3:\n        case 4:\n            return nbr;\n    }\n    return null;\n}\nfunction inferMonth(monthStr) {\n    if (!monthStr) {\n        return CURRENT_MONTH;\n    }\n    const nbr = Number(monthStr);\n    if (nbr >= 1 && nbr <= 12) {\n        return nbr - 1;\n    }\n    return null;\n}\nfunction inferDay(dayStr) {\n    if (!dayStr) {\n        return 1;\n    }\n    const nbr = Number(dayStr);\n    if (nbr >= 0 && nbr <= 31) {\n        return nbr;\n    }\n    return null;\n}\nfunction parseTime(str) {\n    str = str.trim();\n    if (timeRegexp.test(str)) {\n        const isAM = /AM/i.test(str);\n        const isPM = /PM/i.test(str);\n        const strTime = isAM || isPM ? str.substring(0, str.length - 2).trim() : str;\n        const parts = strTime.split(/:/);\n        const isMinutes = parts.length >= 2;\n        const isSeconds = parts.length === 3;\n        let hours = Number(parts[0]);\n        let minutes = isMinutes ? Number(parts[1]) : 0;\n        let seconds = isSeconds ? Number(parts[2]) : 0;\n        let format = isSeconds ? \"hh:mm:ss\" : \"hh:mm\";\n        if (isAM || isPM) {\n            format += \" a\";\n        }\n        else if (!isMinutes) {\n            return null;\n        }\n        if (hours >= 12 && isAM) {\n            hours -= 12;\n        }\n        else if (hours < 12 && isPM) {\n            hours += 12;\n        }\n        minutes += Math.floor(seconds / 60);\n        seconds %= 60;\n        hours += Math.floor(minutes / 60);\n        minutes %= 60;\n        if (hours >= 24) {\n            format = \"hhhh:mm:ss\";\n        }\n        const jsDate = new DateTime(1899, 11, 30, hours, minutes, seconds);\n        return {\n            value: hours / 24 + minutes / 1440 + seconds / 86400,\n            format: format,\n            jsDate: jsDate,\n        };\n    }\n    return null;\n}\n// -----------------------------------------------------------------------------\n// Conversion\n// -----------------------------------------------------------------------------\nfunction numberToJsDate(value) {\n    const truncValue = Math.trunc(value);\n    let date = DateTime.fromTimestamp(truncValue * MS_PER_DAY - DATE_JS_1900_OFFSET);\n    let time = value - truncValue;\n    time = time < 0 ? 1 + time : time;\n    const hours = Math.round(time * 24);\n    const minutes = Math.round((time - hours / 24) * 24 * 60);\n    const seconds = Math.round((time - hours / 24 - minutes / 24 / 60) * 24 * 60 * 60);\n    date.setHours(hours);\n    date.setMinutes(minutes);\n    date.setSeconds(seconds);\n    return date;\n}\nfunction jsDateToRoundNumber(date) {\n    return Math.round(jsDateToNumber(date));\n}\nfunction jsDateToNumber(date) {\n    const delta = date.getTime() - INITIAL_1900_DAY.getTime();\n    return delta / MS_PER_DAY;\n}\n/** Return the number of days in the current month of the given date */\nfunction getDaysInMonth(date) {\n    return new DateTime(date.getFullYear(), date.getMonth() + 1, 0).getDate();\n}\nfunction isLastDayOfMonth(date) {\n    return getDaysInMonth(date) === date.getDate();\n}\n/**\n * Add a certain number of months to a date. This will adapt the month number, and possibly adapt\n * the day of the month to keep it in the month.\n *\n * For example \"31/12/2020\" minus one month will be \"30/11/2020\", and not \"31/11/2020\"\n *\n * @param keepEndOfMonth if true, if the given date was the last day of a month, the returned date will\n *          also always be the last day of a month.\n */\nfunction addMonthsToDate(date, months, keepEndOfMonth) {\n    const yStart = date.getFullYear();\n    const mStart = date.getMonth();\n    const dStart = date.getDate();\n    const jsDate = new DateTime(yStart, mStart + months, 1);\n    if (keepEndOfMonth && dStart === getDaysInMonth(date)) {\n        jsDate.setDate(getDaysInMonth(jsDate));\n    }\n    else if (dStart > getDaysInMonth(jsDate)) {\n        // 31/03 minus one month should be 28/02, not 31/02\n        jsDate.setDate(getDaysInMonth(jsDate));\n    }\n    else {\n        jsDate.setDate(dStart);\n    }\n    return jsDate;\n}\nfunction isLeapYear(year) {\n    const _year = Math.trunc(year);\n    return (_year % 4 === 0 && _year % 100 != 0) || _year % 400 === 0;\n}\nfunction getYearFrac(startDate, endDate, _dayCountConvention) {\n    if (startDate === endDate) {\n        return 0;\n    }\n    if (startDate > endDate) {\n        const stack = endDate;\n        endDate = startDate;\n        startDate = stack;\n    }\n    const jsStartDate = numberToJsDate(startDate);\n    const jsEndDate = numberToJsDate(endDate);\n    let dayStart = jsStartDate.getDate();\n    let dayEnd = jsEndDate.getDate();\n    const monthStart = jsStartDate.getMonth(); // january is 0\n    const monthEnd = jsEndDate.getMonth(); // january is 0\n    const yearStart = jsStartDate.getFullYear();\n    const yearEnd = jsEndDate.getFullYear();\n    let yearsStart = 0;\n    let yearsEnd = 0;\n    switch (_dayCountConvention) {\n        // 30/360 US convention --------------------------------------------------\n        case 0:\n            if (dayStart === 31)\n                dayStart = 30;\n            if (dayStart === 30 && dayEnd === 31)\n                dayEnd = 30;\n            // If jsStartDate is the last day of February\n            if (monthStart === 1 && dayStart === (isLeapYear(yearStart) ? 29 : 28)) {\n                dayStart = 30;\n                // If jsEndDate is the last day of February\n                if (monthEnd === 1 && dayEnd === (isLeapYear(yearEnd) ? 29 : 28)) {\n                    dayEnd = 30;\n                }\n            }\n            yearsStart = yearStart + (monthStart * 30 + dayStart) / 360;\n            yearsEnd = yearEnd + (monthEnd * 30 + dayEnd) / 360;\n            break;\n        // actual/actual convention ----------------------------------------------\n        case 1:\n            let daysInYear = 365;\n            const isSameYear = yearStart === yearEnd;\n            const isOneDeltaYear = yearStart + 1 === yearEnd;\n            const isMonthEndBigger = monthStart < monthEnd;\n            const isSameMonth = monthStart === monthEnd;\n            const isDayEndBigger = dayStart < dayEnd;\n            // |-----|  <-- one Year\n            // 'A' is start date\n            // 'B' is end date\n            if ((!isSameYear && !isOneDeltaYear) ||\n                (!isSameYear && isMonthEndBigger) ||\n                (!isSameYear && isSameMonth && isDayEndBigger)) {\n                // |---A-|-----|-B---|  <-- !isSameYear && !isOneDeltaYear\n                // |---A-|----B|-----|  <-- !isSameYear && isMonthEndBigger\n                // |---A-|---B-|-----|  <-- !isSameYear && isSameMonth && isDayEndBigger\n                let countYears = 0;\n                let countDaysInYears = 0;\n                for (let y = yearStart; y <= yearEnd; y++) {\n                    countYears++;\n                    countDaysInYears += isLeapYear(y) ? 366 : 365;\n                }\n                daysInYear = countDaysInYears / countYears;\n            }\n            else if (!isSameYear) {\n                // |-AF--|B----|-----|\n                if (isLeapYear(yearStart) && monthStart < 2) {\n                    daysInYear = 366;\n                }\n                // |--A--|FB---|-----|\n                if (isLeapYear(yearEnd) && (monthEnd > 1 || (monthEnd === 1 && dayEnd === 29))) {\n                    daysInYear = 366;\n                }\n            }\n            else {\n                // remaining cases:\n                //\n                // |-F-AB|-----|-----|\n                // |AB-F-|-----|-----|\n                // |A-F-B|-----|-----|\n                // if February 29 occurs between date1 (exclusive) and date2 (inclusive)\n                // daysInYear --> 366\n                if (isLeapYear(yearStart)) {\n                    daysInYear = 366;\n                }\n            }\n            yearsStart = startDate / daysInYear;\n            yearsEnd = endDate / daysInYear;\n            break;\n        // actual/360 convention -------------------------------------------------\n        case 2:\n            yearsStart = startDate / 360;\n            yearsEnd = endDate / 360;\n            break;\n        // actual/365 convention -------------------------------------------------\n        case 3:\n            yearsStart = startDate / 365;\n            yearsEnd = endDate / 365;\n            break;\n        // 30/360 European convention --------------------------------------------\n        case 4:\n            if (dayStart === 31)\n                dayStart = 30;\n            if (dayEnd === 31)\n                dayEnd = 30;\n            yearsStart = yearStart + (monthStart * 30 + dayStart) / 360;\n            yearsEnd = yearEnd + (monthEnd * 30 + dayEnd) / 360;\n            break;\n    }\n    return yearsEnd - yearsStart;\n}\n/**\n * Get the number of whole months between two dates.\n * e.g.\n *  2002/01/01 -> 2002/02/01 = 1 month,\n *  2002/01/01 -> 2003/02/01 = 13 months\n * @param startDate\n * @param endDate\n * @returns\n */\nfunction getTimeDifferenceInWholeMonths(startDate, endDate) {\n    const months = (endDate.getFullYear() - startDate.getFullYear()) * 12 +\n        endDate.getMonth() -\n        startDate.getMonth();\n    return startDate.getDate() > endDate.getDate() ? months - 1 : months;\n}\nfunction getTimeDifferenceInWholeDays(startDate, endDate) {\n    const startUtc = startDate.getTime();\n    const endUtc = endDate.getTime();\n    return Math.floor((endUtc - startUtc) / MS_PER_DAY);\n}\nfunction getTimeDifferenceInWholeYears(startDate, endDate) {\n    const years = endDate.getFullYear() - startDate.getFullYear();\n    const monthStart = startDate.getMonth();\n    const monthEnd = endDate.getMonth();\n    const dateStart = startDate.getDate();\n    const dateEnd = endDate.getDate();\n    const isEndMonthDateBigger = monthEnd > monthStart || (monthEnd === monthStart && dateEnd >= dateStart);\n    return isEndMonthDateBigger ? years : years - 1;\n}\nfunction areTwoDatesWithinOneYear(startDate, endDate) {\n    return getYearFrac(startDate, endDate, 1) < 1;\n}\nfunction areDatesSameDay(startDate, endDate) {\n    return Math.trunc(startDate) === Math.trunc(endDate);\n}\nfunction isDateBetween(date, startDate, endDate) {\n    if (startDate > endDate) {\n        return isDateBetween(date, endDate, startDate);\n    }\n    date = Math.trunc(date);\n    startDate = Math.trunc(startDate);\n    endDate = Math.trunc(endDate);\n    return date >= startDate && date <= endDate;\n}\n/** Check if the first date is strictly before the second date */\nfunction isDateStrictlyBefore(date, dateBefore) {\n    return Math.trunc(date) < Math.trunc(dateBefore);\n}\n/** Check if the first date is before or equal to the second date */\nfunction isDateBefore(date, dateBefore) {\n    return Math.trunc(date) <= Math.trunc(dateBefore);\n}\n/** Check if the first date is strictly after the second date */\nfunction isDateStrictlyAfter(date, dateAfter) {\n    return Math.trunc(date) > Math.trunc(dateAfter);\n}\n/** Check if the first date is after or equal to the second date */\nfunction isDateAfter(date, dateAfter) {\n    return Math.trunc(date) >= Math.trunc(dateAfter);\n}\n\n/**\n * This function returns a regexp that is supposed to be as close as possible as the numberRegexp,\n * but its purpose is to be used by the tokenizer.\n *\n * - it tolerates extra characters at the end. This is useful because the tokenizer\n *   only needs to find the number at the start of a string\n * - it does not support % symbol, in formulas % is an operator\n */\nconst getFormulaNumberRegex = memoize(function getFormulaNumberRegex(decimalSeparator) {\n    decimalSeparator = escapeRegExp(decimalSeparator);\n    return new RegExp(`(?:^-?\\\\d+(?:${decimalSeparator}?\\\\d*(?:e\\\\d+)?)?|^-?${decimalSeparator}\\\\d+)(?!\\\\w|!)`);\n});\nconst getNumberRegex = memoize(function getNumberRegex(locale) {\n    const decimalSeparator = escapeRegExp(locale.decimalSeparator);\n    const thousandsSeparator = escapeRegExp(locale.thousandsSeparator || \"\");\n    const pIntegerAndDecimals = `(?:\\\\d+(?:${thousandsSeparator}\\\\d{3,})*(?:${decimalSeparator}\\\\d*)?)`; // pattern that match integer number with or without decimal digits\n    const pOnlyDecimals = `(?:${decimalSeparator}\\\\d+)`; // pattern that match only expression with decimal digits\n    const pScientificFormat = \"(?:e(?:\\\\+|-)?\\\\d+)?\"; // pattern that match scientific format between zero and one time (should be placed before pPercentFormat)\n    const pPercentFormat = \"(?:\\\\s*%)?\"; // pattern that match percent symbol between zero and one time\n    const pNumber = \"(?:\\\\s*\" +\n        pIntegerAndDecimals +\n        \"|\" +\n        pOnlyDecimals +\n        \")\" +\n        pScientificFormat +\n        pPercentFormat;\n    const pMinus = \"(?:\\\\s*-)?\"; // pattern that match negative symbol between zero and one time\n    const pCurrencyFormat = \"(?:\\\\s*[\\\\$\u20ac])?\";\n    const p1 = pMinus + pCurrencyFormat + pNumber;\n    const p2 = pMinus + pNumber + pCurrencyFormat;\n    const p3 = pCurrencyFormat + pMinus + pNumber;\n    const pNumberExp = \"^(?:(?:\" + [p1, p2, p3].join(\")|(?:\") + \"))$\";\n    const numberRegexp = new RegExp(pNumberExp, \"i\");\n    return numberRegexp;\n});\n/**\n * Return true if the argument is a \"number string\".\n *\n * Note that \"\" (empty string) does not count as a number string\n */\nfunction isNumber(value, locale) {\n    if (!value)\n        return false;\n    // TO DO: add regexp for DATE string format (ex match: \"28 02 2020\")\n    return getNumberRegex(locale).test(value.trim());\n}\nconst getInvaluableSymbolsRegexp = memoize(function getInvaluableSymbolsRegexp(locale) {\n    return new RegExp(`[\\$\u20ac${escapeRegExp(locale.thousandsSeparator || \"\")}]`, \"g\");\n});\n/**\n * Convert a string into a number. It assumes that the string actually represents\n * a number (as determined by the isNumber function)\n *\n * Note that it accepts \"\" (empty string), even though it does not count as a\n * number from the point of view of the isNumber function.\n */\nfunction parseNumber(str, locale) {\n    if (locale.decimalSeparator !== \".\") {\n        str = str.replace(locale.decimalSeparator, \".\");\n    }\n    // remove invaluable characters\n    str = str.replace(getInvaluableSymbolsRegexp(locale), \"\");\n    let n = Number(str);\n    if (isNaN(n) && str.includes(\"%\")) {\n        n = Number(str.split(\"%\")[0]);\n        if (!isNaN(n)) {\n            return n / 100;\n        }\n    }\n    return n;\n}\nfunction percentile(values, percent, isInclusive) {\n    const sortedValues = [...values].sort((a, b) => a - b);\n    let percentIndex = (sortedValues.length + (isInclusive ? -1 : 1)) * percent;\n    if (!isInclusive) {\n        percentIndex--;\n    }\n    if (Number.isInteger(percentIndex)) {\n        return sortedValues[percentIndex];\n    }\n    const indexSup = Math.ceil(percentIndex);\n    const indexLow = Math.floor(percentIndex);\n    return (sortedValues[indexSup] * (percentIndex - indexLow) +\n        sortedValues[indexLow] * (indexSup - percentIndex));\n}\n\nvar CellValueType;\n(function (CellValueType) {\n    CellValueType[\"boolean\"] = \"boolean\";\n    CellValueType[\"number\"] = \"number\";\n    CellValueType[\"text\"] = \"text\";\n    CellValueType[\"empty\"] = \"empty\";\n    CellValueType[\"error\"] = \"error\";\n})(CellValueType || (CellValueType = {}));\n\nvar ClipboardMIMEType;\n(function (ClipboardMIMEType) {\n    ClipboardMIMEType[\"PlainText\"] = \"text/plain\";\n    ClipboardMIMEType[\"Html\"] = \"text/html\";\n})(ClipboardMIMEType || (ClipboardMIMEType = {}));\n\nfunction isSheetDependent(cmd) {\n    return \"sheetId\" in cmd;\n}\nfunction isHeadersDependant(cmd) {\n    return \"dimension\" in cmd && \"sheetId\" in cmd && \"elements\" in cmd;\n}\nfunction isTargetDependent(cmd) {\n    return \"target\" in cmd && \"sheetId\" in cmd;\n}\nfunction isRangeDependant(cmd) {\n    return \"ranges\" in cmd;\n}\nfunction isZoneDependent(cmd) {\n    return \"zone\" in cmd;\n}\nfunction isPositionDependent(cmd) {\n    return \"col\" in cmd && \"row\" in cmd && \"sheetId\" in cmd;\n}\nconst invalidateEvaluationCommands = new Set([\n    \"RENAME_SHEET\",\n    \"DELETE_SHEET\",\n    \"CREATE_SHEET\",\n    \"DUPLICATE_SHEET\",\n    \"ADD_COLUMNS_ROWS\",\n    \"REMOVE_COLUMNS_ROWS\",\n    \"UNDO\",\n    \"REDO\",\n    \"ADD_MERGE\",\n    \"REMOVE_MERGE\",\n    \"UPDATE_LOCALE\",\n    \"ADD_PIVOT\",\n    \"UPDATE_PIVOT\",\n    \"INSERT_PIVOT\",\n    \"RENAME_PIVOT\",\n    \"REMOVE_PIVOT\",\n    \"DUPLICATE_PIVOT\",\n]);\nconst invalidateChartEvaluationCommands = new Set([\n    \"EVALUATE_CELLS\",\n    \"UPDATE_CELL\",\n    \"UNHIDE_COLUMNS_ROWS\",\n    \"HIDE_COLUMNS_ROWS\",\n    \"GROUP_HEADERS\",\n    \"UNGROUP_HEADERS\",\n    \"FOLD_ALL_HEADER_GROUPS\",\n    \"FOLD_HEADER_GROUP\",\n    \"FOLD_HEADER_GROUPS_IN_ZONE\",\n    \"UNFOLD_ALL_HEADER_GROUPS\",\n    \"UNFOLD_HEADER_GROUP\",\n    \"UNFOLD_HEADER_GROUPS_IN_ZONE\",\n    \"UPDATE_TABLE\",\n    \"UPDATE_FILTER\",\n    \"UNDO\",\n    \"REDO\",\n]);\nconst invalidateDependenciesCommands = new Set([\"MOVE_RANGES\"]);\nconst invalidateCFEvaluationCommands = new Set([\n    \"DUPLICATE_SHEET\",\n    \"EVALUATE_CELLS\",\n    \"ADD_CONDITIONAL_FORMAT\",\n    \"REMOVE_CONDITIONAL_FORMAT\",\n    \"CHANGE_CONDITIONAL_FORMAT_PRIORITY\",\n]);\nconst invalidateBordersCommands = new Set([\n    \"AUTOFILL_CELL\",\n    \"SET_BORDER\",\n    \"SET_ZONE_BORDERS\",\n]);\nconst readonlyAllowedCommands = new Set([\n    \"START\",\n    \"ACTIVATE_SHEET\",\n    \"COPY\",\n    \"RESIZE_SHEETVIEW\",\n    \"SET_VIEWPORT_OFFSET\",\n    \"EVALUATE_CELLS\",\n    \"SET_FORMULA_VISIBILITY\",\n    \"UPDATE_FILTER\",\n]);\nconst coreTypes = new Set([\n    /** CELLS */\n    \"UPDATE_CELL\",\n    \"UPDATE_CELL_POSITION\",\n    \"CLEAR_CELL\",\n    \"CLEAR_CELLS\",\n    \"DELETE_CONTENT\",\n    /** GRID SHAPE */\n    \"ADD_COLUMNS_ROWS\",\n    \"REMOVE_COLUMNS_ROWS\",\n    \"RESIZE_COLUMNS_ROWS\",\n    \"HIDE_COLUMNS_ROWS\",\n    \"UNHIDE_COLUMNS_ROWS\",\n    \"SET_GRID_LINES_VISIBILITY\",\n    \"UNFREEZE_COLUMNS\",\n    \"UNFREEZE_ROWS\",\n    \"FREEZE_COLUMNS\",\n    \"FREEZE_ROWS\",\n    \"UNFREEZE_COLUMNS_ROWS\",\n    /** MERGE */\n    \"ADD_MERGE\",\n    \"REMOVE_MERGE\",\n    /** SHEETS MANIPULATION */\n    \"CREATE_SHEET\",\n    \"DELETE_SHEET\",\n    \"DUPLICATE_SHEET\",\n    \"MOVE_SHEET\",\n    \"RENAME_SHEET\",\n    \"COLOR_SHEET\",\n    \"HIDE_SHEET\",\n    \"SHOW_SHEET\",\n    /** RANGES MANIPULATION */\n    \"MOVE_RANGES\",\n    /** CONDITIONAL FORMAT */\n    \"ADD_CONDITIONAL_FORMAT\",\n    \"REMOVE_CONDITIONAL_FORMAT\",\n    \"CHANGE_CONDITIONAL_FORMAT_PRIORITY\",\n    /** FIGURES */\n    \"CREATE_FIGURE\",\n    \"DELETE_FIGURE\",\n    \"UPDATE_FIGURE\",\n    /** FORMATTING */\n    \"SET_FORMATTING\",\n    \"CLEAR_FORMATTING\",\n    \"SET_BORDER\",\n    \"SET_ZONE_BORDERS\",\n    /** CHART */\n    \"CREATE_CHART\",\n    \"UPDATE_CHART\",\n    /** FILTERS */\n    \"CREATE_TABLE\",\n    \"REMOVE_TABLE\",\n    \"UPDATE_TABLE\",\n    \"CREATE_TABLE_STYLE\",\n    \"REMOVE_TABLE_STYLE\",\n    /** IMAGE */\n    \"CREATE_IMAGE\",\n    /** HEADER GROUP */\n    \"GROUP_HEADERS\",\n    \"UNGROUP_HEADERS\",\n    \"UNFOLD_HEADER_GROUP\",\n    \"FOLD_HEADER_GROUP\",\n    \"FOLD_ALL_HEADER_GROUPS\",\n    \"UNFOLD_ALL_HEADER_GROUPS\",\n    \"UNFOLD_HEADER_GROUPS_IN_ZONE\",\n    \"FOLD_HEADER_GROUPS_IN_ZONE\",\n    /** DATA VALIDATION */\n    \"ADD_DATA_VALIDATION_RULE\",\n    \"REMOVE_DATA_VALIDATION_RULE\",\n    /** MISC */\n    \"UPDATE_LOCALE\",\n    /** PIVOT */\n    \"ADD_PIVOT\",\n    \"UPDATE_PIVOT\",\n    \"INSERT_PIVOT\",\n    \"RENAME_PIVOT\",\n    \"REMOVE_PIVOT\",\n    \"DUPLICATE_PIVOT\",\n]);\nfunction isCoreCommand(cmd) {\n    return coreTypes.has(cmd.type);\n}\nfunction canExecuteInReadonly(cmd) {\n    return readonlyAllowedCommands.has(cmd.type);\n}\n/**\n * Holds the result of a command dispatch.\n * The command may have been successfully dispatched or cancelled\n * for one or more reasons.\n */\nclass DispatchResult {\n    reasons;\n    constructor(results = []) {\n        if (!Array.isArray(results)) {\n            results = [results];\n        }\n        results = [...new Set(results)];\n        this.reasons = results.filter((result) => result !== \"Success\" /* CommandResult.Success */);\n    }\n    /**\n     * Static helper which returns a successful DispatchResult\n     */\n    static get Success() {\n        return SUCCESS;\n    }\n    get isSuccessful() {\n        return this.reasons.length === 0;\n    }\n    /**\n     * Check if the dispatch has been cancelled because of\n     * the given reason.\n     */\n    isCancelledBecause(reason) {\n        return this.reasons.includes(reason);\n    }\n}\nconst SUCCESS = new DispatchResult();\nvar CommandResult;\n(function (CommandResult) {\n    CommandResult[\"Success\"] = \"Success\";\n    CommandResult[\"CancelledForUnknownReason\"] = \"CancelledForUnknownReason\";\n    CommandResult[\"WillRemoveExistingMerge\"] = \"WillRemoveExistingMerge\";\n    CommandResult[\"MergeIsDestructive\"] = \"MergeIsDestructive\";\n    CommandResult[\"CellIsMerged\"] = \"CellIsMerged\";\n    CommandResult[\"InvalidTarget\"] = \"InvalidTarget\";\n    CommandResult[\"EmptyUndoStack\"] = \"EmptyUndoStack\";\n    CommandResult[\"EmptyRedoStack\"] = \"EmptyRedoStack\";\n    CommandResult[\"NotEnoughElements\"] = \"NotEnoughElements\";\n    CommandResult[\"NotEnoughSheets\"] = \"NotEnoughSheets\";\n    CommandResult[\"MissingSheetName\"] = \"MissingSheetName\";\n    CommandResult[\"UnchangedSheetName\"] = \"UnchangedSheetName\";\n    CommandResult[\"DuplicatedSheetName\"] = \"DuplicatedSheetName\";\n    CommandResult[\"DuplicatedSheetId\"] = \"DuplicatedSheetId\";\n    CommandResult[\"ForbiddenCharactersInSheetName\"] = \"ForbiddenCharactersInSheetName\";\n    CommandResult[\"WrongSheetMove\"] = \"WrongSheetMove\";\n    CommandResult[\"WrongSheetPosition\"] = \"WrongSheetPosition\";\n    CommandResult[\"InvalidAnchorZone\"] = \"InvalidAnchorZone\";\n    CommandResult[\"SelectionOutOfBound\"] = \"SelectionOutOfBound\";\n    CommandResult[\"TargetOutOfSheet\"] = \"TargetOutOfSheet\";\n    CommandResult[\"WrongCutSelection\"] = \"WrongCutSelection\";\n    CommandResult[\"WrongPasteSelection\"] = \"WrongPasteSelection\";\n    CommandResult[\"WrongPasteOption\"] = \"WrongPasteOption\";\n    CommandResult[\"WrongFigurePasteOption\"] = \"WrongFigurePasteOption\";\n    CommandResult[\"EmptyClipboard\"] = \"EmptyClipboard\";\n    CommandResult[\"EmptyRange\"] = \"EmptyRange\";\n    CommandResult[\"InvalidRange\"] = \"InvalidRange\";\n    CommandResult[\"InvalidZones\"] = \"InvalidZones\";\n    CommandResult[\"InvalidSheetId\"] = \"InvalidSheetId\";\n    CommandResult[\"InvalidFigureId\"] = \"InvalidFigureId\";\n    CommandResult[\"InputAlreadyFocused\"] = \"InputAlreadyFocused\";\n    CommandResult[\"MaximumRangesReached\"] = \"MaximumRangesReached\";\n    CommandResult[\"MinimumRangesReached\"] = \"MinimumRangesReached\";\n    CommandResult[\"InvalidChartDefinition\"] = \"InvalidChartDefinition\";\n    CommandResult[\"InvalidDataSet\"] = \"InvalidDataSet\";\n    CommandResult[\"InvalidLabelRange\"] = \"InvalidLabelRange\";\n    CommandResult[\"InvalidScorecardKeyValue\"] = \"InvalidScorecardKeyValue\";\n    CommandResult[\"InvalidScorecardBaseline\"] = \"InvalidScorecardBaseline\";\n    CommandResult[\"InvalidGaugeDataRange\"] = \"InvalidGaugeDataRange\";\n    CommandResult[\"EmptyGaugeRangeMin\"] = \"EmptyGaugeRangeMin\";\n    CommandResult[\"GaugeRangeMinNaN\"] = \"GaugeRangeMinNaN\";\n    CommandResult[\"EmptyGaugeRangeMax\"] = \"EmptyGaugeRangeMax\";\n    CommandResult[\"GaugeRangeMaxNaN\"] = \"GaugeRangeMaxNaN\";\n    CommandResult[\"GaugeRangeMinBiggerThanRangeMax\"] = \"GaugeRangeMinBiggerThanRangeMax\";\n    CommandResult[\"GaugeLowerInflectionPointNaN\"] = \"GaugeLowerInflectionPointNaN\";\n    CommandResult[\"GaugeUpperInflectionPointNaN\"] = \"GaugeUpperInflectionPointNaN\";\n    CommandResult[\"GaugeLowerBiggerThanUpper\"] = \"GaugeLowerBiggerThanUpper\";\n    CommandResult[\"InvalidAutofillSelection\"] = \"InvalidAutofillSelection\";\n    CommandResult[\"MinBiggerThanMax\"] = \"MinBiggerThanMax\";\n    CommandResult[\"LowerBiggerThanUpper\"] = \"LowerBiggerThanUpper\";\n    CommandResult[\"MidBiggerThanMax\"] = \"MidBiggerThanMax\";\n    CommandResult[\"MinBiggerThanMid\"] = \"MinBiggerThanMid\";\n    CommandResult[\"FirstArgMissing\"] = \"FirstArgMissing\";\n    CommandResult[\"SecondArgMissing\"] = \"SecondArgMissing\";\n    CommandResult[\"MinNaN\"] = \"MinNaN\";\n    CommandResult[\"MidNaN\"] = \"MidNaN\";\n    CommandResult[\"MaxNaN\"] = \"MaxNaN\";\n    CommandResult[\"ValueUpperInflectionNaN\"] = \"ValueUpperInflectionNaN\";\n    CommandResult[\"ValueLowerInflectionNaN\"] = \"ValueLowerInflectionNaN\";\n    CommandResult[\"MinInvalidFormula\"] = \"MinInvalidFormula\";\n    CommandResult[\"MidInvalidFormula\"] = \"MidInvalidFormula\";\n    CommandResult[\"MaxInvalidFormula\"] = \"MaxInvalidFormula\";\n    CommandResult[\"ValueUpperInvalidFormula\"] = \"ValueUpperInvalidFormula\";\n    CommandResult[\"ValueLowerInvalidFormula\"] = \"ValueLowerInvalidFormula\";\n    CommandResult[\"InvalidSortZone\"] = \"InvalidSortZone\";\n    CommandResult[\"WaitingSessionConfirmation\"] = \"WaitingSessionConfirmation\";\n    CommandResult[\"MergeOverlap\"] = \"MergeOverlap\";\n    CommandResult[\"TooManyHiddenElements\"] = \"TooManyHiddenElements\";\n    CommandResult[\"Readonly\"] = \"Readonly\";\n    CommandResult[\"InvalidViewportSize\"] = \"InvalidViewportSize\";\n    CommandResult[\"InvalidScrollingDirection\"] = \"InvalidScrollingDirection\";\n    CommandResult[\"ViewportScrollLimitsReached\"] = \"ViewportScrollLimitsReached\";\n    CommandResult[\"FigureDoesNotExist\"] = \"FigureDoesNotExist\";\n    CommandResult[\"InvalidConditionalFormatId\"] = \"InvalidConditionalFormatId\";\n    CommandResult[\"InvalidCellPopover\"] = \"InvalidCellPopover\";\n    CommandResult[\"EmptyTarget\"] = \"EmptyTarget\";\n    CommandResult[\"InvalidFreezeQuantity\"] = \"InvalidFreezeQuantity\";\n    CommandResult[\"FrozenPaneOverlap\"] = \"FrozenPaneOverlap\";\n    CommandResult[\"ValuesNotChanged\"] = \"ValuesNotChanged\";\n    CommandResult[\"InvalidFilterZone\"] = \"InvalidFilterZone\";\n    CommandResult[\"TableNotFound\"] = \"TableNotFound\";\n    CommandResult[\"TableOverlap\"] = \"TableOverlap\";\n    CommandResult[\"InvalidTableConfig\"] = \"InvalidTableConfig\";\n    CommandResult[\"InvalidTableStyle\"] = \"InvalidTableStyle\";\n    CommandResult[\"FilterNotFound\"] = \"FilterNotFound\";\n    CommandResult[\"MergeInTable\"] = \"MergeInTable\";\n    CommandResult[\"NonContinuousTargets\"] = \"NonContinuousTargets\";\n    CommandResult[\"DuplicatedFigureId\"] = \"DuplicatedFigureId\";\n    CommandResult[\"InvalidSelectionStep\"] = \"InvalidSelectionStep\";\n    CommandResult[\"DuplicatedChartId\"] = \"DuplicatedChartId\";\n    CommandResult[\"ChartDoesNotExist\"] = \"ChartDoesNotExist\";\n    CommandResult[\"InvalidHeaderIndex\"] = \"InvalidHeaderIndex\";\n    CommandResult[\"InvalidQuantity\"] = \"InvalidQuantity\";\n    CommandResult[\"MoreThanOneColumnSelected\"] = \"MoreThanOneColumnSelected\";\n    CommandResult[\"EmptySplitSeparator\"] = \"EmptySplitSeparator\";\n    CommandResult[\"SplitWillOverwriteContent\"] = \"SplitWillOverwriteContent\";\n    CommandResult[\"NoSplitSeparatorInSelection\"] = \"NoSplitSeparatorInSelection\";\n    CommandResult[\"NoActiveSheet\"] = \"NoActiveSheet\";\n    CommandResult[\"InvalidLocale\"] = \"InvalidLocale\";\n    CommandResult[\"MoreThanOneRangeSelected\"] = \"MoreThanOneRangeSelected\";\n    CommandResult[\"NoColumnsProvided\"] = \"NoColumnsProvided\";\n    CommandResult[\"ColumnsNotIncludedInZone\"] = \"ColumnsNotIncludedInZone\";\n    CommandResult[\"DuplicatesColumnsSelected\"] = \"DuplicatesColumnsSelected\";\n    CommandResult[\"InvalidHeaderGroupStartEnd\"] = \"InvalidHeaderGroupStartEnd\";\n    CommandResult[\"HeaderGroupAlreadyExists\"] = \"HeaderGroupAlreadyExists\";\n    CommandResult[\"UnknownHeaderGroup\"] = \"UnknownHeaderGroup\";\n    CommandResult[\"UnknownDataValidationRule\"] = \"UnknownDataValidationRule\";\n    CommandResult[\"UnknownDataValidationCriterionType\"] = \"UnknownDataValidationCriterionType\";\n    CommandResult[\"InvalidDataValidationCriterionValue\"] = \"InvalidDataValidationCriterionValue\";\n    CommandResult[\"InvalidNumberOfCriterionValues\"] = \"InvalidNumberOfCriterionValues\";\n    CommandResult[\"InvalidCopyPasteSelection\"] = \"InvalidCopyPasteSelection\";\n    CommandResult[\"NoChanges\"] = \"NoChanges\";\n    CommandResult[\"InvalidInputId\"] = \"InvalidInputId\";\n    CommandResult[\"SheetIsHidden\"] = \"SheetIsHidden\";\n    CommandResult[\"InvalidTableResize\"] = \"InvalidTableResize\";\n    CommandResult[\"PivotIdNotFound\"] = \"PivotIdNotFound\";\n    CommandResult[\"EmptyName\"] = \"EmptyName\";\n    CommandResult[\"ValueCellIsInvalidFormula\"] = \"ValueCellIsInvalidFormula\";\n    CommandResult[\"InvalidDefinition\"] = \"InvalidDefinition\";\n    CommandResult[\"InvalidColor\"] = \"InvalidColor\";\n})(CommandResult || (CommandResult = {}));\n\nconst DEFAULT_LOCALES = [\n    {\n        name: \"English (US)\",\n        code: \"en_US\",\n        thousandsSeparator: \",\",\n        decimalSeparator: \".\",\n        weekStart: 7, // Sunday\n        dateFormat: \"m/d/yyyy\",\n        timeFormat: \"hh:mm:ss a\",\n        formulaArgSeparator: \",\",\n    },\n    {\n        name: \"French\",\n        code: \"fr_FR\",\n        thousandsSeparator: \" \",\n        decimalSeparator: \",\",\n        weekStart: 1, // Monday\n        dateFormat: \"dd/mm/yyyy\",\n        timeFormat: \"hh:mm:ss\",\n        formulaArgSeparator: \";\",\n    },\n];\nconst DEFAULT_LOCALE = DEFAULT_LOCALES[0];\n\nconst borderStyles = [\"thin\", \"medium\", \"thick\", \"dashed\", \"dotted\"];\nfunction isMatrix(x) {\n    return Array.isArray(x) && Array.isArray(x[0]);\n}\nvar DIRECTION;\n(function (DIRECTION) {\n    DIRECTION[\"UP\"] = \"up\";\n    DIRECTION[\"DOWN\"] = \"down\";\n    DIRECTION[\"LEFT\"] = \"left\";\n    DIRECTION[\"RIGHT\"] = \"right\";\n})(DIRECTION || (DIRECTION = {}));\n\nconst LAYERS = {\n    Background: 0,\n    Highlights: 1,\n    Clipboard: 2,\n    Chart: 4,\n    Autofill: 5,\n    Selection: 6,\n    Headers: 100, // ensure that we end up on  top\n};\nconst OrderedLayers = memoize(() => Object.keys(LAYERS).sort((a, b) => LAYERS[a] - LAYERS[b]));\n/**\n *\n * @param layer New layer name\n * @param priority The lower priorities are rendered first\n */\nfunction addRenderingLayer(layer, priority) {\n    if (LAYERS[layer]) {\n        throw new Error(`Layer ${layer} already exists`);\n    }\n    LAYERS[layer] = priority;\n}\n\nconst CellErrorType = {\n    NotAvailable: \"#N/A\",\n    InvalidReference: \"#REF\",\n    BadExpression: \"#BAD_EXPR\",\n    CircularDependency: \"#CYCLE\",\n    UnknownFunction: \"#NAME?\",\n    DivisionByZero: \"#DIV/0!\",\n    SpilledBlocked: \"#SPILL!\",\n    GenericError: \"#ERROR\",\n    NullError: \"#NULL!\",\n};\nconst errorTypes = new Set(Object.values(CellErrorType));\nclass EvaluationError extends Error {\n    value;\n    constructor(message = _t(\"Error\"), value = CellErrorType.GenericError) {\n        super(message);\n        this.value = value;\n    }\n}\nclass BadExpressionError extends EvaluationError {\n    constructor(message = _t(\"Invalid expression\")) {\n        super(message, CellErrorType.BadExpression);\n    }\n}\nclass CircularDependencyError extends EvaluationError {\n    constructor(message = _t(\"Circular reference\")) {\n        super(message, CellErrorType.CircularDependency);\n    }\n}\nclass InvalidReferenceError extends EvaluationError {\n    constructor(message = _t(\"Invalid reference\")) {\n        super(message, CellErrorType.InvalidReference);\n    }\n}\nclass NotAvailableError extends EvaluationError {\n    constructor(message = _t(\"Data not available\")) {\n        super(message, CellErrorType.NotAvailable);\n    }\n}\nclass UnknownFunctionError extends EvaluationError {\n    constructor(message = _t(\"Unknown function\")) {\n        super(message, CellErrorType.UnknownFunction);\n    }\n}\nclass SplillBlockedError extends EvaluationError {\n    constructor(message = _t(\"Spill range is not empty\")) {\n        super(message, CellErrorType.SpilledBlocked);\n    }\n}\n\n// HELPERS\nconst SORT_TYPES_ORDER = [\"number\", \"string\", \"boolean\", \"undefined\"];\nfunction assert(condition, message, value) {\n    if (!condition()) {\n        throw new EvaluationError(message, value);\n    }\n}\nfunction inferFormat(data) {\n    if (data === undefined) {\n        return undefined;\n    }\n    if (isMatrix(data)) {\n        return data[0][0]?.format;\n    }\n    return data.format;\n}\nfunction isEvaluationError(error) {\n    return typeof error === \"string\" && errorTypes.has(error);\n}\n// -----------------------------------------------------------------------------\n// FORMAT FUNCTIONS\n// -----------------------------------------------------------------------------\nconst expectNumberValueError = (value) => _t(\"The function [[FUNCTION_NAME]] expects a number value, but '%s' is a string, and cannot be coerced to a number.\", value);\nconst expectNumberRangeError = (lowerBound, upperBound, value) => _t(\"The function [[FUNCTION_NAME]] expects a number value between %s and %s inclusive, but receives %s.\", lowerBound.toString(), upperBound.toString(), value.toString());\nconst expectStringSetError = (stringSet, value) => {\n    const stringSetString = stringSet.map((str) => `'${str}'`).join(\", \");\n    return _t(\"The function [[FUNCTION_NAME]] has an argument with value '%s'. It should be one of: %s.\", value, stringSetString);\n};\nfunction toNumber(data, locale) {\n    const value = toValue(data);\n    switch (typeof value) {\n        case \"number\":\n            return value;\n        case \"boolean\":\n            return value ? 1 : 0;\n        case \"string\":\n            if (isNumber(value, locale) || value === \"\") {\n                return parseNumber(value, locale);\n            }\n            const internalDate = parseDateTime(value, locale);\n            if (internalDate) {\n                return internalDate.value;\n            }\n            throw new EvaluationError(expectNumberValueError(value));\n        default:\n            return 0;\n    }\n}\nfunction tryToNumber(value, locale) {\n    try {\n        return toNumber(value, locale);\n    }\n    catch (e) {\n        return undefined;\n    }\n}\nfunction toNumberMatrix(data, argName) {\n    return toMatrix(data).map((row) => {\n        return row.map((cell) => {\n            if (typeof cell.value !== \"number\") {\n                throw new EvaluationError(_t(\"Function [[FUNCTION_NAME]] expects number values for %s, but got a %s.\", argName, typeof cell.value));\n            }\n            return cell.value;\n        });\n    });\n}\nfunction strictToNumber(data, locale) {\n    const value = toValue(data);\n    if (value === \"\") {\n        throw new EvaluationError(expectNumberValueError(value));\n    }\n    return toNumber(value, locale);\n}\nfunction toInteger(value, locale) {\n    return Math.trunc(toNumber(value, locale));\n}\nfunction strictToInteger(value, locale) {\n    return Math.trunc(strictToNumber(value, locale));\n}\nfunction assertNumberGreaterThanOrEqualToOne(value) {\n    assert(() => value >= 1, _t(\"The function [[FUNCTION_NAME]] expects a number value to be greater than or equal to 1, but receives %s.\", value.toString()));\n}\nfunction assertNotZero(value) {\n    assert(() => value !== 0, _t(\"Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.\"), CellErrorType.DivisionByZero);\n}\nfunction toString(data) {\n    const value = toValue(data);\n    switch (typeof value) {\n        case \"string\":\n            return value;\n        case \"number\":\n            return value.toString();\n        case \"boolean\":\n            return value ? \"TRUE\" : \"FALSE\";\n        default:\n            return \"\";\n    }\n}\n/** Normalize string by setting it to lowercase and replacing accent letters with plain letters */\nconst normalizeString = memoize(function normalizeString(str) {\n    return str\n        .toLowerCase()\n        .normalize(\"NFD\")\n        .replace(/[\\u0300-\\u036f]/g, \"\");\n});\nconst expectBooleanValueError = (value) => _t(\"The function [[FUNCTION_NAME]] expects a boolean value, but '%s' is a text, and cannot be coerced to a boolean.\", value);\nfunction toBoolean(data) {\n    const value = toValue(data);\n    switch (typeof value) {\n        case \"boolean\":\n            return value;\n        case \"string\":\n            if (value) {\n                let uppercaseVal = value.toUpperCase();\n                if (uppercaseVal === \"TRUE\") {\n                    return true;\n                }\n                if (uppercaseVal === \"FALSE\") {\n                    return false;\n                }\n                throw new EvaluationError(expectBooleanValueError(value));\n            }\n            else {\n                return false;\n            }\n        case \"number\":\n            return value ? true : false;\n        default:\n            return false;\n    }\n}\nfunction strictToBoolean(data) {\n    const value = toValue(data);\n    if (value === \"\") {\n        throw new EvaluationError(expectBooleanValueError(value));\n    }\n    return toBoolean(value);\n}\nfunction toJsDate(data, locale) {\n    const value = toValue(data);\n    return numberToJsDate(toNumber(value, locale));\n}\nfunction toValue(data) {\n    if (typeof data === \"object\" && data !== null && \"value\" in data) {\n        if (isEvaluationError(data.value)) {\n            throw data;\n        }\n        return data.value;\n    }\n    if (isEvaluationError(data)) {\n        throw new EvaluationError(\"\", data);\n    }\n    return data;\n}\n// -----------------------------------------------------------------------------\n// VISIT FUNCTIONS\n// -----------------------------------------------------------------------------\nfunction visitArgs(args, cellCb, dataCb) {\n    for (let arg of args) {\n        if (isMatrix(arg)) {\n            // arg is ref to a Cell/Range\n            const lenRow = arg.length;\n            const lenCol = arg[0].length;\n            for (let y = 0; y < lenCol; y++) {\n                for (let x = 0; x < lenRow; x++) {\n                    cellCb(arg[x][y]);\n                }\n            }\n        }\n        else {\n            // arg is set directly in the formula function\n            dataCb(arg);\n        }\n    }\n}\nfunction visitAny(args, cb) {\n    visitArgs(args, (cell) => {\n        if (isEvaluationError(cell.value)) {\n            throw cell;\n        }\n        cb(cell);\n    }, (arg) => {\n        if (isEvaluationError(arg?.value)) {\n            throw arg;\n        }\n        cb(arg);\n    });\n}\nfunction visitNumbers(args, cb, locale) {\n    visitArgs(args, (cell) => {\n        if (typeof cell?.value === \"number\") {\n            cb(cell);\n        }\n        if (isEvaluationError(cell?.value)) {\n            throw cell;\n        }\n    }, (arg) => {\n        cb({ value: strictToNumber(arg, locale), format: arg?.format });\n    });\n}\n// -----------------------------------------------------------------------------\n// REDUCE FUNCTIONS\n// -----------------------------------------------------------------------------\nfunction reduceArgs(args, cellCb, dataCb, initialValue, dir = \"rowFirst\") {\n    let val = initialValue;\n    for (let arg of args) {\n        if (isMatrix(arg)) {\n            // arg is ref to a Cell/Range\n            const numberOfCols = arg.length;\n            const numberOfRows = arg[0].length;\n            if (dir === \"rowFirst\") {\n                for (let row = 0; row < numberOfRows; row++) {\n                    for (let col = 0; col < numberOfCols; col++) {\n                        val = cellCb(val, arg[col][row]);\n                    }\n                }\n            }\n            else {\n                for (let col = 0; col < numberOfCols; col++) {\n                    for (let row = 0; row < numberOfRows; row++) {\n                        val = cellCb(val, arg[col][row]);\n                    }\n                }\n            }\n        }\n        else {\n            // arg is set directly in the formula function\n            val = dataCb(val, arg);\n        }\n    }\n    return val;\n}\nfunction reduceAny(args, cb, initialValue, dir = \"rowFirst\") {\n    return reduceArgs(args, cb, cb, initialValue, dir);\n}\nfunction reduceNumbers(args, cb, initialValue, locale) {\n    return reduceArgs(args, (acc, arg) => {\n        const argValue = arg?.value;\n        if (typeof argValue === \"number\") {\n            return cb(acc, argValue);\n        }\n        else if (isEvaluationError(argValue)) {\n            throw arg;\n        }\n        return acc;\n    }, (acc, arg) => {\n        return cb(acc, strictToNumber(arg, locale));\n    }, initialValue);\n}\nfunction reduceNumbersTextAs0(args, cb, initialValue, locale) {\n    return reduceArgs(args, (acc, arg) => {\n        const argValue = arg?.value;\n        if (argValue !== undefined && argValue !== null) {\n            if (typeof argValue === \"number\") {\n                return cb(acc, argValue);\n            }\n            else if (typeof argValue === \"boolean\") {\n                return cb(acc, toNumber(argValue, locale));\n            }\n            else if (isEvaluationError(argValue)) {\n                throw arg;\n            }\n            else {\n                return cb(acc, 0);\n            }\n        }\n        return acc;\n    }, (acc, arg) => {\n        return cb(acc, toNumber(arg, locale));\n    }, initialValue);\n}\n// -----------------------------------------------------------------------------\n// MATRIX FUNCTIONS\n// -----------------------------------------------------------------------------\n/**\n * Generate a matrix of size nColumns x nRows and apply a callback on each position\n */\nfunction generateMatrix(nColumns, nRows, callback) {\n    const returned = Array(nColumns);\n    for (let col = 0; col < nColumns; col++) {\n        returned[col] = Array(nRows);\n        for (let row = 0; row < nRows; row++) {\n            returned[col][row] = callback(col, row);\n        }\n    }\n    return returned;\n}\nfunction matrixMap(matrix, callback) {\n    if (matrix.length === 0) {\n        return [];\n    }\n    return generateMatrix(matrix.length, matrix[0].length, (col, row) => callback(matrix[col][row]));\n}\nfunction matrixForEach(matrix, fn) {\n    const numberOfCols = matrix.length;\n    const numberOfRows = matrix[0]?.length ?? 0;\n    for (let col = 0; col < numberOfCols; col++) {\n        for (let row = 0; row < numberOfRows; row++) {\n            fn(matrix[col][row]);\n        }\n    }\n}\nfunction transposeMatrix(matrix) {\n    if (!matrix.length) {\n        return [];\n    }\n    return generateMatrix(matrix[0].length, matrix.length, (i, j) => matrix[j][i]);\n}\n// -----------------------------------------------------------------------------\n// CONDITIONAL EXPLORE FUNCTIONS\n// -----------------------------------------------------------------------------\n/**\n * This function allows to visit arguments and stop the visit if necessary.\n * It is mainly used to bypass argument evaluation for functions like OR or AND.\n */\nfunction conditionalVisitArgs(args, cellCb, dataCb) {\n    for (let arg of args) {\n        if (isMatrix(arg)) {\n            // arg is ref to a Cell/Range\n            const lenRow = arg.length;\n            const lenCol = arg[0].length;\n            for (let y = 0; y < lenCol; y++) {\n                for (let x = 0; x < lenRow; x++) {\n                    if (!cellCb(arg[x][y] ?? undefined))\n                        return;\n                }\n            }\n        }\n        else {\n            // arg is set directly in the formula function\n            if (!dataCb(arg))\n                return;\n        }\n    }\n}\nfunction conditionalVisitBoolean(args, cb) {\n    return conditionalVisitArgs(args, (arg) => {\n        const argValue = arg?.value;\n        if (typeof argValue === \"boolean\") {\n            return cb(argValue);\n        }\n        if (typeof argValue === \"number\") {\n            return cb(argValue ? true : false);\n        }\n        if (isEvaluationError(argValue)) {\n            throw arg;\n        }\n        return true;\n    }, (arg) => {\n        if (arg !== undefined && arg.value !== null) {\n            return cb(strictToBoolean(arg));\n        }\n        return true;\n    });\n}\nfunction getPredicate(descr, locale) {\n    let operator;\n    let operand;\n    let subString = descr.substring(0, 2);\n    if (subString === \"<=\" || subString === \">=\" || subString === \"<>\") {\n        operator = subString;\n        operand = descr.substring(2);\n    }\n    else {\n        subString = descr.substring(0, 1);\n        if (subString === \"<\" || subString === \">\" || subString === \"=\") {\n            operator = subString;\n            operand = descr.substring(1);\n        }\n        else {\n            operator = \"=\";\n            operand = descr;\n        }\n    }\n    if (isNumber(operand, locale) || isDateTime(operand, locale)) {\n        operand = toNumber(operand, locale);\n    }\n    else if (operand === \"TRUE\" || operand === \"FALSE\") {\n        operand = toBoolean(operand);\n    }\n    return { operator, operand };\n}\n/**\n * Converts a search string containing wildcard characters to a regular expression.\n *\n * The function iterates over each character in the input string. If the character is a wildcard\n * character (\"?\" or \"*\") and is not preceded by a \"~\", it is replaced by the corresponding regular\n * expression.\n * If the character is a special regular expression character, it is escaped with \"\\\\\".\n */\nconst wildcardToRegExp = memoize(function wildcardToRegExp(operand) {\n    if (operand === \"*\") {\n        return /.+/;\n    }\n    let exp = \"\";\n    let predecessor = \"\";\n    for (let char of operand) {\n        if (char === \"?\" && predecessor !== \"~\") {\n            exp += \".\";\n        }\n        else if (char === \"*\" && predecessor !== \"~\") {\n            exp += \".*\";\n        }\n        else {\n            if (char === \"*\" || char === \"?\") {\n                //remove \"~\"\n                exp = exp.slice(0, -1);\n            }\n            if ([\"^\", \".\", \"[\", \"]\", \"$\", \"(\", \")\", \"*\", \"+\", \"?\", \"|\", \"{\", \"}\", \"\\\\\"].includes(char)) {\n                exp += \"\\\\\";\n            }\n            exp += char;\n        }\n        predecessor = char;\n    }\n    return new RegExp(\"^\" + exp + \"$\", \"i\");\n});\nfunction evaluatePredicate(value = \"\", criterion, locale) {\n    const { operator, operand } = criterion;\n    if (operand === undefined || value === null || operand === null) {\n        return false;\n    }\n    if (typeof operand === \"number\" && operator === \"=\") {\n        if (typeof value === \"string\" && (isNumber(value, locale) || isDateTime(value, locale))) {\n            return toNumber(value, locale) === operand;\n        }\n        return value === operand;\n    }\n    if (operator === \"<>\" || operator === \"=\") {\n        let result;\n        if (typeof value === typeof operand) {\n            if (typeof value === \"string\" && typeof operand === \"string\") {\n                result = wildcardToRegExp(operand).test(value);\n            }\n            else {\n                result = value === operand;\n            }\n        }\n        else {\n            result = false;\n        }\n        return operator === \"=\" ? result : !result;\n    }\n    if (typeof value === typeof operand) {\n        switch (operator) {\n            case \"<\":\n                return value < operand;\n            case \">\":\n                return value > operand;\n            case \"<=\":\n                return value <= operand;\n            case \">=\":\n                return value >= operand;\n        }\n    }\n    return false;\n}\n/**\n * Functions used especially for predicate evaluation on ranges.\n *\n * Take ranges with same dimensions and take predicates, one for each range.\n * For (i, j) coordinates, if all elements with coordinates (i, j) of each\n * range correspond to the associated predicate, then the function uses a callback\n * function with the parameters \"i\" and \"j\".\n *\n * Syntax:\n * visitMatchingRanges([range1, predicate1, range2, predicate2, ...], cb(i,j), likeSelection)\n *\n * - range1 (range): The range to check against predicate1.\n * - predicate1 (string): The pattern or test to apply to range1.\n * - range2: (range, repeatable) ranges to check.\n * - predicate2 (string, repeatable): Additional pattern or test to apply to range2.\n *\n * - cb(i: number, j: number) => void: the callback function.\n *\n * - isQuery (boolean) indicates if the comparison with a string should be done as a SQL-like query.\n * (Ex1 isQuery = true, predicate = \"abc\", element = \"abcde\": predicate match the element),\n * (Ex2 isQuery = false, predicate = \"abc\", element = \"abcde\": predicate not match the element).\n * (Ex3 isQuery = true, predicate = \"abc\", element = \"abc\": predicate match the element),\n * (Ex4 isQuery = false, predicate = \"abc\", element = \"abc\": predicate match the element).\n */\nfunction visitMatchingRanges(args, cb, locale, isQuery = false) {\n    const countArg = args.length;\n    if (countArg % 2 === 1) {\n        throw new EvaluationError(_t(\"Function [[FUNCTION_NAME]] expects criteria_range and criterion to be in pairs.\"));\n    }\n    const firstArg = toMatrix(args[0]);\n    const dimRow = firstArg.length;\n    const dimCol = firstArg[0].length;\n    let predicates = [];\n    for (let i = 0; i < countArg - 1; i += 2) {\n        const criteriaRange = toMatrix(args[i]);\n        if (criteriaRange.length !== dimRow || criteriaRange[0].length !== dimCol) {\n            throw new EvaluationError(_t(\"Function [[FUNCTION_NAME]] expects criteria_range to have the same dimension\"));\n        }\n        const description = toString(args[i + 1]);\n        const predicate = getPredicate(description, locale);\n        if (isQuery && typeof predicate.operand === \"string\") {\n            predicate.operand += \"*\";\n        }\n        predicates.push(predicate);\n    }\n    for (let i = 0; i < dimRow; i++) {\n        for (let j = 0; j < dimCol; j++) {\n            let validatedPredicates = true;\n            for (let k = 0; k < countArg - 1; k += 2) {\n                const criteriaValue = toMatrix(args[k])[i][j].value;\n                const criterion = predicates[k / 2];\n                validatedPredicates = evaluatePredicate(criteriaValue ?? undefined, criterion, locale);\n                if (!validatedPredicates) {\n                    break;\n                }\n            }\n            if (validatedPredicates) {\n                cb(i, j);\n            }\n        }\n    }\n}\n// -----------------------------------------------------------------------------\n// COMMON FUNCTIONS\n// -----------------------------------------------------------------------------\n/**\n * Perform a dichotomic search on an array and return the index of the nearest match.\n *\n * The array should be sorted, if not an incorrect value might be returned. In the case where multiple\n * element of the array match the target, the method will return the first match if the array is sorted\n * in descending order, and the last match if the array is in ascending order.\n *\n *\n * @param data the array in which to search.\n * @param target the value to search.\n * @param mode \"nextGreater/nextSmaller\" : return next greater/smaller value if no exact match is found.\n * @param sortOrder whether the array is sorted in ascending or descending order.\n * @param rangeLength the number of elements to consider in the search array.\n * @param getValueInData function returning the element at index i in the search array.\n */\nfunction dichotomicSearch(data, target, mode, sortOrder, rangeLength, getValueInData) {\n    if (target === undefined || target.value === null) {\n        return -1;\n    }\n    if (isEvaluationError(target.value)) {\n        throw target;\n    }\n    const _target = normalizeValue(target.value);\n    const targetType = typeof _target;\n    let matchVal = undefined;\n    let matchValIndex = undefined;\n    let indexLeft = 0;\n    let indexRight = rangeLength - 1;\n    let indexMedian;\n    let currentIndex;\n    let currentVal;\n    let currentType;\n    while (indexRight - indexLeft >= 0) {\n        indexMedian = Math.floor((indexLeft + indexRight) / 2);\n        currentIndex = indexMedian;\n        currentVal = normalizeValue(getValueInData(data, currentIndex));\n        currentType = typeof currentVal;\n        // 1 - linear search to find value with the same type\n        while (indexLeft < currentIndex && targetType !== currentType) {\n            currentIndex--;\n            currentVal = normalizeValue(getValueInData(data, currentIndex));\n            currentType = typeof currentVal;\n        }\n        if (currentType !== targetType || currentVal === undefined || currentVal === null) {\n            indexLeft = indexMedian + 1;\n            continue;\n        }\n        // 2 - check if value match\n        if (mode === \"strict\" && currentVal === _target) {\n            matchVal = currentVal;\n            matchValIndex = currentIndex;\n        }\n        else if (mode === \"nextSmaller\" && currentVal <= _target) {\n            if (matchVal === undefined ||\n                matchVal === null ||\n                matchVal < currentVal ||\n                (matchVal === currentVal && sortOrder === \"asc\" && matchValIndex < currentIndex) ||\n                (matchVal === currentVal && sortOrder === \"desc\" && matchValIndex > currentIndex)) {\n                matchVal = currentVal;\n                matchValIndex = currentIndex;\n            }\n        }\n        else if (mode === \"nextGreater\" && currentVal >= _target) {\n            if (matchVal === undefined ||\n                matchVal > currentVal ||\n                (matchVal === currentVal && sortOrder === \"asc\" && matchValIndex < currentIndex) ||\n                (matchVal === currentVal && sortOrder === \"desc\" && matchValIndex > currentIndex)) {\n                matchVal = currentVal;\n                matchValIndex = currentIndex;\n            }\n        }\n        // 3 - give new indexes for the Binary search\n        if ((sortOrder === \"asc\" && currentVal > _target) ||\n            (sortOrder === \"desc\" && currentVal <= _target)) {\n            indexRight = currentIndex - 1;\n        }\n        else {\n            indexLeft = indexMedian + 1;\n        }\n    }\n    // note that valMinIndex could be 0\n    return matchValIndex !== undefined ? matchValIndex : -1;\n}\n/**\n * Perform a linear search and return the index of the match.\n * -1 is returned if no value is found.\n *\n * Example:\n * - [3, 6, 10], 3 => 0\n * - [3, 6, 10], 6 => 1\n * - [3, 6, 10], 9 => -1\n * - [3, 6, 10], 2 => -1\n *\n * @param data the array to search in.\n * @param target the value to search in the array.\n * @param mode if \"strict\" return exact match index. \"nextGreater\" returns the next greater\n * element from the target and \"nextSmaller\" the next smaller\n * @param numberOfValues the number of elements to consider in the search array.\n * @param getValueInData function returning the element at index i in the search array.\n * @param reverseSearch if true, search in the array starting from the end.\n\n */\nfunction linearSearch(data, target, mode, numberOfValues, getValueInData, reverseSearch = false) {\n    if (target === undefined || target.value === null) {\n        return -1;\n    }\n    if (isEvaluationError(target.value)) {\n        throw target;\n    }\n    const _target = normalizeValue(target.value);\n    const getValue = reverseSearch\n        ? (data, i) => getValueInData(data, numberOfValues - i - 1)\n        : getValueInData;\n    let indexMatchTarget = (i) => {\n        return normalizeValue(getValue(data, i)) === _target;\n    };\n    if (mode === \"wildcard\" &&\n        typeof _target === \"string\" &&\n        (_target.includes(\"*\") || _target.includes(\"?\"))) {\n        const regExp = wildcardToRegExp(_target);\n        indexMatchTarget = (i) => {\n            const value = normalizeValue(getValue(data, i));\n            if (typeof value === \"string\") {\n                return regExp.test(value);\n            }\n            return false;\n        };\n    }\n    let closestMatch = undefined;\n    let closestMatchIndex = -1;\n    if (mode === \"nextSmaller\") {\n        indexMatchTarget = (i) => {\n            const value = normalizeValue(getValue(data, i));\n            if ((!closestMatch && compareCellValues(_target, value) >= 0) ||\n                (compareCellValues(_target, value) >= 0 && compareCellValues(value, closestMatch) > 0)) {\n                closestMatch = value;\n                closestMatchIndex = i;\n            }\n            return value === _target;\n        };\n    }\n    if (mode === \"nextGreater\") {\n        indexMatchTarget = (i) => {\n            const value = normalizeValue(getValue(data, i));\n            if ((!closestMatch && compareCellValues(_target, value) <= 0) ||\n                (compareCellValues(_target, value) <= 0 && compareCellValues(value, closestMatch) < 0)) {\n                closestMatch = value;\n                closestMatchIndex = i;\n            }\n            return value === _target;\n        };\n    }\n    for (let i = 0; i < numberOfValues; i++) {\n        if (indexMatchTarget(i)) {\n            return reverseSearch ? numberOfValues - i - 1 : i;\n        }\n    }\n    return reverseSearch && closestMatchIndex !== -1\n        ? numberOfValues - closestMatchIndex - 1\n        : closestMatchIndex;\n}\n/**\n * Normalize a value.\n * If the cell value is a string, this will set it to lowercase and replacing accent letters with plain letters\n */\nfunction normalizeValue(value) {\n    return typeof value === \"string\" ? normalizeString(value) : value;\n}\nfunction compareCellValues(left, right) {\n    let typeOrder = SORT_TYPES_ORDER.indexOf(typeof left) - SORT_TYPES_ORDER.indexOf(typeof right);\n    if (typeOrder === 0) {\n        if (typeof left === \"string\" && typeof right === \"string\") {\n            typeOrder = left.localeCompare(right);\n        }\n        else if (typeof left === \"number\" && typeof right === \"number\") {\n            typeOrder = left - right;\n        }\n        else if (typeof left === \"boolean\" && typeof right === \"boolean\") {\n            typeOrder = Number(left) - Number(right);\n        }\n    }\n    return typeOrder;\n}\nfunction toMatrix(data) {\n    if (data === undefined) {\n        return [[]];\n    }\n    return isMatrix(data) ? data : [[data]];\n}\n/**\n * Flatten an array of items, where each item can be a single value or a 2D array, and apply the\n * callback to each element.\n *\n * The 2D array are flattened row first.\n */\nfunction flattenRowFirst(items, callback) {\n    /**/\n    return reduceAny(items, (array, val) => {\n        array.push(callback(val));\n        return array;\n    }, [], \"rowFirst\");\n}\nfunction isDataNonEmpty(data) {\n    if (data === undefined) {\n        return false;\n    }\n    const { value } = data;\n    if (value === null || value === \"\") {\n        return false;\n    }\n    return true;\n}\n\nfunction toCriterionDateNumber(dateValue) {\n    const today = DateTime.now();\n    switch (dateValue) {\n        case \"today\":\n            return jsDateToNumber(today);\n        case \"yesterday\":\n            return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 1)));\n        case \"tomorrow\":\n            return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() + 1)));\n        case \"lastWeek\":\n            return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 7)));\n        case \"lastMonth\":\n            return jsDateToNumber(DateTime.fromTimestamp(today.setMonth(today.getMonth() - 1)));\n        case \"lastYear\":\n            return jsDateToNumber(DateTime.fromTimestamp(today.setFullYear(today.getFullYear() - 1)));\n    }\n}\n/** Get all the dates values of a criterion converted to numbers, converting date values such as \"today\" to actual dates  */\nfunction getDateNumberCriterionValues(criterion, locale) {\n    if (\"dateValue\" in criterion && criterion.dateValue !== \"exactDate\") {\n        return [toCriterionDateNumber(criterion.dateValue)];\n    }\n    return criterion.values.map((value) => valueToDateNumber(value, locale));\n}\n/** Convert the criterion values to numbers. Return undefined values if they cannot be converted to numbers. */\nfunction getCriterionValuesAsNumber(criterion, locale) {\n    return criterion.values.map((value) => tryToNumber(value, locale));\n}\n\nconst MAX_DELAY = 140;\nconst MIN_DELAY = 20;\nconst ACCELERATION = 0.035;\n/**\n * Decreasing exponential function used to determine the \"speed\" of edge-scrolling\n * as the timeout delay.\n *\n * Returns a timeout delay in milliseconds.\n */\nfunction scrollDelay(value) {\n    // decreasing exponential from MAX_DELAY to MIN_DELAY\n    return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1));\n}\n\nfunction tokenizeFormat(str) {\n    const chars = new TokenizingChars(str);\n    const result = [];\n    let currentFormatPart = [];\n    result.push(currentFormatPart);\n    while (!chars.isOver()) {\n        if (chars.current === \";\") {\n            currentFormatPart = [];\n            result.push(currentFormatPart);\n            chars.shift();\n            continue;\n        }\n        let token = tokenizeDigit(chars) ||\n            tokenizeString$1(chars) ||\n            tokenizeEscapedChars(chars) ||\n            tokenizeThousandsSeparator(chars) ||\n            tokenizeDecimalPoint(chars) ||\n            tokenizePercent(chars) ||\n            tokenizeDatePart(chars) ||\n            tokenizeTextPlaceholder(chars) ||\n            tokenizeRepeatedChar(chars);\n        if (!token) {\n            throw new Error(\"Unknown token at \" + chars.remaining());\n        }\n        currentFormatPart.push(token);\n    }\n    return result;\n}\nfunction tokenizeString$1(chars) {\n    let enfOfStringChar;\n    if (chars.current === '\"') {\n        chars.shift();\n        enfOfStringChar = '\"';\n    }\n    else if (chars.currentStartsWith(\"[$\")) {\n        chars.advanceBy(2);\n        enfOfStringChar = \"]\";\n    }\n    if (!enfOfStringChar) {\n        return null;\n    }\n    let letters = \"\";\n    while (chars.current && chars.current !== enfOfStringChar) {\n        letters += chars.shift();\n    }\n    if (chars.current === enfOfStringChar) {\n        chars.shift();\n    }\n    else {\n        throw new Error(\"Unterminated string in format\");\n    }\n    return {\n        type: \"STRING\",\n        value: letters,\n    };\n}\nconst alwaysEscapedCharsInFormat = new Set(\"$+-/():!^&~{}<>= \");\nfunction tokenizeEscapedChars(chars) {\n    if (chars.current === \"\\\\\") {\n        chars.shift();\n        const escapedChar = chars.shift();\n        if (!escapedChar) {\n            throw new Error(\"Unexpected end of format string\");\n        }\n        return {\n            type: \"CHAR\",\n            value: escapedChar,\n        };\n    }\n    if (alwaysEscapedCharsInFormat.has(chars.current)) {\n        return {\n            type: \"CHAR\",\n            value: chars.shift(),\n        };\n    }\n    return null;\n}\nfunction tokenizeThousandsSeparator(chars) {\n    if (chars.current === \",\") {\n        chars.shift();\n        return { type: \"THOUSANDS_SEPARATOR\", value: \",\" };\n    }\n    return null;\n}\nfunction tokenizeTextPlaceholder(chars) {\n    if (chars.current === \"@\") {\n        chars.shift();\n        return { type: \"TEXT_PLACEHOLDER\", value: \"@\" };\n    }\n    return null;\n}\nfunction tokenizeDecimalPoint(chars) {\n    if (chars.current === \".\") {\n        chars.shift();\n        return { type: \"DECIMAL_POINT\", value: \".\" };\n    }\n    return null;\n}\nfunction tokenizePercent(chars) {\n    if (chars.current === \"%\") {\n        chars.shift();\n        return { type: \"PERCENT\", value: \"%\" };\n    }\n    return null;\n}\nfunction tokenizeDigit(chars) {\n    if (chars.current === \"0\" || chars.current === \"#\") {\n        const value = chars.current;\n        chars.shift();\n        return { type: \"DIGIT\", value };\n    }\n    return null;\n}\nconst dateSymbols = new Set(\"dmqyhsa\");\nfunction tokenizeDatePart(chars) {\n    if (!dateSymbols.has(chars.current)) {\n        return null;\n    }\n    const char = chars.current;\n    let value = \"\";\n    while (chars.current === char) {\n        value += chars.shift();\n    }\n    return { type: \"DATE_PART\", value };\n}\nfunction tokenizeRepeatedChar(chars) {\n    if (chars.current !== \"*\") {\n        return null;\n    }\n    chars.shift();\n    const repeatedChar = chars.shift();\n    if (!repeatedChar) {\n        throw new Error(\"Unexpected end of format string\");\n    }\n    return {\n        type: \"REPEATED_CHAR\",\n        value: repeatedChar,\n    };\n}\n\n/**\n *  Constant used to indicate the maximum of digits that is possible to display\n *  in a cell with standard size.\n */\nconst MAX_DECIMAL_PLACES = 20;\nconst internalFormatCache = {};\nfunction parseFormat(formatString) {\n    let internalFormat = internalFormatCache[formatString];\n    if (internalFormat === undefined) {\n        internalFormat = convertFormatToInternalFormat(formatString);\n        internalFormatCache[formatString] = internalFormat;\n    }\n    return internalFormat;\n}\nfunction convertFormatToInternalFormat(format) {\n    const formatParts = tokenizeFormat(format);\n    // A format can only have a single REPEATED_CHAR token. The rest are converted to simple CHAR tokens.\n    for (const part of formatParts) {\n        const repeatedCharTokens = part.filter((token) => token.type === \"REPEATED_CHAR\");\n        for (const repeatedCharToken of repeatedCharTokens.slice(1)) {\n            repeatedCharToken.type = \"CHAR\";\n        }\n    }\n    const positiveFormat = parseDateFormatTokens(formatParts[0]) ||\n        parseNumberFormatTokens(formatParts[0]) ||\n        tokensToTextInternalFormat(formatParts[0]);\n    if (!positiveFormat) {\n        throw new Error(\"Invalid first format part of: \" + format);\n    }\n    if (formatParts.length > 1 && positiveFormat.type === \"text\") {\n        throw new Error(\"The first format in a multi-part format must be a number format: \" + format);\n    }\n    const negativeFormat = parseDateFormatTokens(formatParts[1]) || parseNumberFormatTokens(formatParts[1]);\n    if (formatParts[1]?.length && !negativeFormat) {\n        throw new Error(\"Invalid second format part of: \" + format);\n    }\n    const zeroFormat = parseDateFormatTokens(formatParts[2]) || parseNumberFormatTokens(formatParts[2]);\n    if (formatParts[2]?.length && !zeroFormat) {\n        throw new Error(\"Invalid third format part of: \" + format);\n    }\n    const textFormat = tokensToTextInternalFormat(formatParts[3]);\n    if (formatParts[3]?.length && !textFormat) {\n        throw new Error(\"Invalid fourth format part of: \" + format);\n    }\n    return { positive: positiveFormat, negative: negativeFormat, zero: zeroFormat, text: textFormat };\n}\nfunction areValidDateFormatTokens(tokens) {\n    return tokens.every((token) => token.type === \"DATE_PART\" ||\n        token.type === \"DECIMAL_POINT\" ||\n        token.type === \"THOUSANDS_SEPARATOR\" ||\n        token.type === \"STRING\" ||\n        token.type === \"CHAR\" ||\n        token.type === \"REPEATED_CHAR\");\n}\nfunction areValidNumberFormatTokens(tokens) {\n    return tokens.every((token) => token.type === \"DIGIT\" ||\n        token.type === \"DECIMAL_POINT\" ||\n        token.type === \"THOUSANDS_SEPARATOR\" ||\n        token.type === \"PERCENT\" ||\n        token.type === \"STRING\" ||\n        token.type === \"CHAR\" ||\n        token.type === \"REPEATED_CHAR\");\n}\nfunction areValidTextFormatTokens(tokens) {\n    return tokens.every((token) => token.type === \"STRING\" ||\n        token.type === \"TEXT_PLACEHOLDER\" ||\n        token.type === \"CHAR\" ||\n        token.type === \"REPEATED_CHAR\");\n}\nfunction parseNumberFormatTokens(tokens) {\n    if (!tokens || !areValidNumberFormatTokens(tokens)) {\n        return undefined;\n    }\n    const integerPart = [];\n    let decimalPart = undefined;\n    let parsedPart = integerPart;\n    let percentSymbols = 0;\n    let magnitude = 0;\n    let lastIndexOfDigit = tokens.findLastIndex((token) => token.type === \"DIGIT\");\n    let hasThousandSeparator = false;\n    let numberOfDecimalsDigits = 0;\n    for (let i = 0; i < tokens.length; i++) {\n        const token = tokens[i];\n        switch (token.type) {\n            case \"DIGIT\":\n                if (parsedPart === integerPart) {\n                    parsedPart.push(token);\n                }\n                else if (numberOfDecimalsDigits < MAX_DECIMAL_PLACES) {\n                    parsedPart.push(token);\n                    numberOfDecimalsDigits++;\n                }\n                break;\n            case \"DECIMAL_POINT\":\n                if (parsedPart === integerPart) {\n                    decimalPart = [];\n                    parsedPart = decimalPart;\n                }\n                else {\n                    throw new Error(\"Multiple decimal points in a number format\");\n                }\n                break;\n            case \"REPEATED_CHAR\":\n            case \"CHAR\":\n            case \"STRING\":\n                parsedPart.push(token);\n                break;\n            case \"PERCENT\":\n                percentSymbols++;\n                parsedPart.push(token);\n                break;\n            // Per OpenXML Spec:\n            // - If a comma is between two DIGIT tokens, and in the integer part, a thousand separator is applied in the formatted value.\n            // - If a comma is at the end of the number placeholder, the number is divided by a thousand.\n            // - Otherwise, it's a string.\n            case \"THOUSANDS_SEPARATOR\":\n                if (i - 1 === lastIndexOfDigit) {\n                    magnitude += 1;\n                    lastIndexOfDigit++; // Can have multiple commas in a row\n                    parsedPart.push(token);\n                }\n                else if (tokens[i + 1]?.type === \"DIGIT\" && tokens[i - 1]?.type === \"DIGIT\") {\n                    if (parsedPart === integerPart) {\n                        hasThousandSeparator = true;\n                    }\n                    parsedPart.push(token);\n                }\n                else {\n                    parsedPart.push({ type: \"CHAR\", value: \",\" });\n                }\n                break;\n        }\n    }\n    return {\n        type: \"number\",\n        integerPart,\n        decimalPart,\n        percentSymbols,\n        thousandsSeparator: hasThousandSeparator,\n        magnitude,\n    };\n}\nfunction parseDateFormatTokens(tokens) {\n    const internalFormat = tokens && areValidDateFormatTokens(tokens) ? { type: \"date\", tokens } : undefined;\n    if (!internalFormat) {\n        return undefined;\n    }\n    if (internalFormat.tokens.length &&\n        internalFormat.tokens.every((token) => token.type === \"DATE_PART\" && token.value === \"a\")) {\n        throw new Error(\"Invalid date format\");\n    }\n    const dateTokens = internalFormat.tokens.map((token) => {\n        if (token.type === \"THOUSANDS_SEPARATOR\" || token.type === \"DECIMAL_POINT\") {\n            return { type: \"CHAR\", value: token.value };\n        }\n        return token;\n    });\n    const convertedTokens = convertTokensToMinutesInDateFormat(dateTokens);\n    return { type: \"date\", tokens: convertedTokens };\n}\nfunction tokensToTextInternalFormat(tokens) {\n    return tokens && areValidTextFormatTokens(tokens) ? { type: \"text\", tokens } : undefined;\n}\n/**\n * Replace in place tokens \"mm\" and \"m\" that denote minutes in date format with \"MM\" to avoid confusion with months.\n *\n * As per OpenXML specification, in date formats if a date token \"m\" or \"mm\" is followed by a date token \"s\" or\n * preceded by a data token \"h\", then it's not a month but an minute.\n */\nfunction convertTokensToMinutesInDateFormat(tokens) {\n    const dateParts = tokens.filter((token) => token.type === \"DATE_PART\");\n    for (let i = 0; i < dateParts.length; i++) {\n        if (!dateParts[i].value.startsWith(\"m\") || dateParts[i].value.length > 2) {\n            continue;\n        }\n        if (dateParts[i - 1]?.value.startsWith(\"h\") || dateParts[i + 1]?.value.startsWith(\"s\")) {\n            dateParts[i].value = dateParts[i].value.replaceAll(\"m\", \"M\");\n        }\n    }\n    return tokens;\n}\nfunction convertInternalFormatToFormat(internalFormat) {\n    return [\n        internalFormatPartToFormat(internalFormat.positive),\n        internalFormatPartToFormat(internalFormat.negative),\n        internalFormatPartToFormat(internalFormat.zero),\n        internalFormatPartToFormat(internalFormat.text),\n    ]\n        .filter(isDefined)\n        .join(\";\");\n}\nfunction internalFormatPartToFormat(internalFormat) {\n    if (!internalFormat) {\n        return undefined;\n    }\n    let format = \"\";\n    const tokens = internalFormat.type !== \"number\"\n        ? internalFormat.tokens\n        : numberInternalFormatToTokenList(internalFormat);\n    for (let token of tokens) {\n        switch (token.type) {\n            case \"STRING\":\n                format += `[$${token.value}]`;\n                break;\n            case \"CHAR\":\n                format += shouldEscapeFormatChar(token.value) ? `\\\\${token.value}` : token.value;\n                break;\n            case \"REPEATED_CHAR\":\n                format += \"*\" + token.value;\n                break;\n            default:\n                format += token.value;\n        }\n    }\n    return format;\n}\nfunction numberInternalFormatToTokenList(internalFormat) {\n    let tokens = [...internalFormat.integerPart];\n    if (internalFormat.decimalPart) {\n        tokens.push({ type: \"DECIMAL_POINT\", value: \".\" });\n        tokens.push(...internalFormat.decimalPart);\n    }\n    return tokens;\n}\nfunction shouldEscapeFormatChar(char) {\n    return !alwaysEscapedCharsInFormat.has(char);\n}\n\n/**\n * Number of digits for the default number format. This number of digit make a number fit well in a cell\n * with default size and default font size.\n */\nconst DEFAULT_FORMAT_NUMBER_OF_DIGITS = 11;\nconst REPEATED_CHAR_PLACEHOLDER = \"REPEATED_CHAR_PLACEHOLDER_\";\n// TODO in the future : remove these constants MONTHS/DAYS, and use a library such as luxon to handle it\n// + possibly handle automatic translation of day/month\nconst MONTHS = {\n    0: _t(\"January\"),\n    1: _t(\"February\"),\n    2: _t(\"March\"),\n    3: _t(\"April\"),\n    4: _t(\"May\"),\n    5: _t(\"June\"),\n    6: _t(\"July\"),\n    7: _t(\"August\"),\n    8: _t(\"September\"),\n    9: _t(\"October\"),\n    10: _t(\"November\"),\n    11: _t(\"December\"),\n};\nconst DAYS$1 = {\n    0: _t(\"Sunday\"),\n    1: _t(\"Monday\"),\n    2: _t(\"Tuesday\"),\n    3: _t(\"Wednesday\"),\n    4: _t(\"Thursday\"),\n    5: _t(\"Friday\"),\n    6: _t(\"Saturday\"),\n};\n/**\n * Formats a cell value with its format.\n */\nfunction formatValue(value, { format, locale, formatWidth }) {\n    if (typeof value === \"boolean\") {\n        value = value ? \"TRUE\" : \"FALSE\";\n    }\n    switch (typeof value) {\n        case \"string\": {\n            if (value.includes('\\\\\"')) {\n                value = value.replaceAll(/\\\\\"/g, '\"');\n            }\n            if (!format) {\n                return value;\n            }\n            const internalFormat = parseFormat(format);\n            let formatToApply = internalFormat.text || internalFormat.positive;\n            if (!formatToApply || formatToApply.type !== \"text\") {\n                return value;\n            }\n            return applyTextInternalFormat(value, formatToApply, formatWidth);\n        }\n        case \"number\":\n            if (!format) {\n                format = createDefaultFormat(value);\n            }\n            const internalFormat = parseFormat(format);\n            if (internalFormat.positive.type === \"text\") {\n                return applyTextInternalFormat(value.toString(), internalFormat.positive, formatWidth);\n            }\n            let formatToApply = internalFormat.positive;\n            if (value < 0 && internalFormat.negative) {\n                formatToApply = internalFormat.negative;\n                value = -value;\n            }\n            else if (value === 0 && internalFormat.zero) {\n                formatToApply = internalFormat.zero;\n            }\n            if (formatToApply.type === \"date\") {\n                return repeatCharToFitWidth(applyDateTimeFormat(value, formatToApply), formatWidth);\n            }\n            const isNegative = value < 0;\n            const formatted = repeatCharToFitWidth(applyInternalNumberFormat(Math.abs(value), formatToApply, locale), formatWidth);\n            return isNegative ? \"-\" + formatted : formatted;\n        case \"object\": // case value === null\n            return \"\";\n    }\n}\nfunction applyTextInternalFormat(value, internalFormat, formatWidth) {\n    let formattedValue = \"\";\n    for (const token of internalFormat.tokens) {\n        switch (token.type) {\n            case \"TEXT_PLACEHOLDER\":\n                formattedValue += value;\n                break;\n            case \"CHAR\":\n            case \"STRING\":\n                formattedValue += token.value;\n                break;\n            case \"REPEATED_CHAR\":\n                formattedValue += REPEATED_CHAR_PLACEHOLDER + token.value;\n                break;\n        }\n    }\n    return repeatCharToFitWidth(formattedValue, formatWidth);\n}\nfunction repeatCharToFitWidth(formattedValue, formatWidth) {\n    const placeholderIndex = formattedValue.indexOf(REPEATED_CHAR_PLACEHOLDER);\n    if (placeholderIndex === -1) {\n        return formattedValue;\n    }\n    const prefix = formattedValue.slice(0, placeholderIndex);\n    const suffix = formattedValue.slice(placeholderIndex + REPEATED_CHAR_PLACEHOLDER.length + 1);\n    const repeatedChar = formattedValue[placeholderIndex + REPEATED_CHAR_PLACEHOLDER.length];\n    function getTimesToRepeat() {\n        if (!formatWidth) {\n            return { timesToRepeat: 0, padding: \"\" };\n        }\n        const widthTaken = formatWidth.measureText(prefix + suffix);\n        const charWidth = formatWidth.measureText(repeatedChar);\n        const availableWidth = formatWidth.availableWidth - widthTaken;\n        if (availableWidth <= 0) {\n            return { timesToRepeat: 0, padding: \"\" };\n        }\n        const timesToRepeat = Math.floor(availableWidth / charWidth);\n        const remainingWidth = availableWidth - timesToRepeat * charWidth;\n        const paddingChar = \"\\u2009\"; // thin space\n        const paddingWidth = formatWidth.measureText(paddingChar);\n        const padding = paddingChar.repeat(Math.floor(remainingWidth / paddingWidth));\n        return { timesToRepeat, padding };\n    }\n    const { timesToRepeat, padding } = getTimesToRepeat();\n    return prefix + repeatedChar.repeat(timesToRepeat) + padding + suffix;\n}\nfunction applyInternalNumberFormat(value, format, locale) {\n    if (value === Infinity) {\n        return \"\u221e\" + (format.percentSymbols ? \"%\" : \"\");\n    }\n    const multiplier = format.percentSymbols * 2 - format.magnitude * 3;\n    value = value * 10 ** multiplier;\n    let maxDecimals = 0;\n    if (format.decimalPart !== undefined) {\n        maxDecimals = format.decimalPart.filter((token) => token.type === \"DIGIT\").length;\n    }\n    const { integerDigits, decimalDigits } = splitNumber(Math.abs(value), maxDecimals);\n    let formattedValue = applyIntegerFormat(integerDigits, format, format.thousandsSeparator ? locale.thousandsSeparator : undefined);\n    if (format.decimalPart !== undefined) {\n        formattedValue += locale.decimalSeparator + applyDecimalFormat(decimalDigits || \"\", format);\n    }\n    return formattedValue;\n}\nfunction applyIntegerFormat(integerDigits, internalFormat, thousandsSeparator) {\n    let tokens = internalFormat.integerPart;\n    if (!tokens.some((token) => token.type === \"DIGIT\")) {\n        tokens = [...tokens, { type: \"DIGIT\", value: \"#\" }];\n    }\n    if (integerDigits === \"0\") {\n        integerDigits = \"\";\n    }\n    let formattedInteger = \"\";\n    const firstDigitIndex = tokens.findIndex((token) => token.type === \"DIGIT\");\n    let indexInIntegerString = integerDigits.length - 1;\n    function appendDigitToFormattedValue(digit, digitType) {\n        if (digitType === \"0\") {\n            digit = digit || \"0\";\n        }\n        if (!digit)\n            return;\n        const digitIndex = integerDigits.length - 1 - indexInIntegerString;\n        if (thousandsSeparator && digitIndex > 0 && digitIndex % 3 === 0) {\n            formattedInteger = digit + thousandsSeparator + formattedInteger;\n        }\n        else {\n            formattedInteger = digit + formattedInteger;\n        }\n    }\n    for (let i = tokens.length - 1; i >= 0; i--) {\n        const token = tokens[i];\n        switch (token.type) {\n            case \"DIGIT\":\n                let digit = integerDigits[indexInIntegerString];\n                appendDigitToFormattedValue(digit, token.value);\n                indexInIntegerString--;\n                // Apply the rest of the integer digits at the first digit character\n                if (firstDigitIndex === i) {\n                    while (indexInIntegerString >= 0) {\n                        appendDigitToFormattedValue(integerDigits[indexInIntegerString], \"0\");\n                        indexInIntegerString--;\n                    }\n                }\n                break;\n            case \"THOUSANDS_SEPARATOR\":\n                break;\n            case \"REPEATED_CHAR\":\n                formattedInteger = REPEATED_CHAR_PLACEHOLDER + token.value + formattedInteger;\n                break;\n            default:\n                formattedInteger = token.value + formattedInteger;\n                break;\n        }\n    }\n    return formattedInteger;\n}\nfunction applyDecimalFormat(decimalDigits, internalFormat) {\n    if (!internalFormat.decimalPart) {\n        return \"\";\n    }\n    let formattedDecimals = \"\";\n    let indexInDecimalString = 0;\n    for (const token of internalFormat.decimalPart) {\n        switch (token.type) {\n            case \"DIGIT\":\n                const digit = token.value === \"#\"\n                    ? decimalDigits[indexInDecimalString] || \"\"\n                    : decimalDigits[indexInDecimalString] || \"0\";\n                formattedDecimals += digit;\n                indexInDecimalString++;\n                break;\n            case \"THOUSANDS_SEPARATOR\":\n                break;\n            case \"REPEATED_CHAR\":\n                formattedDecimals += REPEATED_CHAR_PLACEHOLDER + token.value;\n                break;\n            default:\n                formattedDecimals += token.value;\n                break;\n        }\n    }\n    return formattedDecimals;\n}\n/**\n * this is a cache that can contains number representation formats\n * from 0 (minimum) to 20 (maximum) digits after the decimal point\n */\nconst numberRepresentation = [];\n/** split a number into two strings that contain respectively:\n * - all digit stored in the integer part of the number\n * - all digit stored in the decimal part of the number\n *\n * The 'maxDecimal' parameter allows to indicate the number of digits to not\n * exceed in the decimal part, in which case digits are rounded.\n *\n **/\nfunction splitNumber(value, maxDecimals = MAX_DECIMAL_PLACES) {\n    const asString = value.toString();\n    if (asString.includes(\"e\"))\n        return splitNumberIntl(value, maxDecimals);\n    if (Number.isInteger(value)) {\n        return { integerDigits: asString, decimalDigits: undefined };\n    }\n    const indexOfDot = asString.indexOf(\".\");\n    let integerDigits = asString.substring(0, indexOfDot);\n    let decimalDigits = asString.substring(indexOfDot + 1);\n    if (maxDecimals === 0) {\n        if (Number(decimalDigits[0]) >= 5) {\n            integerDigits = (Number(integerDigits) + 1).toString();\n        }\n        return { integerDigits, decimalDigits: undefined };\n    }\n    if (decimalDigits.length > maxDecimals) {\n        const { integerDigits: roundedIntegerDigits, decimalDigits: roundedDecimalDigits } = limitDecimalDigits(decimalDigits, maxDecimals);\n        decimalDigits = roundedDecimalDigits;\n        if (roundedIntegerDigits !== \"0\") {\n            integerDigits = (Number(integerDigits) + Number(roundedIntegerDigits)).toString();\n        }\n    }\n    return { integerDigits, decimalDigits: removeTrailingZeroes(decimalDigits || \"\") };\n}\n/**\n *  Return the given string minus the trailing \"0\" characters.\n *\n * @param numberString : a string of integers\n * @returns the numberString, minus the eventual zeroes at the end\n */\nfunction removeTrailingZeroes(numberString) {\n    let i = numberString.length - 1;\n    while (i >= 0 && numberString[i] === \"0\") {\n        i--;\n    }\n    return numberString.slice(0, i + 1) || undefined;\n}\nconst leadingZeroesRegexp = /^0+/;\n/**\n * Limit the size of the decimal part of a number to the given number of digits.\n */\nfunction limitDecimalDigits(decimalDigits, maxDecimals) {\n    let integerDigits = \"0\";\n    let resultDecimalDigits = decimalDigits;\n    // Note : we'd want to simply use number.toFixed() to handle the max digits & rounding,\n    // but it has very strange behaviour. Ex: 12.345.toFixed(2) => \"12.35\", but 1.345.toFixed(2) => \"1.34\"\n    let slicedDecimalDigits = decimalDigits.slice(0, maxDecimals);\n    const i = maxDecimals;\n    if (Number(decimalDigits[i]) < 5) {\n        return { integerDigits, decimalDigits: slicedDecimalDigits };\n    }\n    // round up\n    const leadingZeroes = slicedDecimalDigits.match(leadingZeroesRegexp)?.[0] || \"\";\n    const slicedRoundedUp = (Number(slicedDecimalDigits) + 1).toString();\n    const withoutLeadingZeroes = slicedDecimalDigits.slice(leadingZeroes.length);\n    // e.g. carry over from 99 to 100\n    const carryOver = slicedRoundedUp.length > withoutLeadingZeroes.length;\n    if (carryOver && !leadingZeroes) {\n        integerDigits = \"1\";\n        resultDecimalDigits = undefined;\n    }\n    else if (carryOver) {\n        resultDecimalDigits = leadingZeroes.slice(0, -1) + slicedRoundedUp;\n    }\n    else {\n        resultDecimalDigits = leadingZeroes + slicedRoundedUp;\n    }\n    return { integerDigits, decimalDigits: resultDecimalDigits };\n}\n/**\n * Split numbers into decimal/integer digits using Intl.NumberFormat.\n * Supports numbers with a lot of digits that are transformed to scientific notation by\n * number.toString(), but is slow.\n */\nfunction splitNumberIntl(value, maxDecimals = MAX_DECIMAL_PLACES) {\n    let formatter = numberRepresentation[maxDecimals];\n    if (!formatter) {\n        formatter = new Intl.NumberFormat(\"en-US\", {\n            maximumFractionDigits: maxDecimals,\n            useGrouping: false,\n        });\n        numberRepresentation[maxDecimals] = formatter;\n    }\n    const [integerDigits, decimalDigits] = formatter.format(value).split(\".\");\n    return { integerDigits, decimalDigits };\n}\n/** Convert a number into a string, without scientific notation */\nfunction numberToString(number, decimalSeparator) {\n    const { integerDigits, decimalDigits } = splitNumber(number, 20);\n    return decimalDigits ? integerDigits + decimalSeparator + decimalDigits : integerDigits;\n}\n/**\n * Check if the given format is a time, date or date time format. Only check the first part of a multi-part format.\n */\nconst isDateTimeFormat = memoize(function isDateTimeFormat(format) {\n    if (!format) {\n        return false;\n    }\n    try {\n        const internalFormat = parseFormat(format);\n        return internalFormat.positive.type === \"date\";\n    }\n    catch (error) {\n        return false;\n    }\n});\nfunction applyDateTimeFormat(value, internalFormat) {\n    const jsDate = numberToJsDate(value);\n    const isMeridian = internalFormat.tokens.some((token) => token.type === \"DATE_PART\" && token.value === \"a\");\n    let currentValue = \"\";\n    for (const token of internalFormat.tokens) {\n        switch (token.type) {\n            case \"DATE_PART\":\n                currentValue += formatJSDatePart(jsDate, token.value, isMeridian);\n                break;\n            case \"REPEATED_CHAR\":\n                currentValue += REPEATED_CHAR_PLACEHOLDER + token.value;\n                break;\n            default:\n                currentValue += token.value;\n                break;\n        }\n    }\n    return currentValue;\n}\nfunction formatJSDatePart(jsDate, tokenValue, isMeridian) {\n    switch (tokenValue) {\n        case \"d\":\n            return jsDate.getDate();\n        case \"dd\":\n            return jsDate.getDate().toString().padStart(2, \"0\");\n        case \"ddd\":\n            return DAYS$1[jsDate.getDay()].slice(0, 3);\n        case \"dddd\":\n            // force translation because somehow node 22 doesn't call LazyTranslatedString.toString() whe concatenating it to a string\n            return DAYS$1[jsDate.getDay()].toString();\n        case \"m\":\n            return jsDate.getMonth() + 1;\n        case \"mm\":\n            return String(jsDate.getMonth() + 1).padStart(2, \"0\");\n        case \"mmm\":\n            return MONTHS[jsDate.getMonth()].slice(0, 3);\n        case \"mmmm\":\n            return MONTHS[jsDate.getMonth()].toString();\n        case \"mmmmm\":\n            return MONTHS[jsDate.getMonth()].slice(0, 1);\n        case \"qq\":\n            return _t(\"Q%(quarter)s\", { quarter: jsDate.getQuarter() }).toString();\n        case \"qqqq\":\n            return _t(\"Quarter %(quarter)s\", { quarter: jsDate.getQuarter() }).toString();\n        case \"yy\":\n            const fullYear = String(jsDate.getFullYear()).replace(\"-\", \"\").padStart(2, \"0\");\n            return fullYear.slice(fullYear.length - 2);\n        case \"yyyy\":\n            return jsDate.getFullYear();\n        case \"hhhh\":\n            const elapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY.getTime()) / (60 * 60 * 1000));\n            return elapsedHours.toString();\n        case \"hh\":\n            const dateHours = jsDate.getHours();\n            let hours = dateHours;\n            if (isMeridian) {\n                hours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;\n            }\n            return hours.toString().padStart(2, \"0\");\n        case \"MM\": // \"MM\" replaces \"mm\" for minutes during format parsing\n            return jsDate.getMinutes().toString().padStart(2, \"0\");\n        case \"ss\":\n            return jsDate.getSeconds().toString().padStart(2, \"0\");\n        case \"a\":\n            return jsDate.getHours() >= 12 ? \"PM\" : \"AM\";\n        default:\n            throw new Error(`invalid date format token: ${tokenValue}`);\n    }\n}\n/**\n * Get a regex matching decimal number based on the locale's thousand separator\n *\n * eg. if the locale's thousand separator is a comma, this will return a regex /[0-9]+,[0-9]/\n */\nconst getDecimalNumberRegex = memoize(function getDecimalNumberRegex(locale) {\n    return new RegExp(`[0-9]+${escapeRegExp(locale.decimalSeparator)}[0-9]`);\n});\n// -----------------------------------------------------------------------------\n// CREATE / MODIFY FORMAT\n// -----------------------------------------------------------------------------\n/**\n * Create a default format for a number.\n *\n * If possible this will try round the number to have less than DEFAULT_FORMAT_NUMBER_OF_DIGITS characters\n * in the number. This is obviously only possible for number with a big decimal part. For number with a lot\n * of digits in the integer part, keep the number as it is.\n */\nfunction createDefaultFormat(value) {\n    let { integerDigits, decimalDigits } = splitNumber(value);\n    if (!decimalDigits)\n        return \"0\";\n    const digitsInIntegerPart = integerDigits.replace(\"-\", \"\").length;\n    // If there's no space for at least the decimal separator + a decimal digit, don't display decimals\n    if (digitsInIntegerPart + 2 > DEFAULT_FORMAT_NUMBER_OF_DIGITS) {\n        return \"0\";\n    }\n    // -1 for the decimal separator character\n    const spaceForDecimalsDigits = DEFAULT_FORMAT_NUMBER_OF_DIGITS - digitsInIntegerPart - 1;\n    ({ decimalDigits } = splitNumber(value, Math.min(spaceForDecimalsDigits, decimalDigits.length)));\n    return decimalDigits ? \"0.\" + \"0\".repeat(decimalDigits.length) : \"0\";\n}\nfunction detectDateFormat(content, locale) {\n    if (!isDateTime(content, locale)) {\n        return undefined;\n    }\n    const internalDate = parseDateTime(content, locale);\n    return internalDate.format;\n}\n/** use this function only if the content corresponds to a number (means that isNumber(content) return true */\nfunction detectNumberFormat(content) {\n    const digitBase = content.includes(\".\") ? \"0.00\" : \"0\";\n    const matchedCurrencies = content.match(/[\\$\u20ac]/);\n    if (matchedCurrencies) {\n        const matchedFirstDigit = content.match(/[\\d]/);\n        const currency = \"[$\" + matchedCurrencies.values().next().value + \"]\";\n        if (matchedFirstDigit.index < matchedCurrencies.index) {\n            return \"#,##\" + digitBase + currency;\n        }\n        return currency + \"#,##\" + digitBase;\n    }\n    if (content.includes(\"%\")) {\n        return digitBase + \"%\";\n    }\n    return undefined;\n}\nfunction createCurrencyFormat(currency) {\n    const decimalPlaces = currency.decimalPlaces ?? 2;\n    const position = currency.position ?? \"before\";\n    const code = currency.code ?? \"\";\n    const symbol = currency.symbol ?? \"\";\n    const decimalRepresentation = decimalPlaces ? \".\" + \"0\".repeat(decimalPlaces) : \"\";\n    const numberFormat = \"#,##0\" + decimalRepresentation;\n    let textExpression = `${code} ${symbol}`.trim();\n    if (position === \"after\" && code) {\n        textExpression = \" \" + textExpression;\n    }\n    return insertTextInFormat(textExpression, position, numberFormat);\n}\nfunction createAccountingFormat(currency) {\n    const decimalPlaces = currency.decimalPlaces ?? 2;\n    const position = currency.position ?? \"before\";\n    const code = currency.code ?? \"\";\n    const symbol = currency.symbol ?? \"\";\n    const decimalRepresentation = decimalPlaces ? \".\" + \"0\".repeat(decimalPlaces) : \"\";\n    const numberFormat = \"#,##0\" + decimalRepresentation;\n    let textExpression = `${code} ${symbol}`.trim();\n    if (position === \"after\" && code) {\n        textExpression = \" \" + textExpression;\n    }\n    const positivePart = insertTextInAccountingFormat(textExpression, position, ` ${numberFormat} `);\n    const negativePart = insertTextInAccountingFormat(textExpression, position, `(${numberFormat})`);\n    const zeroPart = insertTextInAccountingFormat(textExpression, position, \"  -  \");\n    return [positivePart, negativePart, zeroPart].join(\";\");\n}\nfunction insertTextInAccountingFormat(text, position, format) {\n    const textExpression = `[$${text}]`;\n    return position === \"before\" ? textExpression + \"* \" + format : format + \"* \" + textExpression;\n}\nfunction insertTextInFormat(text, position, format) {\n    const textExpression = `[$${text}]`;\n    return position === \"before\" ? textExpression + format : format + textExpression;\n}\nfunction roundFormat(format) {\n    const multiPartFormat = parseFormat(format);\n    const roundedInternalFormat = {\n        positive: _roundFormat(multiPartFormat.positive),\n        negative: multiPartFormat.negative ? _roundFormat(multiPartFormat.negative) : undefined,\n        zero: multiPartFormat.zero ? _roundFormat(multiPartFormat.zero) : undefined,\n        text: multiPartFormat.text,\n    };\n    return convertInternalFormatToFormat(roundedInternalFormat);\n}\nfunction _roundFormat(internalFormat) {\n    if (internalFormat.type !== \"number\" || !internalFormat.decimalPart) {\n        return internalFormat;\n    }\n    const nonDigitDecimalPart = internalFormat.decimalPart.filter((token) => token.type !== \"DIGIT\");\n    return {\n        ...internalFormat,\n        decimalPart: undefined,\n        integerPart: [...internalFormat.integerPart, ...nonDigitDecimalPart],\n    };\n}\nfunction humanizeNumber({ value, format }, locale) {\n    const numberFormat = formatLargeNumber({\n        value,\n        format,\n    }, undefined, locale);\n    return formatValue(value, { format: numberFormat, locale });\n}\nfunction formatLargeNumber(arg, unit, locale) {\n    let value = 0;\n    try {\n        value = Math.abs(toNumber(arg?.value, locale));\n    }\n    catch (e) {\n        return \"\";\n    }\n    const format = arg?.format;\n    if (unit !== undefined) {\n        const postFix = unit?.value;\n        switch (postFix) {\n            case \"k\":\n                return createLargeNumberFormat(format, 1, \"k\");\n            case \"m\":\n                return createLargeNumberFormat(format, 2, \"m\");\n            case \"b\":\n                return createLargeNumberFormat(format, 3, \"b\");\n            default:\n                throw new EvaluationError(_t(\"The formatting unit should be 'k', 'm' or 'b'.\"));\n        }\n    }\n    if (value < 1e5) {\n        return createLargeNumberFormat(format, 0, \"\");\n    }\n    else if (value < 1e8) {\n        return createLargeNumberFormat(format, 1, \"k\");\n    }\n    else if (value < 1e11) {\n        return createLargeNumberFormat(format, 2, \"m\");\n    }\n    return createLargeNumberFormat(format, 3, \"b\");\n}\nfunction createLargeNumberFormat(format, magnitude, postFix, locale) {\n    const multiPartFormat = parseFormat(format || \"#,##0\");\n    const roundedInternalFormat = {\n        positive: _createLargeNumberFormat(multiPartFormat.positive, magnitude, postFix),\n        negative: multiPartFormat.negative\n            ? _createLargeNumberFormat(multiPartFormat.negative, magnitude, postFix)\n            : undefined,\n        zero: multiPartFormat.zero\n            ? _createLargeNumberFormat(multiPartFormat.zero, magnitude, postFix)\n            : undefined,\n        text: multiPartFormat.text,\n    };\n    return convertInternalFormatToFormat(roundedInternalFormat);\n}\nfunction _createLargeNumberFormat(format, magnitude, postFix) {\n    if (format.type !== \"number\") {\n        return format;\n    }\n    const postFixToken = { type: \"STRING\", value: postFix };\n    let newIntegerPart = [...format.integerPart];\n    const lastDigitIndex = newIntegerPart.findLastIndex((token) => token.type === \"DIGIT\");\n    if (lastDigitIndex === -1) {\n        throw new Error(\"Cannot create a large number format from a format with no digit.\");\n    }\n    while (newIntegerPart[lastDigitIndex + 1]?.type === \"THOUSANDS_SEPARATOR\") {\n        newIntegerPart = removeIndexesFromArray(newIntegerPart, [lastDigitIndex + 1]);\n    }\n    const tokenAfterDigits = newIntegerPart[lastDigitIndex + 1];\n    if (tokenAfterDigits?.type === \"STRING\" && [\"m\", \"k\", \"b\"].includes(tokenAfterDigits.value)) {\n        newIntegerPart = replaceItemAtIndex(newIntegerPart, postFixToken, lastDigitIndex + 1);\n    }\n    else {\n        newIntegerPart = insertItemsAtIndex(newIntegerPart, [postFixToken], lastDigitIndex + 1);\n    }\n    if (magnitude > 0) {\n        newIntegerPart = insertItemsAtIndex(newIntegerPart, Array(magnitude).fill({ type: \"THOUSANDS_SEPARATOR\", value: \",\" }), lastDigitIndex + 1);\n    }\n    const missingPercents = format.percentSymbols - newIntegerPart.filter((tk) => tk.type === \"PERCENT\").length;\n    newIntegerPart.push(...new Array(missingPercents).fill({ type: \"PERCENT\", value: \"%\" }));\n    return { ...format, integerPart: newIntegerPart, decimalPart: undefined, magnitude };\n}\nfunction changeDecimalPlaces(format, step) {\n    const multiPartFormat = parseFormat(format);\n    const newInternalFormat = {\n        positive: _changeDecimalPlace(multiPartFormat.positive, step),\n        negative: multiPartFormat.negative\n            ? _changeDecimalPlace(multiPartFormat.negative, step)\n            : undefined,\n        zero: multiPartFormat.zero ? _changeDecimalPlace(multiPartFormat.zero, step) : undefined,\n        text: multiPartFormat.text,\n    };\n    // Re-parse the format to make sure we don't break the number of digit limit\n    return convertInternalFormatToFormat(parseFormat(convertInternalFormatToFormat(newInternalFormat)));\n}\nfunction _changeDecimalPlace(format, step) {\n    if (format.type !== \"number\") {\n        return format;\n    }\n    return (step > 0 ? addDecimalPlaces(format, step) : removeDecimalPlaces(format, Math.abs(step)));\n}\nfunction removeDecimalPlaces(format, step) {\n    let decimalPart = format.decimalPart;\n    if (!decimalPart) {\n        return format;\n    }\n    const indexesToRemove = [];\n    let digitCount = 0;\n    for (let i = decimalPart.length - 1; i >= 0; i--) {\n        if (digitCount >= Math.abs(step)) {\n            break;\n        }\n        if (decimalPart[i].type === \"DIGIT\") {\n            digitCount++;\n            indexesToRemove.push(i);\n        }\n    }\n    decimalPart = removeIndexesFromArray(decimalPart, indexesToRemove);\n    if (decimalPart.some((token) => token.type === \"DIGIT\")) {\n        return { ...format, decimalPart };\n    }\n    return {\n        ...format,\n        decimalPart: undefined,\n        integerPart: [...format.integerPart, ...decimalPart],\n    };\n}\nfunction addDecimalPlaces(format, step) {\n    let integerPart = format.integerPart;\n    let decimalPart = format.decimalPart;\n    if (!decimalPart) {\n        const lastDigitIndex = integerPart.findLastIndex((token) => token.type === \"DIGIT\");\n        decimalPart = integerPart.slice(lastDigitIndex + 1);\n        integerPart = integerPart.slice(0, lastDigitIndex + 1);\n    }\n    const digitsToAdd = range(0, step).map(() => ({ type: \"DIGIT\", value: \"0\" }));\n    const lastDigitIndex = decimalPart.findLastIndex((token) => token.type === \"DIGIT\");\n    if (lastDigitIndex === -1) {\n        decimalPart = [...digitsToAdd, ...decimalPart];\n    }\n    else {\n        decimalPart = insertItemsAtIndex(decimalPart, digitsToAdd, lastDigitIndex + 1);\n    }\n    return { ...format, decimalPart, integerPart };\n}\nfunction isExcelCompatible(format) {\n    const internalFormat = parseFormat(format);\n    for (const part of [internalFormat.positive, internalFormat.negative, internalFormat.zero]) {\n        if (part &&\n            part.type === \"date\" &&\n            part.tokens.some((token) => token.type === \"DATE_PART\" && token.value.includes(\"q\"))) {\n            return false;\n        }\n    }\n    return true;\n}\nfunction isTextFormat(format) {\n    if (!format)\n        return false;\n    try {\n        const internalFormat = parseFormat(format);\n        return internalFormat.positive.type === \"text\";\n    }\n    catch {\n        return false;\n    }\n}\n\nclass RangeImpl {\n    getSheetSize;\n    _zone;\n    parts;\n    invalidXc;\n    prefixSheet = false;\n    sheetId; // the sheet on which the range is defined\n    invalidSheetName; // the name of any sheet that is invalid\n    constructor(args, getSheetSize) {\n        this.getSheetSize = getSheetSize;\n        this._zone = args.zone;\n        this.prefixSheet = args.prefixSheet;\n        this.invalidXc = args.invalidXc;\n        this.sheetId = args.sheetId;\n        this.invalidSheetName = args.invalidSheetName;\n        let _fixedParts = [...args.parts];\n        if (args.parts.length === 1 && getZoneArea(this.zone) > 1) {\n            _fixedParts.push({ ...args.parts[0] });\n        }\n        else if (args.parts.length === 2 && getZoneArea(this.zone) === 1) {\n            _fixedParts.pop();\n        }\n        this.parts = _fixedParts;\n    }\n    static fromRange(range, getters) {\n        if (range instanceof RangeImpl) {\n            return range;\n        }\n        return new RangeImpl(range, getters.getSheetSize);\n    }\n    get unboundedZone() {\n        return this._zone;\n    }\n    get zone() {\n        const { left, top, bottom, right } = this._zone;\n        if (right !== undefined && bottom !== undefined) {\n            return this._zone;\n        }\n        else if (bottom === undefined && right !== undefined) {\n            return { right, top, left, bottom: this.getSheetSize(this.sheetId).numberOfRows - 1 };\n        }\n        else if (right === undefined && bottom !== undefined) {\n            return { bottom, left, top, right: this.getSheetSize(this.sheetId).numberOfCols - 1 };\n        }\n        throw new Error(_t(\"Bad zone format\"));\n    }\n    static getRangeParts(xc, zone) {\n        const parts = xc.split(\":\").map((p) => {\n            const isFullRow = isRowReference(p);\n            return {\n                colFixed: isFullRow ? false : p.startsWith(\"$\"),\n                rowFixed: isFullRow ? p.startsWith(\"$\") : p.includes(\"$\", 1),\n            };\n        });\n        const isFullCol = zone.bottom === undefined;\n        const isFullRow = zone.right === undefined;\n        if (isFullCol) {\n            parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;\n            parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;\n        }\n        if (isFullRow) {\n            parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;\n            parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;\n        }\n        return parts;\n    }\n    get isFullCol() {\n        return this._zone.bottom === undefined;\n    }\n    get isFullRow() {\n        return this._zone.right === undefined;\n    }\n    get rangeData() {\n        return {\n            _zone: this._zone,\n            _sheetId: this.sheetId,\n        };\n    }\n    /**\n     * Check that a zone is valid regarding the order of top-bottom and left-right.\n     * Left should be smaller than right, top should be smaller than bottom.\n     * If it's not the case, simply invert them, and invert the linked parts\n     */\n    orderZone() {\n        if (isZoneOrdered(this._zone)) {\n            return this;\n        }\n        const zone = { ...this._zone };\n        let parts = this.parts;\n        if (zone.right !== undefined && zone.right < zone.left) {\n            let right = zone.right;\n            zone.right = zone.left;\n            zone.left = right;\n            parts = [\n                {\n                    colFixed: parts[1]?.colFixed || false,\n                    rowFixed: parts[0]?.rowFixed || false,\n                },\n                {\n                    colFixed: parts[0]?.colFixed || false,\n                    rowFixed: parts[1]?.rowFixed || false,\n                },\n            ];\n        }\n        if (zone.bottom !== undefined && zone.bottom < zone.top) {\n            let bottom = zone.bottom;\n            zone.bottom = zone.top;\n            zone.top = bottom;\n            parts = [\n                {\n                    colFixed: parts[0]?.colFixed || false,\n                    rowFixed: parts[1]?.rowFixed || false,\n                },\n                {\n                    colFixed: parts[1]?.colFixed || false,\n                    rowFixed: parts[0]?.rowFixed || false,\n                },\n            ];\n        }\n        return this.clone({ zone, parts });\n    }\n    /**\n     *\n     * @param rangeParams optional, values to put in the cloned range instead of the current values of the range\n     */\n    clone(rangeParams) {\n        return new RangeImpl({\n            zone: rangeParams?.zone ? rangeParams.zone : { ...this._zone },\n            sheetId: rangeParams?.sheetId ? rangeParams.sheetId : this.sheetId,\n            invalidSheetName: rangeParams && \"invalidSheetName\" in rangeParams // 'attr in obj' instead of just 'obj.attr' because we accept undefined values\n                ? rangeParams.invalidSheetName\n                : this.invalidSheetName,\n            invalidXc: rangeParams && \"invalidXc\" in rangeParams ? rangeParams.invalidXc : this.invalidXc,\n            parts: rangeParams?.parts\n                ? rangeParams.parts\n                : this.parts.map((part) => {\n                    return { rowFixed: part.rowFixed, colFixed: part.colFixed };\n                }),\n            prefixSheet: rangeParams?.prefixSheet !== undefined ? rangeParams.prefixSheet : this.prefixSheet,\n        }, this.getSheetSize);\n    }\n}\n/**\n * Copy a range. If the range is on the sheetIdFrom, the range will target\n * sheetIdTo.\n */\nfunction copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) {\n    const sheetId = range.sheetId === sheetIdFrom ? sheetIdTo : range.sheetId;\n    return range.clone({ sheetId });\n}\n/**\n * Create a range from a xc. If the xc is empty, this function returns undefined.\n */\nfunction createValidRange(getters, sheetId, xc) {\n    if (!xc)\n        return;\n    const range = getters.getRangeFromSheetXC(sheetId, xc);\n    return !(range.invalidSheetName || range.invalidXc) ? range : undefined;\n}\n/**\n * Spread multiple colrows zone to one row/col zone and add a many new input range as needed.\n * For example, A1:B4 will become [A1:A4, B1:B4]\n */\nfunction spreadRange(getters, dataSets) {\n    const postProcessedRanges = [];\n    for (const dataSet of dataSets) {\n        const range = dataSet.dataRange;\n        if (!getters.isRangeValid(range)) {\n            postProcessedRanges.push(dataSet); // ignore invalid range\n            continue;\n        }\n        const { sheetName } = splitReference(range);\n        const sheetPrefix = sheetName ? `${sheetName}!` : \"\";\n        const zone = toUnboundedZone(range);\n        if (zone.bottom !== zone.top && zone.left != zone.right) {\n            if (zone.right) {\n                for (let j = zone.left; j <= zone.right; ++j) {\n                    postProcessedRanges.push({\n                        ...dataSet,\n                        dataRange: `${sheetPrefix}${zoneToXc({\n                            left: j,\n                            right: j,\n                            top: zone.top,\n                            bottom: zone.bottom,\n                        })}`,\n                    });\n                }\n            }\n            else {\n                for (let j = zone.top; j <= zone.bottom; ++j) {\n                    postProcessedRanges.push({\n                        ...dataSet,\n                        dataRange: `${sheetPrefix}${zoneToXc({\n                            left: zone.left,\n                            right: zone.right,\n                            top: j,\n                            bottom: j,\n                        })}`,\n                    });\n                }\n            }\n        }\n        else {\n            postProcessedRanges.push(dataSet);\n        }\n    }\n    return postProcessedRanges;\n}\n/**\n * Get all the cell positions in the given ranges. If a cell is in multiple ranges, it will be returned multiple times.\n */\nfunction getCellPositionsInRanges(ranges) {\n    const cellPositions = [];\n    for (const range of ranges) {\n        for (const position of positions(range.zone)) {\n            cellPositions.push({ ...position, sheetId: range.sheetId });\n        }\n    }\n    return cellPositions;\n}\n\n/** Methods from Odoo Web Utils  */\n/**\n * This function computes a score that represent the fact that the\n * string contains the pattern, or not\n *\n * - If the score is 0, the string does not contain the letters of the pattern in\n *   the correct order.\n * - if the score is > 0, it actually contains the letters.\n *\n * Better matches will get a higher score: consecutive letters are better,\n * and a match closer to the beginning of the string is also scored higher.\n */\nfunction fuzzyMatch(pattern, str) {\n    pattern = pattern.toLocaleLowerCase();\n    str = str.toLocaleLowerCase();\n    let totalScore = 0;\n    let currentScore = 0;\n    let len = str.length;\n    let patternIndex = 0;\n    for (let i = 0; i < len; i++) {\n        if (str[i] === pattern[patternIndex]) {\n            patternIndex++;\n            currentScore += 100 + currentScore - i / 200;\n        }\n        else {\n            currentScore = 0;\n        }\n        totalScore = totalScore + currentScore;\n    }\n    return patternIndex === pattern.length ? totalScore : 0;\n}\n/**\n * Return a list of things that matches a pattern, ordered by their 'score' (\n * higher score first). An higher score means that the match is better. For\n * example, consecutive letters are considered a better match.\n */\nfunction fuzzyLookup(pattern, list, fn) {\n    const results = [];\n    list.forEach((data) => {\n        const score = fuzzyMatch(pattern, fn(data));\n        if (score > 0) {\n            results.push({ score, elem: data });\n        }\n    });\n    // we want better matches first\n    results.sort((a, b) => b.score - a.score);\n    return results.map((r) => r.elem);\n}\n\nfunction createDefaultRows(rowNumber) {\n    const rows = [];\n    for (let i = 0; i < rowNumber; i++) {\n        const row = {\n            cells: {},\n        };\n        rows.push(row);\n    }\n    return rows;\n}\nfunction moveHeaderIndexesOnHeaderAddition(indexHeaderAdded, numberAdded, headers) {\n    return headers.map((header) => {\n        if (header >= indexHeaderAdded) {\n            return header + numberAdded;\n        }\n        return header;\n    });\n}\nfunction moveHeaderIndexesOnHeaderDeletion(deletedHeaders, headers) {\n    deletedHeaders = [...deletedHeaders].sort((a, b) => b - a);\n    return headers\n        .map((header) => {\n        for (const deletedHeader of deletedHeaders) {\n            if (header > deletedHeader) {\n                header--;\n            }\n            else if (header === deletedHeader) {\n                return undefined;\n            }\n        }\n        return header;\n    })\n        .filter(isDefined);\n}\n\nfunction computeTextLinesHeight(textLineHeight, numberOfLines = 1) {\n    return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN;\n}\n/**\n * Get the default height of the cell given its style.\n */\nfunction getDefaultCellHeight(ctx, cell, colSize) {\n    if (!cell || (!cell.isFormula && !cell.content)) {\n        return DEFAULT_CELL_HEIGHT;\n    }\n    const maxWidth = cell.style?.wrapping === \"wrap\" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;\n    const numberOfLines = cell.isFormula\n        ? 1\n        : splitTextToWidth(ctx, cell.content, cell.style, maxWidth).length;\n    const fontSize = computeTextFontSizeInPixels(cell.style);\n    return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL;\n}\nfunction getDefaultContextFont(fontSize, bold = false, italic = false) {\n    const italicStr = italic ? \"italic\" : \"\";\n    const weight = bold ? \"bold\" : \"\";\n    return `${italicStr} ${weight} ${fontSize}px ${DEFAULT_FONT}`;\n}\nconst textWidthCache = {};\nfunction computeTextWidth(context, text, style, fontUnit = \"pt\") {\n    const font = computeTextFont(style, fontUnit);\n    context.save();\n    context.font = font;\n    const width = computeCachedTextWidth(context, text);\n    context.restore();\n    return width;\n}\nfunction computeCachedTextWidth(context, text) {\n    const font = context.font;\n    if (!textWidthCache[font]) {\n        textWidthCache[font] = {};\n    }\n    if (textWidthCache[font][text] === undefined) {\n        const textWidth = context.measureText(text).width;\n        textWidthCache[font][text] = textWidth;\n    }\n    return textWidthCache[font][text];\n}\nconst textDimensionsCache = {};\nfunction computeTextDimension(context, text, style, fontUnit = \"pt\") {\n    const font = computeTextFont(style, fontUnit);\n    context.save();\n    context.font = font;\n    const dimensions = computeCachedTextDimension(context, text);\n    context.restore();\n    return dimensions;\n}\nfunction computeCachedTextDimension(context, text) {\n    const font = context.font;\n    if (!textDimensionsCache[font]) {\n        textDimensionsCache[font] = {};\n    }\n    if (textDimensionsCache[font][text] === undefined) {\n        const measure = context.measureText(text);\n        const width = measure.width;\n        const height = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;\n        textDimensionsCache[font][text] = { width, height };\n    }\n    return textDimensionsCache[font][text];\n}\nfunction fontSizeInPixels(fontSize) {\n    return Math.round((fontSize * 96) / 72);\n}\nfunction computeTextFont(style, fontUnit = \"pt\") {\n    const italic = style.italic ? \"italic \" : \"\";\n    const weight = style.bold ? \"bold\" : DEFAULT_FONT_WEIGHT;\n    const size = fontUnit === \"pt\" ? computeTextFontSizeInPixels(style) : style.fontSize;\n    return `${italic}${weight} ${size ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;\n}\nfunction computeTextFontSizeInPixels(style) {\n    const sizeInPt = style?.fontSize || DEFAULT_FONT_SIZE;\n    return fontSizeInPixels(sizeInPt);\n}\nfunction splitWordToSpecificWidth(ctx, word, width, style) {\n    const wordWidth = computeTextWidth(ctx, word, style);\n    if (wordWidth <= width) {\n        return [word];\n    }\n    const splitWord = [];\n    let wordPart = \"\";\n    for (let l of word) {\n        const wordPartWidth = computeTextWidth(ctx, wordPart + l, style);\n        if (wordPartWidth > width) {\n            splitWord.push(wordPart);\n            wordPart = l;\n        }\n        else {\n            wordPart += l;\n        }\n    }\n    splitWord.push(wordPart);\n    return splitWord;\n}\n/**\n * Return the given text, split in multiple lines if needed. The text will be split in multiple\n * line if it contains NEWLINE characters, or if it's longer than the given width.\n */\nfunction splitTextToWidth(ctx, text, style, width) {\n    if (!style)\n        style = {};\n    const brokenText = [];\n    // Checking if text contains NEWLINE before split makes it very slightly slower if text contains it,\n    // but 5-10x faster if it doesn't\n    const lines = text.includes(NEWLINE) ? text.split(NEWLINE) : [text];\n    for (const line of lines) {\n        const words = line.includes(\" \") ? line.split(\" \") : [line];\n        if (!width) {\n            brokenText.push(line);\n            continue;\n        }\n        let textLine = \"\";\n        let availableWidth = width;\n        for (let word of words) {\n            const splitWord = splitWordToSpecificWidth(ctx, word, width, style);\n            const lastPart = splitWord.pop();\n            const lastPartWidth = computeTextWidth(ctx, lastPart, style);\n            // At this step: \"splitWord\" is an array composed of parts of word whose\n            // length is at most equal to \"width\".\n            // Last part contains the end of the word.\n            // Note that: When word length is less than width, then lastPart is equal\n            // to word and splitWord is empty\n            if (splitWord.length) {\n                if (textLine !== \"\") {\n                    brokenText.push(textLine);\n                    textLine = \"\";\n                    availableWidth = width;\n                }\n                splitWord.forEach((wordPart) => {\n                    brokenText.push(wordPart);\n                });\n                textLine = lastPart;\n                availableWidth = width - lastPartWidth;\n            }\n            else {\n                // here \"lastPart\" is equal to \"word\" and the \"word\" size is smaller than \"width\"\n                const _word = textLine === \"\" ? lastPart : \" \" + lastPart;\n                const wordWidth = computeTextWidth(ctx, _word, style);\n                if (wordWidth <= availableWidth) {\n                    textLine += _word;\n                    availableWidth -= wordWidth;\n                }\n                else {\n                    brokenText.push(textLine);\n                    textLine = lastPart;\n                    availableWidth = width - lastPartWidth;\n                }\n            }\n        }\n        if (textLine !== \"\") {\n            brokenText.push(textLine);\n        }\n    }\n    return brokenText;\n}\n/**\n * Return the font size that makes the width of a text match the given line width.\n * Minimum font size is 1.\n *\n * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size.\n */\nfunction getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = 0.25) {\n    let minFontSize = 1;\n    if (getTextWidth(minFontSize) > lineWidth)\n        return minFontSize;\n    if (getTextWidth(maxFontSize) < lineWidth)\n        return maxFontSize;\n    // Dichotomic search\n    let fontSize = (minFontSize + maxFontSize) / 2;\n    let currentTextWidth = getTextWidth(fontSize);\n    // Use a maximum number of iterations to be safe, because measuring text isn't 100% precise\n    let iterations = 0;\n    while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) {\n        if (currentTextWidth >= lineWidth) {\n            maxFontSize = (minFontSize + maxFontSize) / 2;\n        }\n        else {\n            minFontSize = (minFontSize + maxFontSize) / 2;\n        }\n        fontSize = (minFontSize + maxFontSize) / 2;\n        currentTextWidth = getTextWidth(fontSize);\n        iterations++;\n    }\n    return fontSize;\n}\nfunction computeIconWidth(style) {\n    return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN;\n}\n/** Transform a string to lower case. If the string is undefined, return an empty string */\nfunction toLowerCase(str) {\n    return str ? str.toLowerCase() : \"\";\n}\n/**\n * Extract the fontSize from a context font string\n * @param font The (context) font string to parse\n * @returns The fontSize in pixels\n */\nconst pxRegex = /([0-9\\.]*)px/;\nfunction getContextFontSize(font) {\n    return Number(font.match(pxRegex)?.[1]);\n}\n// Inspired from https://stackoverflow.com/a/10511598\nfunction clipTextWithEllipsis(ctx, text, maxWidth) {\n    let width = computeCachedTextWidth(ctx, text);\n    if (width <= maxWidth) {\n        return text;\n    }\n    const ellipsis = \"\u2026\";\n    const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis);\n    if (width <= ellipsisWidth) {\n        return text;\n    }\n    let len = text.length;\n    while (width >= maxWidth - ellipsisWidth && len-- > 0) {\n        text = text.substring(0, len);\n        width = computeCachedTextWidth(ctx, text);\n    }\n    return text + ellipsis;\n}\nfunction drawDecoratedText(context, text, position, underline = false, strikethrough = false, strokeWidth = getContextFontSize(context.font) / 10 //This value is defined to get a good looking stroke\n) {\n    context.fillText(text, position.x, position.y);\n    if (!underline && !strikethrough) {\n        return;\n    }\n    const measure = context.measureText(text);\n    const textWidth = measure.width;\n    const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;\n    const boxHeight = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;\n    let { x, y } = position;\n    let strikeY = y, underlineY = y;\n    switch (context.textAlign) {\n        case \"center\":\n            x -= textWidth / 2;\n            break;\n        case \"right\":\n            x -= textWidth;\n            break;\n    }\n    switch (context.textBaseline) {\n        case \"top\":\n            underlineY += boxHeight - 2 * strokeWidth;\n            strikeY += boxHeight / 2 - strokeWidth;\n            break;\n        case \"middle\":\n            underlineY += boxHeight / 2 - strokeWidth;\n            break;\n        case \"alphabetic\":\n            underlineY += 2 * strokeWidth;\n            strikeY -= 3 * strokeWidth;\n            break;\n        case \"bottom\":\n            underlineY = y;\n            strikeY -= textHeight / 2 - strokeWidth / 2;\n            break;\n    }\n    if (underline) {\n        context.lineWidth = strokeWidth;\n        context.strokeStyle = context.fillStyle;\n        context.beginPath();\n        context.moveTo(x, underlineY);\n        context.lineTo(x + textWidth, underlineY);\n        context.stroke();\n    }\n    if (strikethrough) {\n        context.lineWidth = strokeWidth;\n        context.strokeStyle = context.fillStyle;\n        context.beginPath();\n        context.moveTo(x, strikeY);\n        context.lineTo(x + textWidth, strikeY);\n        context.stroke();\n    }\n}\n\n/*\n * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript\n * */\nclass UuidGenerator {\n    isFastIdStrategy = false;\n    fastIdStart = 0;\n    setIsFastStrategy(isFast) {\n        this.isFastIdStrategy = isFast;\n    }\n    uuidv4() {\n        if (this.isFastIdStrategy) {\n            this.fastIdStart++;\n            return String(this.fastIdStart);\n            //@ts-ignore\n        }\n        else if (window.crypto && window.crypto.getRandomValues) {\n            //@ts-ignore\n            return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));\n        }\n        else {\n            // mainly for jest and other browsers that do not have the crypto functionality\n            return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, function (c) {\n                var r = (Math.random() * 16) | 0, v = c === \"x\" ? r : (r & 0x3) | 0x8;\n                return v.toString(16);\n            });\n        }\n    }\n}\n\nfunction getClipboardDataPositions(sheetId, zones) {\n    const lefts = new Set(zones.map((z) => z.left));\n    const rights = new Set(zones.map((z) => z.right));\n    const tops = new Set(zones.map((z) => z.top));\n    const bottoms = new Set(zones.map((z) => z.bottom));\n    const areZonesCompatible = (tops.size === 1 && bottoms.size === 1) || (lefts.size === 1 && rights.size === 1);\n    // In order to don't paste several times the same cells in intersected zones\n    // --> we merge zones that have common cells\n    const clippedZones = areZonesCompatible\n        ? mergeOverlappingZones(zones)\n        : [zones[zones.length - 1]];\n    const cellsPosition = clippedZones.map((zone) => positions(zone)).flat();\n    const columnsIndexes = [...new Set(cellsPosition.map((p) => p.col))].sort((a, b) => a - b);\n    const rowsIndexes = [...new Set(cellsPosition.map((p) => p.row))].sort((a, b) => a - b);\n    return { sheetId, zones, clippedZones, columnsIndexes, rowsIndexes };\n}\n/**\n * The clipped zone is copied as many times as it fits in the target.\n * This returns the list of zones where the clipped zone is copy-pasted.\n */\nfunction splitZoneForPaste(selection, splitWidth, splitHeight) {\n    const right = Math.max(selection.right - splitWidth + 1, selection.left);\n    const bottom = Math.max(selection.bottom - splitHeight + 1, selection.top);\n    const zones = [];\n    for (let left = selection.left; left <= right; left += splitWidth) {\n        for (let top = selection.top; top <= bottom; top += splitHeight) {\n            zones.push({\n                left,\n                top,\n                bottom: top + splitHeight - 1,\n                right: left + splitWidth - 1,\n            });\n        }\n    }\n    return zones;\n}\n/**\n * Compute the complete zones where to paste the current clipboard\n */\nfunction getPasteZones(target, content) {\n    if (!content.length || !content[0].length) {\n        return target;\n    }\n    const width = content[0].length, height = content.length;\n    return target.map((t) => splitZoneForPaste(t, width, height)).flat();\n}\nfunction parseOSClipboardContent(content) {\n    if (!content[ClipboardMIMEType.Html]) {\n        return {\n            text: content[ClipboardMIMEType.PlainText],\n        };\n    }\n    const htmlDocument = new DOMParser().parseFromString(content[ClipboardMIMEType.Html], \"text/html\");\n    const oSheetClipboardData = htmlDocument\n        .querySelector(\"div\")\n        ?.getAttribute(\"data-osheet-clipboard\");\n    const spreadsheetContent = oSheetClipboardData && JSON.parse(oSheetClipboardData);\n    return {\n        text: content[ClipboardMIMEType.PlainText],\n        data: spreadsheetContent,\n    };\n}\n\nclass ClipboardHandler {\n    getters;\n    dispatch;\n    constructor(getters, dispatch) {\n        this.getters = getters;\n        this.dispatch = dispatch;\n    }\n    copy(data) {\n        return;\n    }\n    paste(target, clippedContent, options) { }\n    isPasteAllowed(sheetId, target, content, option) {\n        return \"Success\" /* CommandResult.Success */;\n    }\n    isCutAllowed(data) {\n        return \"Success\" /* CommandResult.Success */;\n    }\n    getPasteTarget(sheetId, target, content, options) {\n        return { zones: [], sheetId };\n    }\n    convertTextToClipboardData(data) {\n        return;\n    }\n}\n\nclass AbstractCellClipboardHandler extends ClipboardHandler {\n    copy(data) {\n        return;\n    }\n    pasteFromCopy(sheetId, target, content, options) {\n        if (target.length === 1) {\n            // in this specific case, due to the isPasteAllowed function:\n            // state.cells can contains several cells.\n            // So if the target zone is larger than the copied zone,\n            // we duplicate each cells as many times as possible to fill the zone.\n            for (const zone of getPasteZones(target, content)) {\n                this.pasteZone(sheetId, zone.left, zone.top, content, options);\n            }\n        }\n        else {\n            // in this case, due to the isPasteAllowed function: state.cells contains\n            // only one cell\n            for (const zone of recomputeZones(target)) {\n                for (let col = zone.left; col <= zone.right; col++) {\n                    for (let row = zone.top; row <= zone.bottom; row++) {\n                        this.pasteZone(sheetId, col, row, content, options);\n                    }\n                }\n            }\n        }\n    }\n    pasteZone(sheetId, col, row, data, clipboardOptions) { }\n}\n\nclass BorderClipboardHandler extends AbstractCellClipboardHandler {\n    copy(data) {\n        const sheetId = data.sheetId;\n        if (data.zones.length === 0) {\n            return;\n        }\n        const { rowsIndexes, columnsIndexes } = data;\n        const borders = [];\n        for (const row of rowsIndexes) {\n            const bordersInRow = [];\n            for (const col of columnsIndexes) {\n                const position = { col, row, sheetId };\n                bordersInRow.push(this.getters.getCellBorder(position));\n            }\n            borders.push(bordersInRow);\n        }\n        return { borders };\n    }\n    paste(target, content, options) {\n        const sheetId = target.sheetId;\n        if (options.pasteOption === \"asValue\") {\n            return;\n        }\n        const zones = target.zones;\n        if (!options.isCutOperation) {\n            this.pasteFromCopy(sheetId, zones, content.borders);\n        }\n        else {\n            const { left, top } = zones[0];\n            this.pasteZone(sheetId, left, top, content.borders);\n        }\n    }\n    pasteZone(sheetId, col, row, borders) {\n        for (const [r, rowBorders] of borders.entries()) {\n            for (const [c, originBorders] of rowBorders.entries()) {\n                const position = { col: col + c, row: row + r, sheetId };\n                this.pasteBorder(originBorders, position);\n            }\n        }\n    }\n    /**\n     * Paste the border at the given position to the target position\n     */\n    pasteBorder(originBorders, target) {\n        const targetBorders = this.getters.getCellBorder(target);\n        const border = {\n            ...targetBorders,\n            ...originBorders,\n        };\n        this.dispatch(\"SET_BORDER\", { ...target, border });\n    }\n}\n\n/**\n * Tokenizer\n *\n * A tokenizer is a piece of code whose job is to transform a string into a list\n * of \"tokens\". For example, \"(12+\" is converted into:\n *   [{type: \"LEFT_PAREN\", value: \"(\"},\n *    {type: \"NUMBER\", value: \"12\"},\n *    {type: \"OPERATOR\", value: \"+\"}]\n *\n * As the example shows, a tokenizer does not care about the meaning behind those\n * tokens. It only cares about the structure.\n *\n * The tokenizer is usually the first step in a compilation pipeline.  Also, it\n * is useful for the composer, which needs to be able to work with incomplete\n * formulas.\n */\nconst POSTFIX_UNARY_OPERATORS = [\"%\"];\nconst OPERATORS = \"+,-,*,/,:,=,<>,>=,>,<=,<,^,&\".split(\",\").concat(POSTFIX_UNARY_OPERATORS);\nfunction tokenize(str, locale = DEFAULT_LOCALE) {\n    str = replaceNewLines(str);\n    const chars = new TokenizingChars(str);\n    const result = [];\n    while (!chars.isOver()) {\n        let token = tokenizeSpace(chars) ||\n            tokenizeArgsSeparator(chars, locale) ||\n            tokenizeParenthesis(chars) ||\n            tokenizeOperator(chars) ||\n            tokenizeString(chars) ||\n            tokenizeDebugger(chars) ||\n            tokenizeInvalidRange(chars) ||\n            tokenizeNumber(chars, locale) ||\n            tokenizeSymbol(chars);\n        if (!token) {\n            token = { type: \"UNKNOWN\", value: chars.shift() };\n        }\n        result.push(token);\n    }\n    return result;\n}\nfunction tokenizeDebugger(chars) {\n    if (chars.current === \"?\") {\n        chars.shift();\n        return { type: \"DEBUGGER\", value: \"?\" };\n    }\n    return null;\n}\nconst parenthesis = {\n    \"(\": { type: \"LEFT_PAREN\", value: \"(\" },\n    \")\": { type: \"RIGHT_PAREN\", value: \")\" },\n};\nfunction tokenizeParenthesis(chars) {\n    if (chars.current === \"(\" || chars.current === \")\") {\n        const value = chars.shift();\n        return parenthesis[value];\n    }\n    return null;\n}\nfunction tokenizeArgsSeparator(chars, locale) {\n    if (chars.current === locale.formulaArgSeparator) {\n        const value = chars.shift();\n        const type = \"ARG_SEPARATOR\";\n        return { type, value };\n    }\n    return null;\n}\nfunction tokenizeOperator(chars) {\n    for (let op of OPERATORS) {\n        if (chars.currentStartsWith(op)) {\n            chars.advanceBy(op.length);\n            return { type: \"OPERATOR\", value: op };\n        }\n    }\n    return null;\n}\nconst FIRST_POSSIBLE_NUMBER_CHARS = new Set(\"0123456789\");\nfunction tokenizeNumber(chars, locale) {\n    if (!FIRST_POSSIBLE_NUMBER_CHARS.has(chars.current) &&\n        chars.current !== locale.decimalSeparator) {\n        return null;\n    }\n    const match = chars.remaining().match(getFormulaNumberRegex(locale.decimalSeparator));\n    if (match) {\n        chars.advanceBy(match[0].length);\n        return { type: \"NUMBER\", value: match[0] };\n    }\n    return null;\n}\nfunction tokenizeString(chars) {\n    if (chars.current === '\"') {\n        const startChar = chars.shift();\n        let letters = startChar;\n        while (chars.current && (chars.current !== startChar || letters[letters.length - 1] === \"\\\\\")) {\n            letters += chars.shift();\n        }\n        if (chars.current === '\"') {\n            letters += chars.shift();\n        }\n        return {\n            type: \"STRING\",\n            value: letters,\n        };\n    }\n    return null;\n}\nconst SYMBOL_CHARS = new Set(\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.!$\");\n/**\n * A \"Symbol\" is just basically any word-like element that can appear in a\n * formula, which is not a string. So:\n *   A1\n *   SUM\n *   CEILING.MATH\n *   A$1\n *   Sheet2!A2\n *   'Sheet 2'!A2\n *\n * are examples of symbols\n */\nfunction tokenizeSymbol(chars) {\n    let result = \"\";\n    // there are two main cases to manage: either something which starts with\n    // a ', like 'Sheet 2'A2, or a word-like element.\n    if (chars.current === \"'\") {\n        let lastChar = chars.shift();\n        result += lastChar;\n        while (chars.current) {\n            lastChar = chars.shift();\n            result += lastChar;\n            if (lastChar === \"'\") {\n                if (chars.current && chars.current === \"'\") {\n                    lastChar = chars.shift();\n                    result += lastChar;\n                }\n                else {\n                    break;\n                }\n            }\n        }\n        if (lastChar !== \"'\") {\n            return {\n                type: \"UNKNOWN\",\n                value: result,\n            };\n        }\n    }\n    while (chars.current && SYMBOL_CHARS.has(chars.current)) {\n        result += chars.shift();\n    }\n    if (result.length) {\n        const value = result;\n        const isReference = rangeReference.test(value);\n        if (isReference) {\n            return { type: \"REFERENCE\", value };\n        }\n        return { type: \"SYMBOL\", value };\n    }\n    return null;\n}\nfunction tokenizeSpace(chars) {\n    let length = 0;\n    while (chars.current === NEWLINE) {\n        length++;\n        chars.shift();\n    }\n    if (length) {\n        return { type: \"SPACE\", value: NEWLINE.repeat(length) };\n    }\n    let spaces = \"\";\n    while (chars.current && chars.current.match(whiteSpaceRegexp)) {\n        spaces += chars.shift();\n    }\n    if (spaces) {\n        return { type: \"SPACE\", value: spaces };\n    }\n    return null;\n}\nfunction tokenizeInvalidRange(chars) {\n    if (chars.currentStartsWith(CellErrorType.InvalidReference)) {\n        chars.advanceBy(CellErrorType.InvalidReference.length);\n        return { type: \"INVALID_REFERENCE\", value: CellErrorType.InvalidReference };\n    }\n    return null;\n}\n\nfunction isValidLocale(locale) {\n    if (!locale ||\n        typeof locale !== \"object\" ||\n        !(!locale.thousandsSeparator || typeof locale.thousandsSeparator === \"string\")) {\n        return false;\n    }\n    for (const property of [\n        \"code\",\n        \"name\",\n        \"decimalSeparator\",\n        \"dateFormat\",\n        \"timeFormat\",\n        \"formulaArgSeparator\",\n    ]) {\n        if (!locale[property] || typeof locale[property] !== \"string\") {\n            return false;\n        }\n    }\n    if (locale.formulaArgSeparator === locale.decimalSeparator) {\n        return false;\n    }\n    if (locale.thousandsSeparator === locale.decimalSeparator) {\n        return false;\n    }\n    try {\n        formatValue(1, { locale, format: \"#,##0.00\" });\n        formatValue(1, { locale, format: locale.dateFormat });\n        formatValue(1, { locale, format: locale.timeFormat });\n    }\n    catch {\n        return false;\n    }\n    return true;\n}\n/**\n * Change a content string from the given locale to its canonical form (en_US locale). Don't convert date string.\n *\n * @example\n * canonicalizeNumberContent(\"=SUM(1,5; 02/12/2012)\", FR_LOCALE) // \"=SUM(1.5, 02/12/2012)\"\n * canonicalizeNumberContent(\"125,9\", FR_LOCALE) // \"125.9\"\n * canonicalizeNumberContent(\"02/12/2012\", FR_LOCALE) // \"02/12/2012\"\n */\nfunction canonicalizeNumberContent(content, locale) {\n    return content.startsWith(\"=\")\n        ? canonicalizeFormula$1(content, locale)\n        : canonicalizeNumberLiteral(content, locale);\n}\n/**\n * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.\n * This is destructive and won't preserve the original format.\n *\n * @example\n * canonicalizeContent(\"=SUM(1,5; 5)\", FR_LOCALE) // \"=SUM(1.5, 5)\"\n * canonicalizeContent(\"125,9\", FR_LOCALE) // \"125.9\"\n * canonicalizeContent(\"02/12/2012\", FR_LOCALE) // \"12/02/2012\"\n * canonicalizeContent(\"02-12-2012\", FR_LOCALE) // \"12/02/2012\"\n */\nfunction canonicalizeContent(content, locale) {\n    return content.startsWith(\"=\")\n        ? canonicalizeFormula$1(content, locale)\n        : canonicalizeLiteral(content, locale);\n}\n/**\n * Change a content string from its canonical form (en_US locale) to the given locale. Also convert date string.\n *\n * @example\n * localizeContent(\"=SUM(1.5, 5)\", FR_LOCALE) // \"=SUM(1,5; 5)\"\n * localizeContent(\"125.9\", FR_LOCALE) // \"125,9\"\n * localizeContent(\"12/02/2012\", FR_LOCALE) // \"02/12/2012\"\n */\nfunction localizeContent(content, locale) {\n    return content.startsWith(\"=\")\n        ? localizeFormula(content, locale)\n        : localizeLiteral(content, locale);\n}\n/** Change a formula to its canonical form (en_US locale) */\nfunction canonicalizeFormula$1(formula, locale) {\n    return _localizeFormula$1(formula, locale, DEFAULT_LOCALE);\n}\n/** Change a formula from the canonical form to the given locale */\nfunction localizeFormula(formula, locale) {\n    return _localizeFormula$1(formula, DEFAULT_LOCALE, locale);\n}\nfunction _localizeFormula$1(formula, fromLocale, toLocale) {\n    if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&\n        fromLocale.decimalSeparator === toLocale.decimalSeparator) {\n        return formula;\n    }\n    const tokens = tokenize(formula, fromLocale);\n    let localizedFormula = \"\";\n    for (const token of tokens) {\n        if (token.type === \"NUMBER\") {\n            localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);\n        }\n        else if (token.type === \"ARG_SEPARATOR\") {\n            localizedFormula += toLocale.formulaArgSeparator;\n        }\n        else {\n            localizedFormula += token.value;\n        }\n    }\n    return localizedFormula;\n}\n/**\n * Change a literal string from the given locale to its canonical form (en_US locale). Don't convert date string.\n *\n * @example\n * canonicalizeNumberLiteral(\"125,9\", FR_LOCALE) // \"125.9\"\n * canonicalizeNumberLiteral(\"02/12/2012\", FR_LOCALE) // \"02/12/2012\"\n */\nfunction canonicalizeNumberLiteral(content, locale) {\n    if (locale.decimalSeparator === \".\" || !isNumber(content, locale)) {\n        return content;\n    }\n    if (locale.thousandsSeparator) {\n        content = content.replaceAll(locale.thousandsSeparator, \"\");\n    }\n    return content.replace(locale.decimalSeparator, \".\");\n}\n/**\n * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.\n * This is destructive and won't preserve the original format.\n *\n * @example\n * canonicalizeLiteral(\"125,9\", FR_LOCALE) // \"125.9\"\n * canonicalizeLiteral(\"02/12/2012\", FR_LOCALE) // \"12/02/2012\"\n * canonicalizeLiteral(\"02-12-2012\", FR_LOCALE) // \"12/02/2012\"\n */\nfunction canonicalizeLiteral(content, locale) {\n    if (isDateTime(content, locale)) {\n        const dateNumber = toNumber(content, locale);\n        let format = DEFAULT_LOCALE.dateFormat;\n        if (!Number.isInteger(dateNumber)) {\n            format += \" \" + DEFAULT_LOCALE.timeFormat;\n        }\n        return formatValue(dateNumber, { locale: DEFAULT_LOCALE, format });\n    }\n    return canonicalizeNumberLiteral(content, locale);\n}\n/**\n * Change a literal string from its canonical form (en_US locale) to the given locale. Don't convert date string.\n * This is destructive and won't preserve the original format.\n *\n * @example\n * localizeNumberLiteral(\"125.9\", FR_LOCALE) // \"125,9\"\n * localizeNumberLiteral(\"12/02/2012\", FR_LOCALE) // \"12/02/2012\"\n * localizeNumberLiteral(\"12-02-2012\", FR_LOCALE) // \"12/02/2012\"\n */\nfunction localizeNumberLiteral(literal, locale) {\n    if (locale.decimalSeparator === \".\" || !isNumber(literal, DEFAULT_LOCALE)) {\n        return literal;\n    }\n    const decimalNumberRegex = getDecimalNumberRegex(DEFAULT_LOCALE);\n    const localized = literal.replace(decimalNumberRegex, (match) => {\n        return match.replace(\".\", locale.decimalSeparator);\n    });\n    return localized;\n}\n/**\n * Change a literal string from its canonical form (en_US locale) to the given locale. Also convert date string.\n *\n * @example\n * localizeLiteral(\"125.9\", FR_LOCALE) // \"125,9\"\n * localizeLiteral(\"12/02/2012\", FR_LOCALE) // \"02/12/2012\"\n */\nfunction localizeLiteral(literal, locale) {\n    if (isDateTime(literal, DEFAULT_LOCALE)) {\n        const dateNumber = toNumber(literal, DEFAULT_LOCALE);\n        let format = locale.dateFormat;\n        if (!Number.isInteger(dateNumber)) {\n            format += \" \" + locale.timeFormat;\n        }\n        return formatValue(dateNumber, { locale, format });\n    }\n    return localizeNumberLiteral(literal, locale);\n}\nfunction canonicalizeCFRule(cf, locale) {\n    return changeCFRuleLocale(cf, (content) => canonicalizeContent(content, locale));\n}\nfunction localizeCFRule(cf, locale) {\n    return changeCFRuleLocale(cf, (content) => localizeContent(content, locale));\n}\nfunction localizeDataValidationRule(rule, locale) {\n    const localizedDVRule = deepCopy(rule);\n    localizedDVRule.criterion.values = localizedDVRule.criterion.values.map((content) => localizeContent(content, locale));\n    return localizedDVRule;\n}\nfunction changeCFRuleLocale(rule, changeContentLocale) {\n    rule = deepCopy(rule);\n    switch (rule.type) {\n        case \"CellIsRule\":\n            // Only change value for number operators\n            switch (rule.operator) {\n                case \"Between\":\n                case \"NotBetween\":\n                case \"Equal\":\n                case \"NotEqual\":\n                case \"GreaterThan\":\n                case \"GreaterThanOrEqual\":\n                case \"LessThan\":\n                case \"LessThanOrEqual\":\n                    rule.values = rule.values.map((v) => changeContentLocale(v));\n                    return rule;\n                case \"BeginsWith\":\n                case \"ContainsText\":\n                case \"EndsWith\":\n                case \"NotContains\":\n                case \"IsEmpty\":\n                case \"IsNotEmpty\":\n                    return rule;\n            }\n        case \"DataBarRule\":\n            return rule;\n        case \"ColorScaleRule\":\n            rule.minimum = changeCFRuleThresholdLocale(rule.minimum, changeContentLocale);\n            rule.maximum = changeCFRuleThresholdLocale(rule.maximum, changeContentLocale);\n            if (rule.midpoint) {\n                rule.midpoint = changeCFRuleThresholdLocale(rule.midpoint, changeContentLocale);\n            }\n            return rule;\n        case \"IconSetRule\":\n            rule.lowerInflectionPoint.value = changeContentLocale(rule.lowerInflectionPoint.value);\n            rule.upperInflectionPoint.value = changeContentLocale(rule.upperInflectionPoint.value);\n            return rule;\n    }\n}\nfunction changeCFRuleThresholdLocale(threshold, changeContentLocale) {\n    if (!threshold?.value) {\n        return threshold;\n    }\n    const value = threshold.type === \"formula\" ? \"=\" + threshold.value : threshold.value;\n    const modified = changeContentLocale(value);\n    const newValue = threshold.type === \"formula\" ? modified.slice(1) : modified;\n    return { ...threshold, value: newValue };\n}\nfunction getDateTimeFormat(locale) {\n    return locale.dateFormat + \" \" + locale.timeFormat;\n}\n\n/** Change a number string to its canonical form (en_US locale) */\nfunction canonicalizeNumberValue(content, locale) {\n    return content.startsWith(\"=\")\n        ? canonicalizeFormula(content, locale)\n        : canonicalizeNumberLiteral(content, locale);\n}\n/** Change a formula to its canonical form (en_US locale) */\nfunction canonicalizeFormula(formula, locale) {\n    return _localizeFormula(formula, locale, DEFAULT_LOCALE);\n}\nfunction _localizeFormula(formula, fromLocale, toLocale) {\n    if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&\n        fromLocale.decimalSeparator === toLocale.decimalSeparator) {\n        return formula;\n    }\n    const tokens = tokenize(formula, fromLocale);\n    let localizedFormula = \"\";\n    for (const token of tokens) {\n        if (token.type === \"NUMBER\") {\n            localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);\n        }\n        else if (token.type === \"ARG_SEPARATOR\") {\n            localizedFormula += toLocale.formulaArgSeparator;\n        }\n        else {\n            localizedFormula += token.value;\n        }\n    }\n    return localizedFormula;\n}\n\nfunction boolAnd(args) {\n    let foundBoolean = false;\n    let acc = true;\n    conditionalVisitBoolean(args, (arg) => {\n        foundBoolean = true;\n        acc = acc && arg;\n        return acc;\n    });\n    return {\n        foundBoolean,\n        result: acc,\n    };\n}\nfunction boolOr(args) {\n    let foundBoolean = false;\n    let acc = false;\n    conditionalVisitBoolean(args, (arg) => {\n        foundBoolean = true;\n        acc = acc || arg;\n        return !acc;\n    });\n    return {\n        foundBoolean,\n        result: acc,\n    };\n}\n\nfunction sum(values, locale) {\n    return reduceNumbers(values, (acc, a) => acc + a, 0, locale);\n}\nfunction countUnique(args) {\n    return reduceAny(args, (acc, a) => (isDataNonEmpty(a) ? acc.add(a?.value) : acc), new Set()).size;\n}\n\nfunction getUnitMatrix(n) {\n    const matrix = Array(n);\n    for (let i = 0; i < n; i++) {\n        matrix[i] = Array(n).fill(0);\n        matrix[i][i] = 1;\n    }\n    return matrix;\n}\n/**\n * Invert a matrix and compute its determinant using Gaussian Elimination.\n *\n * The Matrix should be a square matrix, and should be indexed [col][row] instead of the\n * standard mathematical indexing [row][col].\n */\nfunction invertMatrix(M) {\n    // Use Gaussian Elimination to calculate the inverse:\n    // (1) 'augment' the matrix (left) by the identity (on the right)\n    // (2) Turn the matrix on the left into the identity using elementary row operations\n    // (3) The matrix on the right becomes the inverse (was the identity matrix)\n    //\n    // There are 3 elementary row operations:\n    // (a) Swap 2 rows. This multiply the determinant by -1.\n    // (b) Multiply a row by a scalar. This multiply the determinant by that scalar.\n    // (c) Add to a row a multiple of another row. This does not change the determinant.\n    if (M.length !== M[0].length) {\n        throw new EvaluationError(_t(\"Function [[FUNCTION_NAME]] invert matrix error, only square matrices are invertible\"));\n    }\n    let determinant = 1;\n    const dim = M.length;\n    const I = getUnitMatrix(dim);\n    const C = M.map((row) => row.slice());\n    // Perform elementary row operations\n    for (let pivot = 0; pivot < dim; pivot++) {\n        let diagonalElement = C[pivot][pivot];\n        // if we have a 0 on the diagonal we'll need to swap with a lower row\n        if (diagonalElement === 0) {\n            //look through every row below the i'th row\n            for (let row = pivot + 1; row < dim; row++) {\n                //if the ii'th row has a non-0 in the i'th col, swap it with that row\n                if (C[pivot][row] != 0) {\n                    swapMatrixRows(C, pivot, row);\n                    swapMatrixRows(I, pivot, row);\n                    determinant *= -1;\n                    break;\n                }\n            }\n            diagonalElement = C[pivot][pivot];\n            //if it's still 0, matrix isn't invertible\n            if (diagonalElement === 0) {\n                return { determinant: 0 };\n            }\n        }\n        // Scale this row down by e (so we have a 1 on the diagonal)\n        for (let col = 0; col < dim; col++) {\n            C[col][pivot] = C[col][pivot] / diagonalElement;\n            I[col][pivot] = I[col][pivot] / diagonalElement;\n        }\n        determinant *= diagonalElement;\n        // Subtract a multiple of the current row from ALL of\n        // the other rows so that there will be 0's in this column in the\n        // rows above and below this one\n        for (let row = 0; row < dim; row++) {\n            if (row === pivot) {\n                continue;\n            }\n            // We want to change this element to 0\n            const e = C[pivot][row];\n            // Subtract (the row above(or below) scaled by e) from (the\n            // current row) but start at the i'th column and assume all the\n            // stuff left of diagonal is 0 (which it should be if we made this\n            // algorithm correctly)\n            for (let col = 0; col < dim; col++) {\n                C[col][row] -= e * C[col][pivot];\n                I[col][row] -= e * I[col][pivot];\n            }\n        }\n    }\n    // We've done all operations, C should be the identity matrix I should be the inverse\n    return { inverted: I, determinant };\n}\nfunction swapMatrixRows(matrix, row1, row2) {\n    for (let i = 0; i < matrix.length; i++) {\n        const tmp = matrix[i][row1];\n        matrix[i][row1] = matrix[i][row2];\n        matrix[i][row2] = tmp;\n    }\n}\n/**\n * Matrix multiplication of 2 matrices.\n * ex: matrix1 : n x l, matrix2 : m x n => result : m x l\n *\n * Note: we use indexing [col][row] instead of the standard mathematical notation [row][col]\n */\nfunction multiplyMatrices(matrix1, matrix2) {\n    if (matrix1.length !== matrix2[0].length) {\n        throw new EvaluationError(_t(\"Cannot multiply matrices : incompatible matrices size.\"));\n    }\n    const rowsM1 = matrix1[0].length;\n    const colsM2 = matrix2.length;\n    const n = matrix1.length;\n    const result = Array(colsM2);\n    for (let col = 0; col < colsM2; col++) {\n        result[col] = Array(rowsM1);\n        for (let row = 0; row < rowsM1; row++) {\n            let sum = 0;\n            for (let k = 0; k < n; k++) {\n                sum += matrix1[k][row] * matrix2[col][k];\n            }\n            result[col][row] = sum;\n        }\n    }\n    return result;\n}\n/**\n * Return the input if it's a scalar or the first element of the input if it's a matrix.\n */\nfunction toScalar(matrix) {\n    if (!isMatrix(matrix)) {\n        return matrix;\n    }\n    if (matrix.length !== 1 || matrix[0].length !== 1) {\n        throw new EvaluationError(_t(\"The value should be a scalar or a 1x1 matrix\"));\n    }\n    return matrix[0][0];\n}\n\nfunction assertSameNumberOfElements(...args) {\n    const dims = args[0].length;\n    args.forEach((arg, i) => assert(() => arg.length === dims, _t(\"[[FUNCTION_NAME]] has mismatched dimensions for argument %s (%s vs %s).\", i.toString(), dims.toString(), arg.length.toString())));\n}\nfunction average(values, locale) {\n    let count = 0;\n    const sum = reduceNumbers(values, (acc, a) => {\n        count += 1;\n        return acc + a;\n    }, 0, locale);\n    assertNotZero(count);\n    return sum / count;\n}\nfunction countNumbers(values, locale) {\n    let count = 0;\n    for (let n of values) {\n        if (isMatrix(n)) {\n            for (let i of n) {\n                for (let j of i) {\n                    if (typeof j.value === \"number\") {\n                        count += 1;\n                    }\n                }\n            }\n        }\n        else {\n            const value = n?.value;\n            if (!isEvaluationError(value) &&\n                (typeof value !== \"string\" || isNumber(value, locale) || parseDateTime(value, locale))) {\n                count += 1;\n            }\n        }\n    }\n    return count;\n}\nfunction countAny(values) {\n    return reduceAny(values, (acc, a) => (a !== undefined && a.value !== null ? acc + 1 : acc), 0);\n}\nfunction max(values, locale) {\n    let max = { value: -Infinity };\n    visitNumbers(values, (a) => {\n        if (a.value >= max.value) {\n            max = a;\n        }\n    }, locale);\n    return max.value === -Infinity ? { value: 0 } : max;\n}\nfunction min(values, locale) {\n    let min = { value: Infinity };\n    visitNumbers(values, (a) => {\n        if (a.value <= min.value) {\n            min = a;\n        }\n    }, locale);\n    return min.value === Infinity ? { value: 0 } : min;\n}\nfunction prepareDataForRegression(X, Y, newX) {\n    const _X = X[0].length ? X : [range(1, Y.flat().length + 1)];\n    const nVar = _X.length;\n    let _newX = newX[0].length ? newX : _X;\n    _newX = _newX.length === nVar ? transposeMatrix(_newX) : _newX;\n    return { _X, _newX };\n}\n/*\n * This function performs a linear regression on the data set. It returns an array with two elements.\n * The first element is the slope, and the second element is the intercept.\n * The linear regression line is: y = slope*x + intercept\n * The function use the least squares method to find the best fit for the data set :\n * see https://www.mathsisfun.com/data/least-squares-regression.html\n *     https://www.statology.org/standard-error-of-estimate/\n *     https://agronomy4future.org/?p=16670\n *     https://vitalflux.com/interpreting-f-statistics-in-linear-regression-formula-examples/\n *     https://web.ist.utl.pt/~ist11038/compute/errtheory/,regression/regrthroughorigin.pdf\n */\nfunction fullLinearRegression(X, Y, computeIntercept = true, verbose = false) {\n    const y = Y.flat();\n    const n = y.length;\n    let { _X } = prepareDataForRegression(X, Y, [[]]);\n    _X = _X.length === n ? transposeMatrix(_X) : _X.slice();\n    assertSameNumberOfElements(_X[0], y);\n    const nVar = _X.length;\n    const nDeg = n - nVar - (computeIntercept ? 1 : 0);\n    const yMatrix = [y];\n    const xMatrix = transposeMatrix(_X.reverse());\n    let avgX = [];\n    for (let i = 0; i < nVar; i++) {\n        avgX.push(0);\n        if (computeIntercept) {\n            for (const xij of _X[i]) {\n                avgX[i] += xij;\n            }\n            avgX[i] /= n;\n        }\n    }\n    let avgY = 0;\n    if (computeIntercept) {\n        for (const yi of y) {\n            avgY += yi;\n        }\n        avgY /= n;\n    }\n    const redX = xMatrix.map((row) => row.map((value, i) => value - avgX[i]));\n    if (computeIntercept) {\n        xMatrix.forEach((row) => row.push(1));\n    }\n    const coeffs = getLMSCoefficients(xMatrix, yMatrix);\n    if (!computeIntercept) {\n        coeffs.push([0]);\n    }\n    if (!verbose) {\n        return coeffs;\n    }\n    const dot1 = multiplyMatrices(redX, transposeMatrix(redX));\n    const { inverted: dotInv } = invertMatrix(dot1);\n    if (dotInv === undefined) {\n        throw new EvaluationError(_t(\"Matrix is not invertible\"));\n    }\n    let SSE = 0, SSR = 0;\n    for (let i = 0; i < n; i++) {\n        const yi = y[i] - avgY;\n        let temp = 0;\n        for (let j = 0; j < nVar; j++) {\n            const xi = redX[i][j];\n            temp += xi * coeffs[j][0];\n        }\n        const ei = yi - temp;\n        SSE += ei * ei;\n        SSR += temp * temp;\n    }\n    const RMSE = Math.sqrt(SSE / nDeg);\n    const r2 = SSR / (SSR + SSE);\n    const f_stat = SSR / nVar / (SSE / nDeg);\n    const deltaCoeffs = [];\n    for (let i = 0; i < nVar; i++) {\n        deltaCoeffs.push(RMSE * Math.sqrt(dotInv[i][i]));\n    }\n    if (computeIntercept) {\n        const dot2 = multiplyMatrices(dotInv, [avgX]);\n        const dot3 = multiplyMatrices(transposeMatrix([avgX]), dot2);\n        deltaCoeffs.push(RMSE * Math.sqrt(dot3[0][0] + 1 / y.length));\n    }\n    const returned = [\n        [coeffs[0][0], deltaCoeffs[0], r2, f_stat, SSR],\n        [coeffs[1][0], deltaCoeffs[1], RMSE, nDeg, SSE],\n    ];\n    for (let i = 2; i < nVar; i++) {\n        returned.push([coeffs[i][0], deltaCoeffs[i], \"\", \"\", \"\"]);\n    }\n    if (computeIntercept) {\n        returned.push([coeffs[nVar][0], deltaCoeffs[nVar], \"\", \"\", \"\"]);\n    }\n    else {\n        returned.push([0, \"\", \"\", \"\", \"\"]);\n    }\n    return returned;\n}\n/*\n  This function performs a polynomial regression on the data set. It returns the coefficients of\n  the polynomial function that best fits the data set.\n  The polynomial function is: y = c0 + c1*x + c2*x^2 + ... + cn*x^n, where n is the order (degree)\n  of the polynomial. The returned coefficients are then in the form: [c0, c1, c2, ..., cn]\n  The function is based on the method of least squares :\n  see: https://mathworld.wolfram.com/LeastSquaresFittingPolynomial.html\n*/\nfunction polynomialRegression(flatY, flatX, order, intercept) {\n    assertSameNumberOfElements(flatX, flatY);\n    assert(() => order >= 1, _t(\"Function [[FUNCTION_NAME]] A regression of order less than 1 cannot be possible.\"));\n    const yMatrix = [flatY];\n    const xMatrix = flatX.map((x) => range(0, order).map((i) => Math.pow(x, order - i)));\n    if (intercept) {\n        xMatrix.forEach((row) => row.push(1));\n    }\n    const coeffs = getLMSCoefficients(xMatrix, yMatrix);\n    if (!intercept) {\n        coeffs.push([0]);\n    }\n    return coeffs;\n}\nfunction getLMSCoefficients(xMatrix, yMatrix) {\n    const xMatrixT = transposeMatrix(xMatrix);\n    const dot1 = multiplyMatrices(xMatrix, xMatrixT);\n    const { inverted: dotInv } = invertMatrix(dot1);\n    if (dotInv === undefined) {\n        throw new EvaluationError(_t(\"Matrix is not invertible\"));\n    }\n    const dot2 = multiplyMatrices(xMatrix, yMatrix);\n    return transposeMatrix(multiplyMatrices(dotInv, dot2));\n}\nfunction evaluatePolynomial(coeffs, x, order) {\n    return coeffs.reduce((acc, coeff, i) => acc + coeff * Math.pow(x, order - i), 0);\n}\nfunction expM(M) {\n    return M.map((col) => col.map((cell) => Math.exp(cell)));\n}\nfunction logM(M) {\n    return M.map((col) => col.map((cell) => Math.log(cell)));\n}\nfunction predictLinearValues(Y, X, newX, computeIntercept) {\n    const { _X, _newX } = prepareDataForRegression(X, Y, newX);\n    const coeffs = fullLinearRegression(_X, Y, computeIntercept, false);\n    const nVar = coeffs.length - 1;\n    const newY = _newX.map((col) => {\n        let value = 0;\n        for (let i = 0; i < nVar; i++) {\n            value += coeffs[i][0] * col[nVar - i - 1];\n        }\n        value += coeffs[nVar][0];\n        return [value];\n    });\n    return newY.length === newX.length ? newY : transposeMatrix(newY);\n}\n\nconst PREVIOUS_VALUE = \"(previous)\";\nconst NEXT_VALUE = \"(next)\";\nfunction getDomainOfParentRow(pivot, domain) {\n    const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);\n    return [...colDomain, ...rowDomain.slice(0, rowDomain.length - 1)];\n}\nfunction getDomainOfParentCol(pivot, domain) {\n    const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);\n    return [...colDomain.slice(0, colDomain.length - 1), ...rowDomain];\n}\n/**\n * Split a pivot domain into the part related to the rows of the pivot, and the part related to the columns.\n */\nfunction domainToColRowDomain(pivot, domain) {\n    const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);\n    const rowDomain = domain.filter((node) => rowFields.includes(node.field));\n    const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);\n    const colDomain = domain.filter((node) => columnFields.includes(node.field));\n    return { colDomain, rowDomain };\n}\nfunction getDimensionDomain(pivot, dimension, domain) {\n    return dimension === \"column\"\n        ? domainToColRowDomain(pivot, domain).colDomain\n        : domainToColRowDomain(pivot, domain).rowDomain;\n}\nfunction getFieldValueInDomain(fieldNameWithGranularity, domain) {\n    const node = domain.find((n) => n.field === fieldNameWithGranularity);\n    return node?.value;\n}\nfunction isDomainIsInPivot(pivot, domain) {\n    const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);\n    return (checkIfDomainInInTree(rowDomain, pivot.getTableStructure().getRowTree()) &&\n        checkIfDomainInInTree(colDomain, pivot.getTableStructure().getColTree()));\n}\nfunction checkIfDomainInInTree(domain, tree) {\n    return walkDomainTree(domain, tree) !== undefined;\n}\n/**\n * Given a tree of the col/rows of a pivot, and a domain related to those col/rows, return the node of the tree\n * corresponding to the domain.\n *\n * @param domain The domain to find in the tree\n * @param tree The tree to search in7\n * @param stopAtField If provided, the search will stop at the field with this name\n */\nfunction walkDomainTree(domain, tree, stopAtField) {\n    let currentTreeNode = tree;\n    for (const node of domain) {\n        const child = currentTreeNode.find((n) => n.value === node.value);\n        if (!child) {\n            return undefined;\n        }\n        if (child.field === stopAtField) {\n            return currentTreeNode;\n        }\n        currentTreeNode = child.children;\n    }\n    return currentTreeNode;\n}\n/**\n * Get the domain parent of the given domain with the field `parentFieldName` as leaf of the domain.\n *\n * In practice, if the `parentFieldName` is a row in the pivot, the helper will return a domain with the same column\n * domain, and with a row domain all groupBys children to `parentFieldName` removed.\n */\nfunction getFieldParentDomain(pivot, parentFieldName, domain) {\n    let { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);\n    const dimension = getFieldDimensionType(pivot, parentFieldName);\n    if (dimension === \"row\") {\n        const index = rowDomain.findIndex((node) => node.field === parentFieldName);\n        if (index === -1) {\n            return domain;\n        }\n        rowDomain = rowDomain.slice(0, index + 1);\n    }\n    else {\n        const index = colDomain.findIndex((node) => node.field === parentFieldName);\n        if (index === -1) {\n            return domain;\n        }\n        colDomain = colDomain.slice(0, index + 1);\n    }\n    return [...rowDomain, ...colDomain];\n}\n/**\n * Replace in the domain the value of the field `fieldNameWithGranularity` with the given `value`\n */\nfunction replaceFieldValueInDomain(domain, fieldNameWithGranularity, value) {\n    domain = deepCopy(domain);\n    const node = domain.find((n) => n.field === fieldNameWithGranularity);\n    if (!node) {\n        return domain;\n    }\n    node.value = value;\n    return domain;\n}\nfunction isFieldInDomain(nameWithGranularity, domain) {\n    return domain.some((node) => node.field === nameWithGranularity);\n}\n/**\n * Check if the field is in the rows or columns of the pivot\n */\nfunction getFieldDimensionType(pivot, nameWithGranularity) {\n    const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);\n    if (rowFields.includes(nameWithGranularity)) {\n        return \"row\";\n    }\n    const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);\n    if (columnFields.includes(nameWithGranularity)) {\n        return \"column\";\n    }\n    throw new Error(`Field ${nameWithGranularity} not found in pivot`);\n}\n/**\n * Replace in the given domain the value of the field `fieldNameWithGranularity` with the previous or next value.\n */\nfunction getPreviousOrNextValueDomain(pivot, domain, fieldNameWithGranularity, direction) {\n    const dimension = getFieldDimensionType(pivot, fieldNameWithGranularity);\n    const tree = dimension === \"row\"\n        ? pivot.getTableStructure().getRowTree()\n        : pivot.getTableStructure().getColTree();\n    const dimDomain = getDimensionDomain(pivot, dimension, domain);\n    const currentTreeNode = walkDomainTree(dimDomain, tree, fieldNameWithGranularity);\n    const values = currentTreeNode?.map((n) => n.value) ?? [];\n    const value = getFieldValueInDomain(fieldNameWithGranularity, domain);\n    if (value === undefined) {\n        return undefined;\n    }\n    const valueIndex = values.indexOf(value);\n    if (value === undefined || valueIndex === -1) {\n        return undefined;\n    }\n    const offset = direction === PREVIOUS_VALUE ? -1 : 1;\n    const newIndex = clip(valueIndex + offset, 0, values.length - 1);\n    return replaceFieldValueInDomain(domain, fieldNameWithGranularity, values[newIndex]);\n}\nfunction domainToString(domain) {\n    return domain ? domain.map(domainNodeToString).join(\", \") : \"\";\n}\nfunction domainNodeToString(domainNode) {\n    return domainNode ? `${domainNode.field}=${domainNode.value}` : \"\";\n}\n/**\n *\n * For the ranking, the pivot cell values of a column (or row) at the same depth are grouped together before being sorted\n * and ranked.\n *\n * The grouping of a pivot cell is done with both the value of the domain nodes that are parent of the field\n * `fieldNameWithGranularity` and the value of the last node of the domain of the pivot cell, if it's not the field\n * `fieldNameWithGranularity`.\n *\n * For example, let's take a pivot grouped by (Date:year, Stage, User, Product), where we want to rank by \"Stage\" field.\n * The domain nodes parents of the \"Stage\" are [Date:year]. The pivot cell with domain:\n * - [Date:year=2021] is not ranked because it does not contain the \"Stage\" field\n * - [Date:year=2021, Stage=Lead] is grouped with the cells [Date:year=2021, Stage=*, User=None, Product=None],\n *      and then ranked within the group\n * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=*, User=Bob, Product=None],\n *      and then ranked within the group\n * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells [Date:year=2021, Stage=*, User=*, Product=Table],\n *      and then ranked within the group\n *\n * If we rank the pivot on \"User\" instead, the parent domain becomes [Date:year, Sage] .The cell with domain:\n * - [Date:year=2021] is not ranked because it does not contain the \"Stage\" field\n * - [Date:year=2021, Stage=Lead] is not ranked because it does not contain the \"User\" field\n * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=Lead, User=Bob, Product=None],\n *      and then ranked within the group\n * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells with [Date:year=2021, Stage=Lead, User=*, Product=Table],\n *     and then ranked within the group\n *\n */\nfunction getRankingDomainKey(domain, fieldNameWithGranularity) {\n    const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);\n    if (index === -1) {\n        return \"\";\n    }\n    const parent = domain.slice(0, index);\n    const lastNode = domain.at(-1);\n    return domainToString(lastNode.field === fieldNameWithGranularity ? parent : [...parent, lastNode]);\n}\n/**\n * The running total domain is the domain without the field `fieldNameWithGranularity`, ie. we do the running total of\n * all the pivot cells of the column that have any value for the field `fieldNameWithGranularity` and the same value for\n * the other fields.\n */\nfunction getRunningTotalDomainKey(domain, fieldNameWithGranularity) {\n    const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);\n    if (index === -1) {\n        return \"\";\n    }\n    return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);\n}\n\nconst pivotTimeAdapterRegistry = new Registry();\nfunction pivotTimeAdapter(granularity) {\n    return pivotTimeAdapterRegistry.get(granularity);\n}\n/**\n * The Time Adapter: Managing Time Periods for Pivot Functions\n *\n * Overview:\n * A time adapter is responsible for managing time periods associated with pivot functions.\n * Each type of period (day, week, month, quarter, etc.) has its own dedicated adapter.\n * The adapter's primary role is to normalize period values between spreadsheet functions,\n * and the pivot.\n * By normalizing the period value, it can be stored consistently in the pivot.\n *\n * Normalization Process:\n * When working with functions in the spreadsheet, the time adapter normalizes\n * the provided period to facilitate accurate lookup of values in the pivot.\n * For instance, if the spreadsheet function represents a day period as a number generated\n * by the DATE function (DATE(2023, 12, 25)), the time adapter will normalize it accordingly.\n *\n */\n/**\n * Normalized value: \"12/25/2023\"\n *\n * Note: Those two format are equivalent:\n * - \"MM/dd/yyyy\" (luxon format)\n * - \"mm/dd/yyyy\" (spreadsheet format)\n **/\nconst dayAdapter = {\n    normalizeFunctionValue(value) {\n        return toNumber(value, DEFAULT_LOCALE);\n    },\n    toValueAndFormat(normalizedValue, locale) {\n        return {\n            value: toNumber(normalizedValue, DEFAULT_LOCALE),\n            format: \"dd mmm yyyy\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        const date = toNumber(normalizedValue, DEFAULT_LOCALE);\n        return `\"${formatValue(date, { locale: DEFAULT_LOCALE, format: \"mm/dd/yyyy\" })}\"`;\n    },\n};\n/**\n * normalizes day of month number\n */\nconst dayOfMonthAdapter = {\n    normalizeFunctionValue(value) {\n        const day = toNumber(value, DEFAULT_LOCALE);\n        if (day < 1 || day > 31) {\n            throw new EvaluationError(_t(\"%s is not a valid day of month (it should be a number between 1 and 31)\", day));\n        }\n        return day;\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: toNumber(normalizedValue, DEFAULT_LOCALE),\n            format: \"0\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\n/**\n * normalizes day of week number\n *\n * The day of week is a bit special as it depends on the locale week start day.\n * =PIVOT.VALUE(1, \"xx:day_of_week\", 1) will be different depending on the locale\n *  - fr_FR: 1: Monday, 7: Sunday   (weekStart = 1)\n *  - en_US: 1: Sunday, 7: Saturday (weekStart = 7)\n *\n * The function that normalizes the value coming from the function\n * (`normalizeFunctionValue`) will return the day of week (1 based index)\n * depending on the locale week start day.\n * To display the value in the pivot, we need to convert it to retrieve the\n * correct day of week name (1 should be \"Monday\" in fr_FR and \"Sunday\" in en_US).\n */\nconst dayOfWeekAdapter = {\n    normalizeFunctionValue(value) {\n        const day = toNumber(value, DEFAULT_LOCALE);\n        if (day < 1 || day > 7) {\n            throw new EvaluationError(_t(\"%s is not a valid day of week (it should be a number between 1 and 7)\", day));\n        }\n        return day;\n    },\n    toValueAndFormat(normalizedValue, locale) {\n        /**\n         * As explain above, normalizedValue is the day of week (1 based index)\n         * depending on the locale week start day. To retrieve the correct day name,\n         * we need to convert it to a 0 based index with 0 being Sunday. (DAYS is\n         * an object of day names with 0 being Sunday)\n         */\n        const index = (normalizedValue - 1 + (locale || DEFAULT_LOCALE).weekStart) % 7;\n        return {\n            value: DAYS$1[index].toString(),\n            format: \"@\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\n/**\n * normalizes iso week number\n */\nconst isoWeekNumberAdapter = {\n    normalizeFunctionValue(value) {\n        const isoWeek = toNumber(value, DEFAULT_LOCALE);\n        if (isoWeek < 0 || isoWeek > 53) {\n            throw new EvaluationError(_t(\"%s is not a valid week (it should be a number between 0 and 53)\", isoWeek));\n        }\n        return isoWeek;\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: toNumber(normalizedValue, DEFAULT_LOCALE),\n            format: \"0\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\n/**\n * normalizes month number\n */\nconst monthNumberAdapter = {\n    normalizeFunctionValue(value) {\n        const month = toNumber(value, DEFAULT_LOCALE);\n        if (month < 1 || month > 12) {\n            throw new EvaluationError(_t(\"%s is not a valid month (it should be a number between 1 and 12)\", month));\n        }\n        return month;\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: MONTHS[toNumber(normalizedValue, DEFAULT_LOCALE) - 1].toString(),\n            format: \"@\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\n/**\n * normalizes quarter number\n */\nconst quarterNumberAdapter = {\n    normalizeFunctionValue(value) {\n        const quarter = toNumber(value, DEFAULT_LOCALE);\n        if (quarter < 1 || quarter > 4) {\n            throw new EvaluationError(_t(\"%s is not a valid quarter (it should be a number between 1 and 4)\", quarter));\n        }\n        return quarter;\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: _t(\"Q%(quarter_number)s\", { quarter_number: normalizedValue }),\n            format: \"@\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\nconst yearAdapter = {\n    normalizeFunctionValue(value) {\n        return toNumber(value, DEFAULT_LOCALE);\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: toNumber(normalizedValue, DEFAULT_LOCALE),\n            format: \"0\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\n/**\n * normalizes hour number\n */\nconst hourNumberAdapter = {\n    normalizeFunctionValue(value) {\n        const hour = toNumber(value, DEFAULT_LOCALE);\n        if (hour < 0 || hour > 23) {\n            throw new EvaluationError(_t(\"%s is not a valid hour (it should be a number between 0 and 23)\", hour));\n        }\n        return hour;\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: _t(\"%(hour_number)sh\", { hour_number: normalizedValue }),\n            format: \"@\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\n/**\n * normalizes hour number\n */\nconst minuteNumberAdapter = {\n    normalizeFunctionValue(value) {\n        const minute = toNumber(value, DEFAULT_LOCALE);\n        if (minute < 0 || minute > 59) {\n            throw new EvaluationError(_t(\"%s is not a valid minute (it should be a number between 0 and 59)\", minute));\n        }\n        return minute;\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: _t(\"%(minute_number)s'\", { minute_number: normalizedValue }),\n            format: \"@\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\n/**\n * normalizes second number\n */\nconst secondNumberAdapter = {\n    normalizeFunctionValue(value) {\n        const second = toNumber(value, DEFAULT_LOCALE);\n        if (second < 0 || second > 59) {\n            throw new EvaluationError(_t(\"%s is not a valid second (it should be a number between 0 and 59)\", second));\n        }\n        return second;\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: _t(\"%(second_number)s''\", { second_number: normalizedValue }),\n            format: \"@\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `${normalizedValue}`;\n    },\n};\n/**\n * This function takes an adapter and wraps it with a null handler.\n * null value means that the value is not set.\n */\nfunction nullHandlerDecorator(adapter) {\n    return {\n        normalizeFunctionValue(value) {\n            if (value === null) {\n                return null;\n            }\n            return adapter.normalizeFunctionValue(value);\n        },\n        toValueAndFormat(normalizedValue, locale) {\n            if (normalizedValue === null) {\n                return { value: _t(\"(Undefined)\") }; //TODO Return NA ?\n            }\n            return adapter.toValueAndFormat(normalizedValue, locale);\n        },\n        toFunctionValue(normalizedValue) {\n            if (normalizedValue === null) {\n                return \"false\"; //TODO Return NA ?\n            }\n            return adapter.toFunctionValue(normalizedValue);\n        },\n    };\n}\npivotTimeAdapterRegistry\n    .add(\"day\", nullHandlerDecorator(dayAdapter))\n    .add(\"year\", nullHandlerDecorator(yearAdapter))\n    .add(\"day_of_month\", nullHandlerDecorator(dayOfMonthAdapter))\n    .add(\"iso_week_number\", nullHandlerDecorator(isoWeekNumberAdapter))\n    .add(\"month_number\", nullHandlerDecorator(monthNumberAdapter))\n    .add(\"quarter_number\", nullHandlerDecorator(quarterNumberAdapter))\n    .add(\"day_of_week\", nullHandlerDecorator(dayOfWeekAdapter))\n    .add(\"hour_number\", nullHandlerDecorator(hourNumberAdapter))\n    .add(\"minute_number\", nullHandlerDecorator(minuteNumberAdapter))\n    .add(\"second_number\", nullHandlerDecorator(secondNumberAdapter));\n\nconst AGGREGATOR_NAMES = {\n    count: _t(\"Count\"),\n    count_distinct: _t(\"Count Distinct\"),\n    bool_and: _t(\"Boolean And\"),\n    bool_or: _t(\"Boolean Or\"),\n    max: _t(\"Maximum\"),\n    min: _t(\"Minimum\"),\n    avg: _t(\"Average\"),\n    sum: _t(\"Sum\"),\n};\nconst NUMBER_CHAR_AGGREGATORS = [\"max\", \"min\", \"avg\", \"sum\", \"count_distinct\", \"count\"];\nconst AGGREGATORS_BY_FIELD_TYPE = {\n    integer: NUMBER_CHAR_AGGREGATORS,\n    char: NUMBER_CHAR_AGGREGATORS,\n    boolean: [\"count_distinct\", \"count\", \"bool_and\", \"bool_or\"],\n};\nconst AGGREGATORS = {};\nfor (const type in AGGREGATORS_BY_FIELD_TYPE) {\n    AGGREGATORS[type] = {};\n    for (const aggregator of AGGREGATORS_BY_FIELD_TYPE[type]) {\n        AGGREGATORS[type][aggregator] = AGGREGATOR_NAMES[aggregator];\n    }\n}\nconst AGGREGATORS_FN = {\n    count: (args) => ({\n        value: countAny([args]),\n        format: \"0\",\n    }),\n    count_distinct: (args) => ({\n        value: countUnique([args]),\n        format: \"0\",\n    }),\n    bool_and: (args) => ({\n        value: boolAnd([args]).result,\n    }),\n    bool_or: (args) => ({\n        value: boolOr([args]).result,\n    }),\n    max: (args, locale) => max([args], locale),\n    min: (args, locale) => min([args], locale),\n    avg: (args, locale) => ({\n        value: average([args], locale),\n        format: inferFormat(args),\n    }),\n    sum: (args, locale) => ({\n        value: sum([args], locale),\n        format: inferFormat(args),\n    }),\n};\n/**\n * Given an object of form {\"1\": {...}, \"2\": {...}, ...} get the maximum ID used\n * in this object\n * If the object has no keys, return 0\n *\n */\nfunction getMaxObjectId(o) {\n    const keys = Object.keys(o);\n    if (!keys.length) {\n        return 0;\n    }\n    const nums = keys.map((id) => parseInt(id, 10));\n    const max = Math.max(...nums);\n    return max;\n}\nconst ALL_PERIODS = {\n    year: _t(\"Year\"),\n    quarter: _t(\"Quarter & Year\"),\n    month: _t(\"Month & Year\"),\n    week: _t(\"Week & Year\"),\n    day: _t(\"Day\"),\n    quarter_number: _t(\"Quarter\"),\n    month_number: _t(\"Month\"),\n    iso_week_number: _t(\"Week\"),\n    day_of_month: _t(\"Day of Month\"),\n    day_of_week: _t(\"Day of Week\"),\n    hour_number: _t(\"Hour\"),\n    minute_number: _t(\"Minute\"),\n    second_number: _t(\"Second\"),\n};\nconst DATE_FIELDS = [\"date\", \"datetime\"];\n/**\n * Parse a dimension string into a pivot dimension definition.\n * e.g \"create_date:month\" => { name: \"create_date\", granularity: \"month\" }\n */\nfunction parseDimension(dimension) {\n    const [fieldName, granularity] = dimension.split(\":\");\n    if (granularity) {\n        return { fieldName, granularity };\n    }\n    return { fieldName };\n}\nfunction isDateOrDatetimeField(field) {\n    return DATE_FIELDS.includes(field.type);\n}\nfunction generatePivotArgs(formulaId, domain, measure) {\n    const args = [formulaId];\n    if (measure) {\n        args.push(`\"${measure}\"`);\n    }\n    for (const { field, value, type } of domain) {\n        if (field === \"measure\") {\n            args.push(`\"measure\"`, `\"${value}\"`);\n            continue;\n        }\n        const { granularity } = parseDimension(field);\n        const formattedValue = toFunctionPivotValue(value, { type, granularity });\n        args.push(`\"${field}\"`, formattedValue);\n    }\n    return args;\n}\n/**\n * Check if the fields in the domain part of\n * a pivot function are valid according to the pivot definition.\n * e.g. =PIVOT.VALUE(1,\"revenue\",\"country_id\",...,\"create_date:month\",...,\"source_id\",...)\n */\nfunction areDomainArgsFieldsValid(dimensions, definition) {\n    let argIndex = 0;\n    let definitionIndex = 0;\n    const cols = definition.columns.map((col) => col.nameWithGranularity);\n    const rows = definition.rows.map((row) => row.nameWithGranularity);\n    while (dimensions[argIndex] !== undefined && dimensions[argIndex] === rows[definitionIndex]) {\n        argIndex++;\n        definitionIndex++;\n    }\n    definitionIndex = 0;\n    while (dimensions[argIndex] !== undefined && dimensions[argIndex] === cols[definitionIndex]) {\n        argIndex++;\n        definitionIndex++;\n    }\n    return dimensions.length === argIndex;\n}\nfunction createPivotFormula(formulaId, cell) {\n    switch (cell.type) {\n        case \"HEADER\":\n            return `=PIVOT.HEADER(${generatePivotArgs(formulaId, cell.domain).join(\",\")})`;\n        case \"VALUE\":\n            return `=PIVOT.VALUE(${generatePivotArgs(formulaId, cell.domain, cell.measure).join(\",\")})`;\n        case \"MEASURE_HEADER\":\n            return `=PIVOT.HEADER(${generatePivotArgs(formulaId, [\n                ...cell.domain,\n                { field: \"measure\", value: cell.measure, type: \"char\" },\n            ]).join(\",\")})`;\n    }\n    return \"\";\n}\n/**\n * Parses the value defining a pivot group in a PIVOT formula\n * e.g. given the following formula PIVOT.VALUE(\"1\", \"stage_id\", \"42\", \"status\", \"won\"),\n * the two group values are \"42\" and \"won\".\n */\nfunction toNormalizedPivotValue(dimension, groupValue) {\n    if (groupValue === null || groupValue === \"null\") {\n        return null;\n    }\n    const groupValueString = typeof groupValue === \"boolean\"\n        ? toString(groupValue).toLocaleLowerCase()\n        : toString(groupValue);\n    if (groupValueString === \"null\") {\n        return null;\n    }\n    if (!pivotNormalizationValueRegistry.contains(dimension.type)) {\n        throw new EvaluationError(_t(\"Field %(field)s is not supported because of its type (%(type)s)\", {\n            field: dimension.displayName,\n            type: dimension.type,\n        }));\n    }\n    // represents a field which is not set (=False server side)\n    if (groupValueString.toLowerCase() === \"false\") {\n        return false;\n    }\n    const normalizer = pivotNormalizationValueRegistry.get(dimension.type);\n    return normalizer(groupValueString, dimension.granularity);\n}\nfunction normalizeDateTime(value, granularity) {\n    if (!granularity) {\n        throw new Error(\"Missing granularity\");\n    }\n    return pivotTimeAdapter(granularity).normalizeFunctionValue(value);\n}\nfunction toFunctionPivotValue(value, dimension) {\n    if (value === null) {\n        return `\"null\"`;\n    }\n    if (!pivotToFunctionValueRegistry.contains(dimension.type)) {\n        return `\"${value}\"`;\n    }\n    return pivotToFunctionValueRegistry.get(dimension.type)(value, dimension.granularity);\n}\nfunction toFunctionValueDateTime(value, granularity) {\n    if (!granularity) {\n        throw new Error(\"Missing granularity\");\n    }\n    return pivotTimeAdapter(granularity).toFunctionValue(value);\n}\nconst pivotNormalizationValueRegistry = new Registry();\npivotNormalizationValueRegistry\n    .add(\"date\", normalizeDateTime)\n    .add(\"datetime\", normalizeDateTime)\n    .add(\"integer\", (value) => toNumber(value, DEFAULT_LOCALE))\n    .add(\"boolean\", (value) => toBoolean(value))\n    .add(\"char\", (value) => toString(value));\nconst pivotToFunctionValueRegistry = new Registry();\npivotToFunctionValueRegistry\n    .add(\"date\", toFunctionValueDateTime)\n    .add(\"datetime\", toFunctionValueDateTime)\n    .add(\"integer\", (value) => `${toNumber(value, DEFAULT_LOCALE)}`)\n    .add(\"boolean\", (value) => (toBoolean(value) ? \"TRUE\" : \"FALSE\"))\n    .add(\"char\", (value) => `\"${toString(value).replace(/\"/g, '\\\\\"')}\"`);\nfunction getFieldDisplayName(field) {\n    return field.displayName + (field.granularity ? ` (${ALL_PERIODS[field.granularity]})` : \"\");\n}\nfunction addIndentAndAlignToPivotHeader(pivot, domain, functionResult) {\n    const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);\n    if (rowDomain.length === 0 && colDomain.length === 0) {\n        return functionResult;\n    }\n    if (rowDomain.length === 0 && colDomain.length > 0) {\n        return {\n            ...functionResult,\n            format: (functionResult.format || \"@\") + \"* \",\n        };\n    }\n    const indent = rowDomain.length - 1;\n    const format = functionResult.format || \"@\";\n    return {\n        ...functionResult,\n        format: `${\"    \".repeat(indent)}${format}* `,\n    };\n}\n\nclass CellClipboardHandler extends AbstractCellClipboardHandler {\n    isCutAllowed(data) {\n        if (data.zones.length !== 1) {\n            return \"WrongCutSelection\" /* CommandResult.WrongCutSelection */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    copy(data) {\n        const sheetId = data.sheetId;\n        const { clippedZones, rowsIndexes, columnsIndexes } = data;\n        const clippedCells = [];\n        const isCopyingOneCell = rowsIndexes.length == 1 && columnsIndexes.length == 1;\n        for (let row of rowsIndexes) {\n            let cellsInRow = [];\n            for (let col of columnsIndexes) {\n                const position = { col, row, sheetId };\n                let cell = this.getters.getCell(position);\n                const evaluatedCell = this.getters.getEvaluatedCell(position);\n                const pivotId = this.getters.getPivotIdFromPosition(position);\n                const spreader = this.getters.getArrayFormulaSpreadingOn(position);\n                if (pivotId && spreader) {\n                    const pivotZone = this.getters.getSpreadZone(spreader);\n                    if ((!deepEquals(spreader, position) || !isCopyingOneCell) &&\n                        pivotZone &&\n                        !data.zones.some((z) => isZoneInside(pivotZone, z))) {\n                        const pivotCell = this.getters.getPivotCellFromPosition(position);\n                        const formulaPivotId = this.getters.getPivotFormulaId(pivotId);\n                        const pivotFormula = createPivotFormula(formulaPivotId, pivotCell);\n                        cell = {\n                            id: cell?.id || \"\",\n                            style: cell?.style,\n                            format: cell?.format,\n                            content: pivotFormula,\n                            isFormula: false,\n                            parsedValue: evaluatedCell.value,\n                        };\n                    }\n                }\n                else {\n                    if (spreader && !deepEquals(spreader, position)) {\n                        const isSpreaderCopied = rowsIndexes.includes(spreader.row) && columnsIndexes.includes(spreader.col);\n                        const content = isSpreaderCopied\n                            ? \"\"\n                            : formatValue(evaluatedCell.value, { locale: this.getters.getLocale() });\n                        cell = {\n                            id: cell?.id || \"\",\n                            style: cell?.style,\n                            format: evaluatedCell.format,\n                            content,\n                            isFormula: false,\n                            parsedValue: evaluatedCell.value,\n                        };\n                    }\n                }\n                cellsInRow.push({\n                    content: cell?.content ?? \"\",\n                    style: cell?.style,\n                    format: cell?.format,\n                    tokens: cell?.isFormula\n                        ? cell.compiledFormula.tokens.map(({ value, type }) => ({ value, type }))\n                        : [],\n                    border: this.getters.getCellBorder(position) || undefined,\n                    evaluatedCell,\n                    position,\n                });\n            }\n            clippedCells.push(cellsInRow);\n        }\n        return {\n            cells: clippedCells,\n            zones: clippedZones,\n            sheetId: data.sheetId,\n        };\n    }\n    isPasteAllowed(sheetId, target, content, clipboardOptions) {\n        if (!content.cells) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        if (clipboardOptions?.isCutOperation && clipboardOptions?.pasteOption !== undefined) {\n            // cannot paste only format or only value if the previous operation is a CUT\n            return \"WrongPasteOption\" /* CommandResult.WrongPasteOption */;\n        }\n        if (target.length > 1) {\n            // cannot paste if we have a clipped zone larger than a cell and multiple\n            // zones selected\n            if (content.cells.length > 1 || content.cells[0].length > 1) {\n                return \"WrongPasteSelection\" /* CommandResult.WrongPasteSelection */;\n            }\n        }\n        const clipboardHeight = content.cells.length;\n        const clipboardWidth = content.cells[0].length;\n        for (const zone of getPasteZones(target, content.cells)) {\n            if (this.getters.doesIntersectMerge(sheetId, zone)) {\n                if (target.length > 1 ||\n                    !this.getters.isSingleCellOrMerge(sheetId, target[0]) ||\n                    clipboardHeight * clipboardWidth !== 1) {\n                    return \"WillRemoveExistingMerge\" /* CommandResult.WillRemoveExistingMerge */;\n                }\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    /**\n     * Paste the clipboard content in the given target\n     */\n    paste(target, content, options) {\n        const zones = target.zones;\n        const sheetId = target.sheetId;\n        if (!options.isCutOperation) {\n            this.pasteFromCopy(sheetId, zones, content.cells, options);\n        }\n        else {\n            this.pasteFromCut(sheetId, zones, content, options);\n        }\n    }\n    getPasteTarget(sheetId, target, content, options) {\n        const width = content.cells[0].length;\n        const height = content.cells.length;\n        if (options?.isCutOperation) {\n            return {\n                sheetId,\n                zones: [\n                    {\n                        left: target[0].left,\n                        top: target[0].top,\n                        right: target[0].left + width - 1,\n                        bottom: target[0].top + height - 1,\n                    },\n                ],\n            };\n        }\n        if (width === 1 && height === 1) {\n            return { zones: [], sheetId };\n        }\n        return { sheetId, zones: getPasteZones(target, content.cells) };\n    }\n    pasteFromCut(sheetId, target, content, options) {\n        this.clearClippedZones(content);\n        const selection = target[0];\n        this.pasteZone(sheetId, selection.left, selection.top, content.cells, options);\n        this.dispatch(\"MOVE_RANGES\", {\n            target: content.zones,\n            sheetId: content.sheetId,\n            targetSheetId: sheetId,\n            col: selection.left,\n            row: selection.top,\n        });\n    }\n    /**\n     * Clear the clipped zones: remove the cells and clear the formatting\n     */\n    clearClippedZones(content) {\n        this.dispatch(\"CLEAR_CELLS\", {\n            sheetId: content.sheetId,\n            target: content.zones,\n        });\n        this.dispatch(\"CLEAR_FORMATTING\", {\n            sheetId: content.sheetId,\n            target: content.zones,\n        });\n    }\n    pasteZone(sheetId, col, row, cells, clipboardOptions) {\n        // then, perform the actual paste operation\n        for (const [r, rowCells] of cells.entries()) {\n            for (const [c, origin] of rowCells.entries()) {\n                if (!origin) {\n                    continue;\n                }\n                const position = { col: col + c, row: row + r, sheetId };\n                this.pasteCell(origin, position, clipboardOptions);\n            }\n        }\n    }\n    /**\n     * Paste the cell at the given position to the target position\n     */\n    pasteCell(origin, target, clipboardOption) {\n        const { sheetId, col, row } = target;\n        const targetCell = this.getters.getEvaluatedCell(target);\n        const originFormat = origin?.format ?? origin.evaluatedCell.format;\n        if (clipboardOption?.pasteOption === \"asValue\") {\n            this.dispatch(\"UPDATE_CELL\", {\n                ...target,\n                content: origin.evaluatedCell.value?.toString() || \"\",\n                format: originFormat,\n            });\n            return;\n        }\n        if (clipboardOption?.pasteOption === \"onlyFormat\") {\n            this.dispatch(\"UPDATE_CELL\", {\n                ...target,\n                style: origin?.style ?? null,\n                format: originFormat ?? targetCell.format,\n            });\n            return;\n        }\n        let content = origin?.content;\n        if (origin?.tokens && origin.tokens.length > 0 && !clipboardOption?.isCutOperation) {\n            content = this.getters.getTranslatedCellFormula(sheetId, col - origin.position.col, row - origin.position.row, origin.tokens);\n        }\n        else if (origin?.tokens && origin.tokens.length > 0) {\n            content = this.getters.getFormulaMovedInSheet(origin.position.sheetId, sheetId, origin.tokens);\n        }\n        if (content !== \"\" || origin?.format || origin?.style) {\n            this.dispatch(\"UPDATE_CELL\", {\n                ...target,\n                content,\n                style: origin?.style || null,\n                format: origin?.format,\n            });\n        }\n        else if (targetCell) {\n            this.dispatch(\"CLEAR_CELL\", target);\n        }\n    }\n    convertTextToClipboardData(text) {\n        const locale = this.getters.getLocale();\n        const copiedData = {\n            cells: [],\n        };\n        const values = [];\n        let rowLength = 0;\n        for (const [i, row] of text.replace(/\\r/g, \"\").split(\"\\n\").entries()) {\n            values.push(row.split(\"\\t\"));\n            if (values[i].length > rowLength) {\n                rowLength = values[i].length;\n            }\n        }\n        for (const row of values) {\n            const cells = [];\n            for (let i = 0; i < rowLength; i++) {\n                const content = canonicalizeNumberValue(row[i] || \"\", locale);\n                cells.push({\n                    content: content,\n                    evaluatedCell: {\n                        formattedValue: content,\n                    },\n                });\n            }\n            copiedData.cells.push(cells);\n        }\n        return copiedData;\n    }\n}\n\nclass AbstractFigureClipboardHandler extends ClipboardHandler {\n    copy(data) {\n        return;\n    }\n}\n\nclass ChartClipboardHandler extends AbstractFigureClipboardHandler {\n    copy(data) {\n        const sheetId = data.sheetId;\n        const figure = this.getters.getFigure(sheetId, data.figureId);\n        if (!figure) {\n            throw new Error(`No figure for the given id: ${data.figureId}`);\n        }\n        if (figure.tag !== \"chart\") {\n            return;\n        }\n        const copiedFigure = { ...figure };\n        const chart = this.getters.getChart(data.figureId);\n        if (!chart) {\n            throw new Error(`No chart for the given id: ${data.figureId}`);\n        }\n        const copiedChart = chart.copyInSheetId(sheetId);\n        return {\n            figureId: data.figureId,\n            copiedFigure,\n            copiedChart,\n        };\n    }\n    getPasteTarget(sheetId, target, content, options) {\n        const newId = new UuidGenerator().uuidv4();\n        return { zones: [], figureId: newId, sheetId };\n    }\n    paste(target, clippedContent, options) {\n        if (!target.figureId) {\n            return;\n        }\n        const { zones, figureId } = target;\n        const sheetId = target.sheetId;\n        const numCols = this.getters.getNumberCols(sheetId);\n        const numRows = this.getters.getNumberRows(sheetId);\n        const targetX = this.getters.getColDimensions(sheetId, zones[0].left).start;\n        const targetY = this.getters.getRowDimensions(sheetId, zones[0].top).start;\n        const maxX = this.getters.getColDimensions(sheetId, numCols - 1).end;\n        const maxY = this.getters.getRowDimensions(sheetId, numRows - 1).end;\n        const { width, height } = clippedContent.copiedFigure;\n        const position = {\n            x: maxX < width ? 0 : Math.min(targetX, maxX - width),\n            y: maxY < height ? 0 : Math.min(targetY, maxY - height),\n        };\n        const copy = clippedContent.copiedChart.copyInSheetId(sheetId);\n        this.dispatch(\"CREATE_CHART\", {\n            id: figureId,\n            sheetId,\n            position,\n            size: { height, width },\n            definition: copy.getDefinition(),\n        });\n        if (options.isCutOperation) {\n            this.dispatch(\"DELETE_FIGURE\", {\n                sheetId: clippedContent.copiedChart.sheetId,\n                id: clippedContent.copiedFigure.id,\n            });\n        }\n        this.dispatch(\"SELECT_FIGURE\", { id: figureId });\n    }\n    isPasteAllowed(sheetId, target, content, option) {\n        if (target.length === 0) {\n            return \"EmptyTarget\" /* CommandResult.EmptyTarget */;\n        }\n        if (option?.pasteOption !== undefined) {\n            return \"WrongFigurePasteOption\" /* CommandResult.WrongFigurePasteOption */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n}\n\nclass ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {\n    uuidGenerator = new UuidGenerator();\n    queuedChanges = {};\n    copy(data) {\n        if (!data.zones.length) {\n            return;\n        }\n        const { rowsIndexes, columnsIndexes } = data;\n        const sheetId = data.sheetId;\n        const cfRules = [];\n        for (const row of rowsIndexes) {\n            const cfRuleInRow = [];\n            for (const col of columnsIndexes) {\n                const cfRules = Array.from(this.getters.getRulesByCell(sheetId, col, row));\n                cfRuleInRow.push({\n                    position: { col, row, sheetId },\n                    rules: cfRules,\n                });\n            }\n            cfRules.push(cfRuleInRow);\n        }\n        return { cfRules };\n    }\n    paste(target, clippedContent, options) {\n        this.queuedChanges = {};\n        if (options.pasteOption === \"asValue\") {\n            return;\n        }\n        const zones = target.zones;\n        const sheetId = target.sheetId;\n        if (!options.isCutOperation) {\n            this.pasteFromCopy(sheetId, zones, clippedContent.cfRules, options);\n        }\n        else {\n            this.pasteFromCut(sheetId, zones, clippedContent);\n        }\n        this.executeQueuedChanges();\n    }\n    pasteFromCut(sheetId, target, content) {\n        const selection = target[0];\n        this.pasteZone(sheetId, selection.left, selection.top, content.cfRules, {\n            isCutOperation: true,\n        });\n    }\n    pasteZone(sheetId, col, row, cfRules, clipboardOptions) {\n        for (const [r, rowCells] of cfRules.entries()) {\n            for (const [c, origin] of rowCells.entries()) {\n                const position = { col: col + c, row: row + r, sheetId };\n                this.pasteCf(origin, position, clipboardOptions?.isCutOperation);\n            }\n        }\n    }\n    pasteCf(origin, target, isCutOperation) {\n        if (origin?.rules && origin.rules.length > 0) {\n            const zone = positionToZone(target);\n            for (const rule of origin.rules) {\n                const toRemoveZones = [];\n                if (isCutOperation) {\n                    //remove from current rule\n                    toRemoveZones.push(positionToZone(origin.position));\n                }\n                if (origin.position.sheetId === target.sheetId) {\n                    this.adaptCFRules(origin.position.sheetId, rule, [zone], toRemoveZones);\n                }\n                else {\n                    this.adaptCFRules(origin.position.sheetId, rule, [], toRemoveZones);\n                    const cfToCopyTo = this.getCFToCopyTo(target.sheetId, rule);\n                    this.adaptCFRules(target.sheetId, cfToCopyTo, [zone], []);\n                }\n            }\n        }\n    }\n    /**\n     * Add or remove cells to a given conditional formatting rule.\n     */\n    adaptCFRules(sheetId, cf, toAdd, toRemove) {\n        if (!this.queuedChanges[sheetId]) {\n            this.queuedChanges[sheetId] = [];\n        }\n        const queuedChange = this.queuedChanges[sheetId].find((queued) => queued.cf.id === cf.id);\n        if (!queuedChange) {\n            this.queuedChanges[sheetId].push({ toAdd, toRemove, cf });\n        }\n        else {\n            queuedChange.toAdd.push(...toAdd);\n            queuedChange.toRemove.push(...toRemove);\n        }\n    }\n    executeQueuedChanges() {\n        for (const sheetId in this.queuedChanges) {\n            for (const { toAdd, toRemove, cf } of this.queuedChanges[sheetId]) {\n                const newRangesXc = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);\n                if (!newRangesXc) {\n                    continue;\n                }\n                if (newRangesXc.length === 0) {\n                    this.dispatch(\"REMOVE_CONDITIONAL_FORMAT\", { id: cf.id, sheetId });\n                    continue;\n                }\n                this.dispatch(\"ADD_CONDITIONAL_FORMAT\", {\n                    cf: {\n                        id: cf.id,\n                        rule: cf.rule,\n                        stopIfTrue: cf.stopIfTrue,\n                    },\n                    ranges: newRangesXc,\n                    sheetId,\n                });\n            }\n        }\n    }\n    getCFToCopyTo(targetSheetId, originCF) {\n        let targetCF = this.getters\n            .getConditionalFormats(targetSheetId)\n            .find((cf) => cf.stopIfTrue === originCF.stopIfTrue && deepEquals(cf.rule, originCF.rule));\n        const queuedCfs = this.queuedChanges[targetSheetId];\n        if (!targetCF && queuedCfs) {\n            targetCF = queuedCfs.find((queued) => queued.cf.stopIfTrue === originCF.stopIfTrue && deepEquals(queued.cf.rule, originCF.rule))?.cf;\n        }\n        return targetCF || { ...originCF, id: this.uuidGenerator.uuidv4(), ranges: [] };\n    }\n}\n\nclass DataValidationClipboardHandler extends AbstractCellClipboardHandler {\n    uuidGenerator = new UuidGenerator();\n    queuedChanges = {};\n    copy(data) {\n        const { rowsIndexes, columnsIndexes } = data;\n        const sheetId = data.sheetId;\n        const dvRules = [];\n        for (const row of rowsIndexes) {\n            const dvRuleInRow = [];\n            for (const col of columnsIndexes) {\n                const position = { sheetId, col, row };\n                const rule = this.getters.getValidationRuleForCell(position);\n                dvRuleInRow.push({ position, rule });\n            }\n            dvRules.push(dvRuleInRow);\n        }\n        return { dvRules };\n    }\n    paste(target, clippedContent, options) {\n        this.queuedChanges = {};\n        if (options.pasteOption) {\n            return;\n        }\n        const zones = target.zones;\n        const sheetId = target.sheetId;\n        if (!options.isCutOperation) {\n            this.pasteFromCopy(sheetId, zones, clippedContent.dvRules);\n        }\n        else {\n            this.pasteFromCut(sheetId, zones, clippedContent);\n        }\n        this.executeQueuedChanges();\n    }\n    pasteFromCut(sheetId, target, content) {\n        const selection = target[0];\n        this.pasteZone(sheetId, selection.left, selection.top, content.dvRules, {\n            isCutOperation: true,\n        });\n    }\n    pasteZone(sheetId, col, row, dvRules, clipboardOptions) {\n        for (const [r, rowCells] of dvRules.entries()) {\n            for (const [c, origin] of rowCells.entries()) {\n                const position = { col: col + c, row: row + r, sheetId };\n                this.pasteDataValidation(origin, position, clipboardOptions?.isCutOperation);\n            }\n        }\n    }\n    pasteDataValidation(origin, target, isCutOperation) {\n        if (origin) {\n            const zone = positionToZone(target);\n            const rule = origin.rule;\n            if (!rule) {\n                const targetRule = this.getters.getValidationRuleForCell(target);\n                if (targetRule) {\n                    // Remove the data validation rule on the target cell\n                    this.adaptDataValidationRule(target.sheetId, targetRule, [], [zone]);\n                }\n                return;\n            }\n            const toRemoveZone = [];\n            if (isCutOperation) {\n                toRemoveZone.push(positionToZone(origin.position));\n            }\n            if (origin.position.sheetId === target.sheetId) {\n                const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule, false);\n                this.adaptDataValidationRule(origin.position.sheetId, copyToRule, [zone], toRemoveZone);\n            }\n            else {\n                const originRule = this.getters.getValidationRuleForCell(origin.position);\n                if (originRule) {\n                    this.adaptDataValidationRule(origin.position.sheetId, originRule, [], toRemoveZone);\n                }\n                const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);\n                this.adaptDataValidationRule(target.sheetId, copyToRule, [zone], []);\n            }\n        }\n    }\n    getDataValidationRuleToCopyTo(targetSheetId, originRule, newId = true) {\n        let targetRule = this.getters\n            .getDataValidationRules(targetSheetId)\n            .find((rule) => deepEquals(originRule.criterion, rule.criterion) &&\n            originRule.isBlocking === rule.isBlocking);\n        const queuedRules = this.queuedChanges[targetSheetId];\n        if (!targetRule && queuedRules) {\n            targetRule = queuedRules.find((queued) => deepEquals(originRule.criterion, queued.rule.criterion) &&\n                originRule.isBlocking === queued.rule.isBlocking)?.rule;\n        }\n        return (targetRule || {\n            ...originRule,\n            id: newId ? this.uuidGenerator.uuidv4() : originRule.id,\n            ranges: [],\n        });\n    }\n    /**\n     * Add or remove XCs to a given data validation rule.\n     */\n    adaptDataValidationRule(sheetId, rule, toAdd, toRemove) {\n        if (!this.queuedChanges[sheetId]) {\n            this.queuedChanges[sheetId] = [];\n        }\n        const queuedChange = this.queuedChanges[sheetId].find((queued) => queued.rule.id === rule.id);\n        if (!queuedChange) {\n            this.queuedChanges[sheetId].push({ toAdd, toRemove, rule });\n        }\n        else {\n            queuedChange.toAdd.push(...toAdd);\n            queuedChange.toRemove.push(...toRemove);\n        }\n    }\n    executeQueuedChanges() {\n        for (const sheetId in this.queuedChanges) {\n            for (const { toAdd, toRemove, rule: dv } of this.queuedChanges[sheetId]) {\n                // Remove the zones first in case the same position is in toAdd and toRemove\n                const dvZones = dv.ranges.map((range) => range.zone);\n                const withRemovedZones = recomputeZones(dvZones, toRemove);\n                const newDvZones = recomputeZones([...withRemovedZones, ...toAdd], []);\n                if (newDvZones.length === 0) {\n                    this.dispatch(\"REMOVE_DATA_VALIDATION_RULE\", { sheetId, id: dv.id });\n                    continue;\n                }\n                this.dispatch(\"ADD_DATA_VALIDATION_RULE\", {\n                    rule: dv,\n                    ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),\n                    sheetId,\n                });\n            }\n        }\n    }\n}\n\nclass ImageClipboardHandler extends AbstractFigureClipboardHandler {\n    copy(data) {\n        const sheetId = data.sheetId;\n        const figure = this.getters.getFigure(sheetId, data.figureId);\n        if (!figure) {\n            throw new Error(`No figure for the given id: ${data.figureId}`);\n        }\n        const copiedFigure = { ...figure };\n        if (figure.tag !== \"image\") {\n            return;\n        }\n        const image = this.getters.getImage(data.figureId);\n        const copiedImage = deepCopy(image);\n        return {\n            figureId: data.figureId,\n            copiedFigure,\n            copiedImage,\n            sheetId,\n        };\n    }\n    getPasteTarget(sheetId, target, content, options) {\n        const newId = new UuidGenerator().uuidv4();\n        return { sheetId, zones: [], figureId: newId };\n    }\n    paste(target, clippedContent, options) {\n        if (!target.figureId) {\n            return;\n        }\n        const { zones, figureId } = target;\n        const sheetId = this.getters.getActiveSheetId();\n        const numCols = this.getters.getNumberCols(sheetId);\n        const numRows = this.getters.getNumberRows(sheetId);\n        const targetX = this.getters.getColDimensions(sheetId, zones[0].left).start;\n        const targetY = this.getters.getRowDimensions(sheetId, zones[0].top).start;\n        const maxX = this.getters.getColDimensions(sheetId, numCols - 1).end;\n        const maxY = this.getters.getRowDimensions(sheetId, numRows - 1).end;\n        const { width, height } = clippedContent.copiedFigure;\n        const position = {\n            x: maxX < width ? 0 : Math.min(targetX, maxX - width),\n            y: maxY < height ? 0 : Math.min(targetY, maxY - height),\n        };\n        const copy = deepCopy(clippedContent.copiedImage);\n        this.dispatch(\"CREATE_IMAGE\", {\n            figureId,\n            sheetId,\n            position,\n            size: { height, width },\n            definition: copy,\n        });\n        if (options.isCutOperation) {\n            this.dispatch(\"DELETE_FIGURE\", {\n                sheetId: clippedContent.sheetId,\n                id: clippedContent.copiedFigure.id,\n            });\n        }\n        this.dispatch(\"SELECT_FIGURE\", { id: figureId });\n    }\n    isPasteAllowed(sheetId, target, content, option) {\n        if (target.length === 0) {\n            return \"EmptyTarget\" /* CommandResult.EmptyTarget */;\n        }\n        if (option?.pasteOption !== undefined) {\n            return \"WrongFigurePasteOption\" /* CommandResult.WrongFigurePasteOption */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n}\n\nclass MergeClipboardHandler extends AbstractCellClipboardHandler {\n    copy(data) {\n        const sheetId = this.getters.getActiveSheetId();\n        const { rowsIndexes, columnsIndexes } = data;\n        const merges = [];\n        for (const row of rowsIndexes) {\n            const mergesInRow = [];\n            for (const col of columnsIndexes) {\n                const position = { col, row, sheetId };\n                mergesInRow.push(this.getters.getMerge(position));\n            }\n            merges.push(mergesInRow);\n        }\n        return { merges };\n    }\n    /**\n     * Paste the clipboard content in the given target\n     */\n    paste(target, content, options) {\n        if (options.isCutOperation) {\n            return;\n        }\n        this.pasteFromCopy(target.sheetId, target.zones, content.merges, options);\n    }\n    pasteZone(sheetId, col, row, merges) {\n        for (const [r, rowMerges] of merges.entries()) {\n            for (const [c, originMerge] of rowMerges.entries()) {\n                const position = { col: col + c, row: row + r, sheetId };\n                this.pasteMerge(originMerge, position);\n            }\n        }\n    }\n    pasteMerge(originMerge, target) {\n        if (!originMerge) {\n            return;\n        }\n        if (this.getters.isInMerge(target)) {\n            return;\n        }\n        const { sheetId, col, row } = target;\n        this.dispatch(\"ADD_MERGE\", {\n            sheetId,\n            force: true,\n            target: [\n                {\n                    left: col,\n                    top: row,\n                    right: col + originMerge.right - originMerge.left,\n                    bottom: row + originMerge.bottom - originMerge.top,\n                },\n            ],\n        });\n    }\n}\n\nclass SheetClipboardHandler extends AbstractCellClipboardHandler {\n    isPasteAllowed(sheetId, target, content, options) {\n        if (!(\"cells\" in content)) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n        for (const zone of getPasteZones(target, content.cells)) {\n            if ((zone.left < xSplit && zone.right >= xSplit) ||\n                (zone.top < ySplit && zone.bottom >= ySplit)) {\n                return \"FrozenPaneOverlap\" /* CommandResult.FrozenPaneOverlap */;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n}\n\nclass TableClipboardHandler extends AbstractCellClipboardHandler {\n    copy(data) {\n        const sheetId = data.sheetId;\n        const { rowsIndexes, columnsIndexes, zones } = data;\n        const copiedTablesIds = new Set();\n        const tableCells = [];\n        for (let row of rowsIndexes) {\n            let tableCellsInRow = [];\n            tableCells.push(tableCellsInRow);\n            for (let col of columnsIndexes) {\n                const position = { col, row, sheetId };\n                const table = this.getters.getTable(position);\n                if (!table) {\n                    tableCellsInRow.push({});\n                    continue;\n                }\n                const coreTable = this.getters.getCoreTable(position);\n                const tableZone = coreTable?.range.zone;\n                let copiedTable = undefined;\n                // Copy whole table\n                if (!copiedTablesIds.has(table.id) &&\n                    coreTable &&\n                    tableZone &&\n                    zones.some((z) => isZoneInside(tableZone, z))) {\n                    copiedTablesIds.add(table.id);\n                    const values = [];\n                    for (const col of range(tableZone.left, tableZone.right + 1)) {\n                        values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));\n                    }\n                    copiedTable = {\n                        range: coreTable.range.rangeData,\n                        config: coreTable.config,\n                        type: coreTable.type,\n                    };\n                }\n                tableCellsInRow.push({\n                    table: copiedTable,\n                    style: this.getTableStyleToCopy(position),\n                    isWholeTableCopied: copiedTablesIds.has(table.id),\n                });\n            }\n        }\n        return {\n            tableCells,\n            sheetId: data.sheetId,\n        };\n    }\n    /**\n     * Get the style to copy for a cell. We need to copy both the table style and the cell style, because\n     * UPDATE_CELL replace the whole style of the cell with the style of the command, it doesn't merge the two.\n     */\n    getTableStyleToCopy(cellPosition) {\n        const styleFromTable = this.getters.getCellTableStyle(cellPosition);\n        const cellStyle = this.getters.getCellStyle(cellPosition);\n        const bordersFromTable = this.getters.getCellTableBorder(cellPosition);\n        const cellBorder = this.getters.getCellBorder(cellPosition);\n        return {\n            style: { ...styleFromTable, ...removeFalsyAttributes(cellStyle) },\n            border: { ...bordersFromTable, ...removeFalsyAttributes(cellBorder) },\n        };\n    }\n    paste(target, content, options) {\n        const zones = target.zones;\n        const sheetId = target.sheetId;\n        if (!options.isCutOperation) {\n            this.pasteFromCopy(sheetId, zones, content.tableCells, options);\n        }\n        else {\n            this.pasteFromCut(sheetId, zones, content, options);\n        }\n    }\n    pasteFromCut(sheetId, target, content, options) {\n        for (const row of content.tableCells) {\n            for (const tableCell of row) {\n                if (tableCell.table) {\n                    this.dispatch(\"REMOVE_TABLE\", {\n                        sheetId: content.sheetId,\n                        target: [this.getters.getRangeFromRangeData(tableCell.table.range).zone],\n                    });\n                }\n            }\n        }\n        const selection = target[0];\n        this.pasteZone(sheetId, selection.left, selection.top, content.tableCells, options);\n    }\n    pasteZone(sheetId, col, row, tableCells, clipboardOptions) {\n        for (let r = 0; r < tableCells.length; r++) {\n            const rowCells = tableCells[r];\n            for (let c = 0; c < rowCells.length; c++) {\n                const tableCell = rowCells[c];\n                if (!tableCell) {\n                    continue;\n                }\n                const position = { col: col + c, row: row + r, sheetId };\n                this.pasteTableCell(sheetId, tableCell, position, clipboardOptions);\n            }\n        }\n        if (tableCells.length === 1) {\n            for (let c = 0; c < tableCells[0].length; c++) {\n                this.dispatch(\"AUTOFILL_TABLE_COLUMN\", { col: col + c, row, sheetId });\n            }\n        }\n    }\n    pasteTableCell(sheetId, tableCell, position, options) {\n        if (tableCell.table && !options?.pasteOption) {\n            const { range: tableRange } = tableCell.table;\n            const zoneDims = zoneToDimension(this.getters.getRangeFromRangeData(tableRange).zone);\n            const newTableZone = {\n                left: position.col,\n                top: position.row,\n                right: position.col + zoneDims.numberOfCols - 1,\n                bottom: position.row + zoneDims.numberOfRows - 1,\n            };\n            this.dispatch(\"CREATE_TABLE\", {\n                sheetId: position.sheetId,\n                ranges: [this.getters.getRangeDataFromZone(sheetId, newTableZone)],\n                config: tableCell.table.config,\n                tableType: tableCell.table.type,\n            });\n        }\n        // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the\n        // dynamic tables are not yet computed\n        if (this.getters.getCoreTable(position) || options?.pasteOption === \"asValue\") {\n            return;\n        }\n        if ((!options?.pasteOption && !tableCell.isWholeTableCopied) ||\n            options?.pasteOption === \"onlyFormat\") {\n            if (tableCell.style?.style) {\n                this.dispatch(\"UPDATE_CELL\", { ...position, style: tableCell.style.style });\n            }\n            if (tableCell.style?.border) {\n                this.dispatch(\"SET_BORDER\", { ...position, border: tableCell.style.border });\n            }\n        }\n    }\n}\n\nconst clipboardHandlersRegistries = {\n    figureHandlers: new Registry(),\n    cellHandlers: new Registry(),\n};\nclipboardHandlersRegistries.figureHandlers\n    .add(\"chart\", ChartClipboardHandler)\n    .add(\"image\", ImageClipboardHandler);\nclipboardHandlersRegistries.cellHandlers\n    .add(\"dataValidation\", DataValidationClipboardHandler)\n    .add(\"cell\", CellClipboardHandler)\n    .add(\"sheet\", SheetClipboardHandler)\n    .add(\"merge\", MergeClipboardHandler)\n    .add(\"border\", BorderClipboardHandler)\n    .add(\"table\", TableClipboardHandler)\n    .add(\"conditionalFormat\", ConditionalFormatClipboardHandler);\n\nfunction transformZone(zone, executed) {\n    if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n        return reduceZoneOnDeletion(zone, executed.dimension === \"COL\" ? \"left\" : \"top\", executed.elements);\n    }\n    if (executed.type === \"ADD_COLUMNS_ROWS\") {\n        return expandZoneOnInsertion(zone, executed.dimension === \"COL\" ? \"left\" : \"top\", executed.base, executed.position, executed.quantity);\n    }\n    return { ...zone };\n}\nfunction transformRangeData(range, executed) {\n    const deletedSheet = executed.type === \"DELETE_SHEET\" && executed.sheetId;\n    if (\"sheetId\" in executed && range._sheetId !== executed.sheetId) {\n        return range;\n    }\n    else {\n        const newZone = transformZone(range._zone, executed);\n        if (newZone && deletedSheet !== range._sheetId) {\n            return { ...range, _zone: newZone };\n        }\n    }\n    return undefined;\n}\n\n/**\n * This is a generic event bus based on the Owl event bus.\n * This bus however ensures type safety across events and subscription callbacks.\n */\nclass EventBus {\n    subscriptions = {};\n    /**\n     * Add a listener for the 'eventType' events.\n     *\n     * Note that the 'owner' of this event can be anything, but will more likely\n     * be a component or a class. The idea is that the callback will be called with\n     * the proper owner bound.\n     *\n     * Also, the owner should be kind of unique. This will be used to remove the\n     * listener.\n     */\n    on(type, owner, callback) {\n        if (!callback) {\n            throw new Error(\"Missing callback\");\n        }\n        if (!this.subscriptions[type]) {\n            this.subscriptions[type] = [];\n        }\n        this.subscriptions[type].push({\n            owner,\n            callback,\n        });\n    }\n    /**\n     * Emit an event of type 'eventType'.  Any extra arguments will be passed to\n     * the listeners callback.\n     */\n    trigger(type, payload) {\n        const subs = this.subscriptions[type] || [];\n        for (let i = 0, iLen = subs.length; i < iLen; i++) {\n            const sub = subs[i];\n            sub.callback.call(sub.owner, payload);\n        }\n    }\n    /**\n     * Remove a listener\n     */\n    off(eventType, owner) {\n        const subs = this.subscriptions[eventType];\n        if (subs) {\n            this.subscriptions[eventType] = subs.filter((s) => s.owner !== owner);\n        }\n    }\n    /**\n     * Remove all subscriptions.\n     */\n    clear() {\n        this.subscriptions = {};\n    }\n}\n\n/**\n * A type-safe dependency container\n */\nclass DependencyContainer extends EventBus {\n    dependencies = new Map();\n    factory = new StoreFactory(this.get.bind(this));\n    /**\n     * Injects a store instance in the dependency container.\n     * Useful for injecting an external store that is not created by the container.\n     * Also useful for mocking a store.\n     */\n    inject(Store, instance) {\n        if (this.dependencies.has(Store) && this.dependencies.get(Store) !== instance) {\n            throw new Error(`Store ${Store.name} already has an instance`);\n        }\n        this.dependencies.set(Store, instance);\n    }\n    /**\n     * Get an instance of a store.\n     */\n    get(Store) {\n        if (!this.dependencies.has(Store)) {\n            this.dependencies.set(Store, this.instantiate(Store));\n        }\n        return this.dependencies.get(Store);\n    }\n    instantiate(Store, ...args) {\n        return this.factory.build(Store, ...args);\n    }\n    resetStores() {\n        this.dependencies.clear();\n    }\n}\nclass StoreFactory {\n    get;\n    pendingBuilds = new Set();\n    constructor(get) {\n        this.get = get;\n    }\n    /**\n     * Build a store instance and get all its dependencies\n     * while detecting and preventing circular dependencies\n     */\n    build(Store, ...args) {\n        if (this.pendingBuilds.has(Store)) {\n            throw new Error(`Circular dependency detected: ${[...this.pendingBuilds, Store]\n                .map((s) => s.name)\n                .join(\" -> \")}`);\n        }\n        this.pendingBuilds.add(Store);\n        const instance = new Store(this.get, ...args);\n        this.pendingBuilds.delete(Store);\n        return instance;\n    }\n}\n\n/**\n * Create a store to expose an external resource (which is not a store itself)\n * to other stores.\n * The external resource needs to be injected in the store provider to provide\n * the store implementation.\n *\n * @example\n * const MyMetaStore = createAbstractStore(\"MyStore\");\n * const stores = useStoreProvider();\n * stores.inject(MyMetaStore, externalResourceInstance);\n */\nfunction createAbstractStore(storeName) {\n    class MetaStore {\n        constructor(get) {\n            throw new Error(`This is a abstract store for ${storeName}, it cannot be instantiated.\nDid you forget to inject your store instance?\n\nconst stores = useStoreProvider();\nstores.inject(MyMetaStore, storeInstance);\n`);\n        }\n    }\n    return MetaStore;\n}\nclass DisposableStore {\n    get;\n    disposeCallbacks = [];\n    constructor(get) {\n        this.get = get;\n    }\n    onDispose(callback) {\n        this.disposeCallbacks.push(callback);\n    }\n    dispose() {\n        this.disposeCallbacks.forEach((cb) => cb());\n    }\n}\n\n/**\n * This hook should be used at the root of your app to provide the store container.\n */\nfunction useStoreProvider() {\n    const env = useEnv();\n    if (env.__spreadsheet_stores__ instanceof DependencyContainer) {\n        return env.__spreadsheet_stores__;\n    }\n    const container = new DependencyContainer();\n    useSubEnv({\n        __spreadsheet_stores__: container,\n        getStore: (Store) => {\n            const store = container.get(Store);\n            return proxifyStoreMutation(store, () => container.trigger(\"store-updated\"));\n        },\n    });\n    return container;\n}\n/**\n * Get the instance of a store.\n */\nfunction useStore(Store) {\n    const env = useEnv();\n    const container = getDependencyContainer(env);\n    const store = container.get(Store);\n    return useStoreRenderProxy(container, store);\n}\nfunction useLocalStore(Store, ...args) {\n    const env = useEnv();\n    const container = getDependencyContainer(env);\n    const store = container.instantiate(Store, ...args);\n    onWillUnmount(() => store.dispose());\n    return useStoreRenderProxy(container, store);\n}\n/**\n * Trigger an event to re-render the app (deep render) when\n * a store is mutated by invoking one of its mutator methods.\n */\nfunction useStoreRenderProxy(container, store) {\n    const component = useComponent();\n    const proxy = proxifyStoreMutation(store, () => {\n        if (status(component) === \"mounted\") {\n            container.trigger(\"store-updated\");\n        }\n    });\n    return proxy;\n}\n/**\n * Creates a proxied version of a store object with mutation tracking.\n * Whenever a mutator method of the store is called, the provided callback function is invoked.\n */\nfunction proxifyStoreMutation(store, callback) {\n    const proxy = new Proxy(store, {\n        get(target, property, receiver) {\n            const thisStore = target;\n            // The third argument is `thisStore` (target) instead of `receiver`.\n            // The goal is to always have the same `this` value in getter functions\n            // (when `target[property]` is an accessor property).\n            // `thisStore` is always the same object reference. `receiver` however is the\n            // object on which the property is called, which is the Proxy object which is different for each component.\n            const value = Reflect.get(target, property, thisStore);\n            if (store.mutators?.includes(property)) {\n                const functionProxy = new Proxy(value, {\n                    // trap the function call\n                    apply(target, thisArg, argArray) {\n                        Reflect.apply(target, thisStore, argArray);\n                        callback();\n                    },\n                });\n                return functionProxy;\n            }\n            return value;\n        },\n    });\n    return proxy;\n}\nfunction getDependencyContainer(env) {\n    const container = env.__spreadsheet_stores__;\n    if (!(container instanceof DependencyContainer)) {\n        throw new Error(\"No store provider found. Did you forget to call useStoreProvider()?\");\n    }\n    return container;\n}\n\nconst ModelStore = createAbstractStore(\"Model\");\n\nclass RendererStore {\n    mutators = [\"register\", \"unRegister\"];\n    renderers = {};\n    register(renderer) {\n        if (!renderer.renderingLayers.length) {\n            return;\n        }\n        for (const layer of renderer.renderingLayers) {\n            if (!this.renderers[layer]) {\n                this.renderers[layer] = [];\n            }\n            this.renderers[layer].push(renderer);\n        }\n    }\n    unRegister(renderer) {\n        for (const layer of Object.keys(this.renderers)) {\n            this.renderers[layer] = this.renderers[layer].filter((r) => r !== renderer);\n        }\n    }\n    drawLayer(context, layer) {\n        const renderers = this.renderers[layer];\n        if (!renderers) {\n            return;\n        }\n        for (const renderer of renderers) {\n            context.ctx.save();\n            renderer.drawLayer(context, layer);\n            context.ctx.restore();\n        }\n    }\n}\n\nclass SpreadsheetStore extends DisposableStore {\n    // cast the model store as Model to allow model.dispatch to return the DispatchResult\n    model = this.get(ModelStore);\n    getters = this.model.getters;\n    renderer = this.get(RendererStore);\n    constructor(get) {\n        super(get);\n        this.model.on(\"command-dispatched\", this, this.handle);\n        this.model.on(\"command-finalized\", this, this.finalize);\n        this.renderer.register(this);\n        this.onDispose(() => {\n            this.model.off(\"command-dispatched\", this);\n            this.model.off(\"command-finalized\", this);\n            this.renderer.unRegister(this);\n        });\n    }\n    get renderingLayers() {\n        return [];\n    }\n    handle(cmd) { }\n    finalize() { }\n    drawLayer(ctx, layer) { }\n}\n\nconst VOID_COMPOSER = {\n    id: \"void-composer\",\n    get editionMode() {\n        return \"inactive\";\n    },\n    startEdition: () => {\n        throw new Error(\"No composer is registered\");\n    },\n    stopEdition: () => {\n        throw new Error(\"No composer is registered\");\n    },\n    setCurrentContent: () => {\n        throw new Error(\"No composer is registered\");\n    },\n};\nclass ComposerFocusStore extends SpreadsheetStore {\n    mutators = [\"focusComposer\", \"focusActiveComposer\"];\n    activeComposer = VOID_COMPOSER;\n    _focusMode = \"inactive\";\n    get focusMode() {\n        return this.activeComposer.editionMode === \"inactive\" ? \"inactive\" : this._focusMode;\n    }\n    focusComposer(listener, args) {\n        this.activeComposer = listener;\n        if (this.getters.isReadonly()) {\n            return;\n        }\n        this._focusMode = args.focusMode || \"contentFocus\";\n        if (this._focusMode !== \"inactive\") {\n            this.setComposerContent(args);\n        }\n    }\n    focusActiveComposer(args) {\n        if (this.getters.isReadonly()) {\n            return;\n        }\n        if (!this.activeComposer) {\n            throw new Error(\"No composer is registered\");\n        }\n        this._focusMode = args.focusMode || \"contentFocus\";\n        if (this._focusMode !== \"inactive\") {\n            this.setComposerContent(args);\n        }\n    }\n    /**\n     * Start the edition or update the content if it's already started.\n     */\n    setComposerContent({ content, selection, }) {\n        if (this.activeComposer.editionMode === \"inactive\") {\n            this.activeComposer.startEdition(content, selection);\n        }\n        else if (content) {\n            this.activeComposer.setCurrentContent(content, selection);\n        }\n    }\n}\n\nconst TREND_LINE_XAXIS_ID = \"x1\";\n/**\n * This file contains helpers that are common to different charts (mainly\n * line, bar and pie charts)\n */\n/**\n * Adapt ranges of a chart which support DataSet (dataSets and LabelRange).\n */\nfunction updateChartRangesWithDataSets(getters, applyChange, chartDataSets, chartLabelRange) {\n    let isStale = false;\n    const dataSetsWithUndefined = [];\n    for (let index in chartDataSets) {\n        let ds = chartDataSets[index];\n        if (ds.labelCell) {\n            const labelCell = adaptChartRange(ds.labelCell, applyChange);\n            if (ds.labelCell !== labelCell) {\n                isStale = true;\n                ds = {\n                    ...ds,\n                    labelCell: labelCell,\n                };\n            }\n        }\n        const dataRange = adaptChartRange(ds.dataRange, applyChange);\n        if (dataRange === undefined ||\n            getters.getRangeString(dataRange, dataRange.sheetId) === CellErrorType.InvalidReference) {\n            isStale = true;\n            ds = undefined;\n        }\n        else if (dataRange !== ds.dataRange) {\n            isStale = true;\n            ds = {\n                ...ds,\n                dataRange,\n            };\n        }\n        dataSetsWithUndefined[index] = ds;\n    }\n    let labelRange = chartLabelRange;\n    const range = adaptChartRange(labelRange, applyChange);\n    if (range !== labelRange) {\n        isStale = true;\n        labelRange = range;\n    }\n    const dataSets = dataSetsWithUndefined.filter(isDefined);\n    return {\n        isStale,\n        dataSets,\n        labelRange,\n    };\n}\n/**\n * Copy the dataSets given. All the ranges which are on sheetIdFrom will target\n * sheetIdTo.\n */\nfunction copyDataSetsWithNewSheetId(sheetIdFrom, sheetIdTo, dataSets) {\n    return dataSets.map((ds) => {\n        return {\n            dataRange: copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, ds.dataRange),\n            labelCell: ds.labelCell\n                ? copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, ds.labelCell)\n                : undefined,\n        };\n    });\n}\n/**\n * Copy a range. If the range is on the sheetIdFrom, the range will target\n * sheetIdTo.\n */\nfunction copyLabelRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) {\n    return range ? copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) : undefined;\n}\n/**\n * Adapt a single range of a chart\n */\nfunction adaptChartRange(range, applyChange) {\n    if (!range) {\n        return undefined;\n    }\n    const change = applyChange(range);\n    switch (change.changeType) {\n        case \"NONE\":\n            return range;\n        case \"REMOVE\":\n            return undefined;\n        default:\n            return change.range;\n    }\n}\n/**\n * Create the dataSet objects from xcs\n */\nfunction createDataSets(getters, customizedDataSets, sheetId, dataSetsHaveTitle) {\n    const dataSets = [];\n    for (const dataSet of customizedDataSets) {\n        const dataRange = getters.getRangeFromSheetXC(sheetId, dataSet.dataRange);\n        const { unboundedZone: zone, sheetId: dataSetSheetId, invalidSheetName, invalidXc } = dataRange;\n        if (invalidSheetName || invalidXc) {\n            continue;\n        }\n        // It's a rectangle. We treat all columns (arbitrary) as different data series.\n        if (zone.left !== zone.right && zone.top !== zone.bottom) {\n            if (zone.right === undefined) {\n                // Should never happens because of the allowDispatch of charts, but just making sure\n                continue;\n            }\n            for (let column = zone.left; column <= zone.right; column++) {\n                const columnZone = {\n                    ...zone,\n                    left: column,\n                    right: column,\n                };\n                dataSets.push({\n                    ...createDataSet(getters, dataSetSheetId, columnZone, dataSetsHaveTitle\n                        ? {\n                            top: columnZone.top,\n                            bottom: columnZone.top,\n                            left: columnZone.left,\n                            right: columnZone.left,\n                        }\n                        : undefined),\n                    backgroundColor: dataSet.backgroundColor,\n                    rightYAxis: dataSet.yAxisId === \"y1\",\n                    customLabel: dataSet.label,\n                });\n            }\n        }\n        else {\n            /* 1 cell, 1 row or 1 column */\n            dataSets.push({\n                ...createDataSet(getters, dataSetSheetId, zone, dataSetsHaveTitle\n                    ? {\n                        top: zone.top,\n                        bottom: zone.top,\n                        left: zone.left,\n                        right: zone.left,\n                    }\n                    : undefined),\n                backgroundColor: dataSet.backgroundColor,\n                rightYAxis: dataSet.yAxisId === \"y1\",\n                customLabel: dataSet.label,\n            });\n        }\n    }\n    return dataSets;\n}\nfunction createDataSet(getters, sheetId, fullZone, titleZone) {\n    if (fullZone.left !== fullZone.right && fullZone.top !== fullZone.bottom) {\n        throw new Error(`Zone should be a single column or row: ${zoneToXc(fullZone)}`);\n    }\n    if (titleZone) {\n        const dataXC = zoneToXc(fullZone);\n        const labelCellXC = zoneToXc(titleZone);\n        return {\n            labelCell: getters.getRangeFromSheetXC(sheetId, labelCellXC),\n            dataRange: getters.getRangeFromSheetXC(sheetId, dataXC),\n        };\n    }\n    else {\n        return {\n            labelCell: undefined,\n            dataRange: getters.getRangeFromSheetXC(sheetId, zoneToXc(fullZone)),\n        };\n    }\n}\n/**\n * Transform a dataSet to a ExcelDataSet\n */\nfunction toExcelDataset(getters, ds) {\n    const labelZone = ds.labelCell?.zone;\n    let dataZone = ds.dataRange.zone;\n    if (labelZone) {\n        const { numberOfRows, numberOfCols } = zoneToDimension(dataZone);\n        if (numberOfRows === 1) {\n            dataZone = { ...dataZone, left: dataZone.left + 1 };\n        }\n        else if (numberOfCols === 1) {\n            dataZone = { ...dataZone, top: dataZone.top + 1 };\n        }\n    }\n    const dataRange = ds.dataRange.clone({ zone: dataZone });\n    let label = {};\n    if (ds.customLabel) {\n        label = {\n            text: ds.customLabel,\n        };\n    }\n    else if (ds.labelCell) {\n        label = {\n            reference: getters.getRangeString(ds.labelCell, \"forceSheetReference\", {\n                useFixedReference: true,\n            }),\n        };\n    }\n    return {\n        label,\n        range: getters.getRangeString(dataRange, \"forceSheetReference\", { useFixedReference: true }),\n        backgroundColor: ds.backgroundColor,\n        rightYAxis: ds.rightYAxis,\n    };\n}\nfunction toExcelLabelRange(getters, labelRange, shouldRemoveFirstLabel) {\n    if (!labelRange)\n        return undefined;\n    let zone = {\n        ...labelRange.zone,\n    };\n    if (shouldRemoveFirstLabel && labelRange.zone.bottom > labelRange.zone.top) {\n        zone.top = zone.top + 1;\n    }\n    const range = labelRange.clone({ zone });\n    return getters.getRangeString(range, \"forceSheetReference\", { useFixedReference: true });\n}\n/**\n * Transform a chart definition which supports dataSets (dataSets and LabelRange)\n * with an executed command\n */\nfunction transformChartDefinitionWithDataSetsWithZone(definition, executed) {\n    let labelRange;\n    if (definition.labelRange) {\n        const labelZone = transformZone(toUnboundedZone(definition.labelRange), executed);\n        labelRange = labelZone ? zoneToXc(labelZone) : undefined;\n    }\n    const dataSets = definition.dataSets\n        .map((ds) => toUnboundedZone(ds.dataRange))\n        .map((zone) => transformZone(zone, executed))\n        .filter(isDefined)\n        .map((xc) => ({ dataRange: zoneToXc(xc) }));\n    return {\n        ...definition,\n        labelRange,\n        dataSets,\n    };\n}\n/**\n * Choose a font color based on a background color.\n * The font is white with a dark background.\n */\nfunction chartFontColor(backgroundColor) {\n    if (!backgroundColor) {\n        return \"#000000\";\n    }\n    return relativeLuminance(backgroundColor) < 0.3 ? \"#FFFFFF\" : \"#000000\";\n}\nfunction checkDataset(definition) {\n    if (definition.dataSets) {\n        const invalidRanges = definition.dataSets.find((range) => !rangeReference.test(range.dataRange)) !== undefined;\n        if (invalidRanges) {\n            return \"InvalidDataSet\" /* CommandResult.InvalidDataSet */;\n        }\n        const zones = definition.dataSets.map((ds) => toUnboundedZone(ds.dataRange));\n        if (zones.some((zone) => zone.top !== zone.bottom && isFullRow(zone))) {\n            return \"InvalidDataSet\" /* CommandResult.InvalidDataSet */;\n        }\n    }\n    return \"Success\" /* CommandResult.Success */;\n}\nfunction checkLabelRange(definition) {\n    if (definition.labelRange) {\n        const invalidLabels = !rangeReference.test(definition.labelRange || \"\");\n        if (invalidLabels) {\n            return \"InvalidLabelRange\" /* CommandResult.InvalidLabelRange */;\n        }\n    }\n    return \"Success\" /* CommandResult.Success */;\n}\nfunction shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {\n    if (!dataSetsHaveTitle)\n        return false;\n    if (!labelRange)\n        return false;\n    if (!dataset)\n        return true;\n    const datasetLength = getZoneArea(dataset.dataRange.zone);\n    const labelLength = getZoneArea(labelRange.zone);\n    if (labelLength < datasetLength) {\n        return false;\n    }\n    return true;\n}\nfunction getChartPositionAtCenterOfViewport(getters, chartSize) {\n    const { x, y } = getters.getMainViewportCoordinates();\n    const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();\n    const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());\n    const position = {\n        x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),\n        y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),\n    }; // Position at the center of the scrollable viewport\n    return position;\n}\nfunction getChartAxisTitleRuntime(design) {\n    if (design?.title?.text) {\n        const { text, color, align, italic, bold } = design.title;\n        return {\n            display: true,\n            text,\n            color,\n            font: {\n                style: italic ? \"italic\" : \"normal\",\n                weight: bold ? \"bold\" : \"normal\",\n            },\n            align: align === \"left\" ? \"start\" : align === \"right\" ? \"end\" : \"center\",\n        };\n    }\n    return;\n}\nfunction getDefinedAxis(definition) {\n    let useLeftAxis = false, useRightAxis = false;\n    if (\"horizontal\" in definition && definition.horizontal) {\n        return { useLeftAxis: true, useRightAxis: false };\n    }\n    for (const design of definition.dataSets || []) {\n        if (design.yAxisId === \"y1\") {\n            useRightAxis = true;\n        }\n        else {\n            useLeftAxis = true;\n        }\n    }\n    useLeftAxis ||= !useRightAxis;\n    return { useLeftAxis, useRightAxis };\n}\nfunction computeChartPadding({ displayTitle, displayLegend, }) {\n    let top = 25;\n    if (displayTitle) {\n        top = 0;\n    }\n    else if (displayLegend) {\n        top = 10;\n    }\n    return { left: 20, right: 20, top, bottom: 10 };\n}\nfunction getTrendDatasetForBarChart(config, dataset) {\n    const filteredValues = [];\n    const filteredLabels = [];\n    const labels = [];\n    for (let i = 0; i < dataset.data.length; i++) {\n        if (typeof dataset.data[i] === \"number\") {\n            filteredValues.push(dataset.data[i]);\n            filteredLabels.push(i + 1);\n        }\n        labels.push(i + 1);\n    }\n    const newLabels = range(0.5, labels.length + 0.55, 0.2);\n    const newValues = interpolateData(config, filteredValues, filteredLabels, newLabels);\n    if (!newValues.length) {\n        return;\n    }\n    return getFullTrendingLineDataSet(dataset, config, newValues);\n}\nfunction getFullTrendingLineDataSet(dataset, config, data) {\n    const defaultBorderColor = colorToRGBA(dataset.backgroundColor);\n    defaultBorderColor.a = 1;\n    const borderColor = config.color || lightenColor(rgbaToHex(defaultBorderColor), 0.5);\n    return {\n        type: \"line\",\n        xAxisID: TREND_LINE_XAXIS_ID,\n        yAxisID: dataset.yAxisID,\n        label: dataset.label ? _t(\"Trend line for %s\", dataset.label) : \"\",\n        data,\n        order: -1,\n        showLine: true,\n        pointRadius: 0,\n        backgroundColor: borderColor,\n        borderColor,\n        borderDash: [5, 5],\n        borderWidth: undefined,\n        fill: false,\n        pointBackgroundColor: borderColor,\n    };\n}\nfunction interpolateData(config, values, labels, newLabels) {\n    if (values.length < 2 || labels.length < 2 || newLabels.length === 0) {\n        return [];\n    }\n    const labelMin = Math.min(...labels);\n    const labelMax = Math.max(...labels);\n    const labelRange = labelMax - labelMin;\n    const normalizedLabels = labels.map((v) => (v - labelMin) / labelRange);\n    const normalizedNewLabels = newLabels.map((v) => (v - labelMin) / labelRange);\n    try {\n        switch (config.type) {\n            case \"polynomial\": {\n                const order = config.order;\n                if (!order) {\n                    return Array.from({ length: newLabels.length }, () => NaN);\n                }\n                if (order === 1) {\n                    return predictLinearValues([values], [normalizedLabels], [normalizedNewLabels], true)[0];\n                }\n                const coeffs = polynomialRegression(values, normalizedLabels, order, true).flat();\n                return normalizedNewLabels.map((v) => evaluatePolynomial(coeffs, v, order));\n            }\n            case \"exponential\": {\n                const positiveLogValues = [];\n                const filteredLabels = [];\n                for (let i = 0; i < values.length; i++) {\n                    if (values[i] > 0) {\n                        positiveLogValues.push(Math.log(values[i]));\n                        filteredLabels.push(normalizedLabels[i]);\n                    }\n                }\n                if (!filteredLabels.length) {\n                    return Array.from({ length: newLabels.length }, () => NaN);\n                }\n                return expM(predictLinearValues([positiveLogValues], [filteredLabels], [normalizedNewLabels], true))[0];\n            }\n            case \"logarithmic\": {\n                return predictLinearValues([values], logM([normalizedLabels]), logM([normalizedNewLabels]), true)[0];\n            }\n            default:\n                return [];\n        }\n    }\n    catch (e) {\n        return Array.from({ length: newLabels.length }, () => NaN);\n    }\n}\nfunction formatTickValue(localeFormat) {\n    return (value) => {\n        value = Number(value);\n        if (isNaN(value))\n            return value;\n        const { locale, format } = localeFormat;\n        return formatValue(value, {\n            locale,\n            format: !format && Math.abs(value) >= 1000 ? \"#,##\" : format,\n        });\n    };\n}\nfunction getChartColorsGenerator(definition, dataSetsSize) {\n    return new ColorGenerator(dataSetsSize, definition.dataSets.map((ds) => ds.backgroundColor));\n}\nconst CHART_AXIS_CHOICES = [\n    { value: \"left\", label: _t(\"Left\") },\n    { value: \"right\", label: _t(\"Right\") },\n];\n\n/** This is a chartJS plugin that will draw the values of each data next to the point/bar/pie slice */\nconst chartShowValuesPlugin = {\n    id: \"chartShowValuesPlugin\",\n    afterDatasetsDraw(chart, args, options) {\n        if (!options.showValues) {\n            return;\n        }\n        const drawData = chart._metasets?.[0]?.data;\n        if (!drawData) {\n            return;\n        }\n        const ctx = chart.ctx;\n        ctx.save();\n        ctx.textAlign = \"center\";\n        ctx.textBaseline = \"middle\";\n        ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText\n        switch (chart.config.type) {\n            case \"pie\":\n            case \"doughnut\":\n                drawPieChartValues(chart, options, ctx);\n                break;\n            case \"bar\":\n            case \"line\":\n                options.horizontal\n                    ? drawHorizontalBarChartValues(chart, options, ctx)\n                    : drawLineOrBarChartValues(chart, options, ctx);\n                break;\n        }\n        ctx.restore();\n    },\n};\nfunction drawTextWithBackground(text, x, y, ctx) {\n    ctx.lineWidth = 3; // Stroke the text with a big lineWidth width to have some kind of background\n    ctx.strokeText(text, x, y);\n    ctx.lineWidth = 1;\n    ctx.fillText(text, x, y);\n}\nfunction drawLineOrBarChartValues(chart, options, ctx) {\n    const yMax = chart.chartArea.bottom;\n    const yMin = chart.chartArea.top;\n    const textsPositions = {};\n    for (const dataset of chart._metasets) {\n        if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {\n            return; // ignore trend lines\n        }\n        for (let i = 0; i < dataset._parsed.length; i++) {\n            const value = dataset._parsed[i].y;\n            const point = dataset.data[i];\n            const xPosition = point.x;\n            let yPosition = 0;\n            if (chart.config.type === \"line\") {\n                yPosition = point.y - 10;\n            }\n            else {\n                yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;\n            }\n            yPosition = Math.min(yPosition, yMax);\n            yPosition = Math.max(yPosition, yMin);\n            // Avoid overlapping texts with same X\n            if (!textsPositions[xPosition]) {\n                textsPositions[xPosition] = [];\n            }\n            for (const otherPosition of textsPositions[xPosition] || []) {\n                if (Math.abs(otherPosition - yPosition) < 13) {\n                    yPosition = otherPosition - 13;\n                }\n            }\n            textsPositions[xPosition].push(yPosition);\n            ctx.fillStyle = point.options.backgroundColor;\n            ctx.strokeStyle = options.background || \"#ffffff\";\n            drawTextWithBackground(options.callback(value - 0), xPosition, yPosition, ctx);\n        }\n    }\n}\nfunction drawHorizontalBarChartValues(chart, options, ctx) {\n    const xMax = chart.chartArea.right;\n    const xMin = chart.chartArea.left;\n    const textsPositions = {};\n    for (const dataset of chart._metasets) {\n        if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {\n            return; // ignore trend lines\n        }\n        for (let i = 0; i < dataset._parsed.length; i++) {\n            const value = dataset._parsed[i].x;\n            const displayValue = options.callback(value - 0);\n            const point = dataset.data[i];\n            const yPosition = point.y;\n            let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;\n            xPosition = Math.min(xPosition, xMax);\n            xPosition = Math.max(xPosition, xMin);\n            // Avoid overlapping texts with same Y\n            if (!textsPositions[yPosition]) {\n                textsPositions[yPosition] = [];\n            }\n            const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, \"px\");\n            for (const otherPosition of textsPositions[yPosition]) {\n                if (Math.abs(otherPosition - xPosition) < textWidth) {\n                    xPosition = otherPosition + textWidth + 3;\n                }\n            }\n            textsPositions[yPosition].push(xPosition);\n            ctx.fillStyle = point.options.backgroundColor;\n            ctx.strokeStyle = options.background || \"#ffffff\";\n            drawTextWithBackground(displayValue, xPosition, yPosition, ctx);\n        }\n    }\n}\nfunction drawPieChartValues(chart, options, ctx) {\n    for (const dataset of chart._metasets) {\n        for (let i = 0; i < dataset._parsed.length; i++) {\n            const value = Number(dataset._parsed[i]);\n            if (isNaN(value) || value === 0) {\n                continue;\n            }\n            const bar = dataset.data[i];\n            const { startAngle, endAngle, innerRadius, outerRadius } = bar;\n            const midAngle = (startAngle + endAngle) / 2;\n            const midRadius = (innerRadius + outerRadius) / 2;\n            const x = bar.x + midRadius * Math.cos(midAngle);\n            const y = bar.y + midRadius * Math.sin(midAngle) + 7;\n            ctx.fillStyle = chartFontColor(options.background);\n            ctx.strokeStyle = options.background || \"#ffffff\";\n            const displayValue = options.callback(value);\n            drawTextWithBackground(displayValue, x, y, ctx);\n        }\n    }\n}\n\n/** This is a chartJS plugin that will draw connector lines between the bars of a Waterfall chart */\nconst waterfallLinesPlugin = {\n    id: \"waterfallLinesPlugin\",\n    beforeDraw(chart, args, options) {\n        if (!options.showConnectorLines) {\n            return;\n        }\n        // Note: private properties are not in the typing of chartJS (and some of the existing types are missing properties)\n        // so we don't type anything in this file\n        const drawData = chart._metasets?.[0]?.data;\n        if (!drawData) {\n            return;\n        }\n        const ctx = chart.ctx;\n        ctx.save();\n        ctx.setLineDash([3, 2]);\n        for (let i = 0; i < drawData.length; i++) {\n            const bar = drawData[i];\n            if (bar.height === 0) {\n                continue;\n            }\n            const nextBar = getNextNonEmptyBar(drawData, i);\n            if (!nextBar) {\n                break;\n            }\n            const rect = getBarElementRect(bar);\n            const nextBarRect = getBarElementRect(nextBar);\n            const rawBarValues = bar.$context.raw;\n            const value = rawBarValues[1] - rawBarValues[0];\n            const lineY = Math.round(value < 0 ? rect.bottom - 1 : rect.top);\n            const lineStart = Math.round(rect.right);\n            const lineEnd = Math.round(nextBarRect.left);\n            ctx.strokeStyle = \"#999\";\n            ctx.beginPath();\n            ctx.moveTo(lineStart + 1, lineY + 0.5);\n            ctx.lineTo(lineEnd, lineY + 0.5);\n            ctx.stroke();\n        }\n        ctx.restore();\n    },\n};\nfunction getBarElementRect(bar) {\n    const flipped = bar.base < bar.y; // Bar are flipped for negative values in the dataset\n    return {\n        left: bar.x - bar.width / 2,\n        right: bar.x + bar.width / 2,\n        bottom: flipped ? bar.base + bar.height : bar.y + bar.height,\n        top: flipped ? bar.base : bar.y,\n    };\n}\nfunction getNextNonEmptyBar(bars, startIndex) {\n    return bars.find((bar, i) => i > startIndex && bar.height !== 0);\n}\n\nwindow.Chart?.register(waterfallLinesPlugin);\nwindow.Chart?.register(chartShowValuesPlugin);\nclass ChartJsComponent extends Component {\n    static template = \"o-spreadsheet-ChartJsComponent\";\n    static props = {\n        figure: Object,\n    };\n    canvas = useRef(\"graphContainer\");\n    chart;\n    currentRuntime;\n    get background() {\n        return this.chartRuntime.background;\n    }\n    get canvasStyle() {\n        return `background-color: ${this.background}`;\n    }\n    get chartRuntime() {\n        const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);\n        if (!(\"chartJsConfig\" in runtime)) {\n            throw new Error(\"Unsupported chart runtime\");\n        }\n        return runtime;\n    }\n    setup() {\n        onMounted(() => {\n            const runtime = this.chartRuntime;\n            this.currentRuntime = runtime;\n            // Note: chartJS modify the runtime in place, so it's important to give it a copy\n            this.createChart(deepCopy(runtime.chartJsConfig));\n        });\n        onWillUnmount(() => this.chart?.destroy());\n        useEffect(() => {\n            const runtime = this.chartRuntime;\n            if (runtime !== this.currentRuntime) {\n                if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {\n                    this.chart?.destroy();\n                    this.createChart(deepCopy(runtime.chartJsConfig));\n                }\n                else {\n                    this.updateChartJs(deepCopy(runtime));\n                }\n                this.currentRuntime = runtime;\n            }\n        });\n    }\n    createChart(chartData) {\n        const canvas = this.canvas.el;\n        const ctx = canvas.getContext(\"2d\");\n        this.chart = new window.Chart(ctx, chartData);\n    }\n    updateChartJs(chartRuntime) {\n        const chartData = chartRuntime.chartJsConfig;\n        if (chartData.data && chartData.data.datasets) {\n            this.chart.data = chartData.data;\n            if (chartData.options?.plugins?.title) {\n                this.chart.config.options.plugins.title = chartData.options.plugins.title;\n            }\n        }\n        else {\n            this.chart.data.datasets = [];\n        }\n        this.chart.config.options = chartData.options;\n        this.chart.update();\n    }\n}\n\n/**\n * AbstractChart is the class from which every Chart should inherit.\n * The role of this class is to maintain the state of each chart.\n */\nclass AbstractChart {\n    sheetId;\n    title;\n    getters;\n    constructor(definition, sheetId, getters) {\n        this.title = definition.title;\n        this.sheetId = sheetId;\n        this.getters = getters;\n    }\n    /**\n     * Validate the chart definition given as arguments. This function will be\n     * called from allowDispatch function\n     */\n    static validateChartDefinition(validator, definition) {\n        throw new Error(\"This method should be implemented by sub class\");\n    }\n    /**\n     * Get a new chart definition transformed with the executed command. This\n     * functions will be called during operational transform process\n     */\n    static transformDefinition(definition, executed) {\n        throw new Error(\"This method should be implemented by sub class\");\n    }\n    /**\n     * Get an empty definition based on the given context\n     */\n    static getDefinitionFromContextCreation(context) {\n        throw new Error(\"This method should be implemented by sub class\");\n    }\n}\n\nfunction getBaselineText(baseline, keyValue, baselineMode, humanize, locale) {\n    if (!baseline) {\n        return \"\";\n    }\n    else if (baselineMode === \"text\" ||\n        keyValue?.type !== CellValueType.number ||\n        baseline.type !== CellValueType.number) {\n        if (humanize) {\n            return humanizeNumber(baseline, locale);\n        }\n        return baseline.formattedValue;\n    }\n    let { value, format } = baseline;\n    if (baselineMode === \"progress\") {\n        value = keyValue.value / value;\n        format = \"0.0%\";\n    }\n    else {\n        value = Math.abs(keyValue.value - value);\n        if (baselineMode === \"percentage\" && value !== 0) {\n            value = value / baseline.value;\n        }\n        if (baselineMode === \"percentage\") {\n            format = \"0.0%\";\n        }\n        if (!format) {\n            value = Math.round(value * 100) / 100;\n        }\n    }\n    if (humanize) {\n        return humanizeNumber({ value, format }, locale);\n    }\n    return formatValue(value, { format, locale });\n}\nfunction getKeyValueText(keyValueCell, humanize, locale) {\n    if (!keyValueCell) {\n        return \"\";\n    }\n    if (humanize) {\n        return humanizeNumber(keyValueCell, locale);\n    }\n    return keyValueCell.formattedValue ?? String(keyValueCell.value ?? \"\");\n}\nfunction getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {\n    if (baselineMode === \"text\" ||\n        baselineMode === \"progress\" ||\n        baseline?.type !== CellValueType.number ||\n        keyValue?.type !== CellValueType.number) {\n        return undefined;\n    }\n    const diff = keyValue.value - baseline.value;\n    if (diff > 0) {\n        return colorUp;\n    }\n    else if (diff < 0) {\n        return colorDown;\n    }\n    return undefined;\n}\nfunction getBaselineArrowDirection(baseline, keyValue, baselineMode) {\n    if (baselineMode === \"text\" ||\n        baseline?.type !== CellValueType.number ||\n        keyValue?.type !== CellValueType.number) {\n        return \"neutral\";\n    }\n    const diff = keyValue.value - baseline.value;\n    if (diff > 0) {\n        return \"up\";\n    }\n    else if (diff < 0) {\n        return \"down\";\n    }\n    return \"neutral\";\n}\nfunction checkKeyValue(definition) {\n    return definition.keyValue && !rangeReference.test(definition.keyValue)\n        ? \"InvalidScorecardKeyValue\" /* CommandResult.InvalidScorecardKeyValue */\n        : \"Success\" /* CommandResult.Success */;\n}\nfunction checkBaseline(definition) {\n    return definition.baseline && !rangeReference.test(definition.baseline)\n        ? \"InvalidScorecardBaseline\" /* CommandResult.InvalidScorecardBaseline */\n        : \"Success\" /* CommandResult.Success */;\n}\nconst arrowDownPath = new window.Path2D(\"M8.6 4.8a.5.5 0 0 1 0 .75l-3.9 3.9a.5 .5 0 0 1 -.75 0l-3.8 -3.9a.5 .5 0 0 1 0 -.75l.4-.4a.5.5 0 0 1 .75 0l2.3 2.4v-5.7c0-.25.25-.5.5-.5h.6c.25 0 .5.25.5.5v5.8l2.3 -2.4a.5.5 0 0 1 .75 0z\");\nconst arrowUpPath = new window.Path2D(\"M8.7 5.5a.5.5 0 0 0 0-.75l-3.8-4a.5.5 0 0 0-.75 0l-3.8 4a.5.5 0 0 0 0 .75l.4.4a.5.5 0 0 0 .75 0l2.3-2.4v5.8c0 .25.25.5.5.5h.6c.25 0 .5-.25.5-.5v-5.8l2.2 2.4a.5.5 0 0 0 .75 0z\");\nlet ScorecardChart$1 = class ScorecardChart extends AbstractChart {\n    keyValue;\n    baseline;\n    baselineMode;\n    baselineDescr;\n    progressBar = false;\n    background;\n    baselineColorUp;\n    baselineColorDown;\n    fontColor;\n    humanize;\n    type = \"scorecard\";\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.keyValue = createValidRange(getters, sheetId, definition.keyValue);\n        this.baseline = createValidRange(getters, sheetId, definition.baseline);\n        this.baselineMode = definition.baselineMode;\n        this.baselineDescr = definition.baselineDescr;\n        this.background = definition.background;\n        this.baselineColorUp = definition.baselineColorUp ?? DEFAULT_SCORECARD_BASELINE_COLOR_UP;\n        this.baselineColorDown = definition.baselineColorDown ?? DEFAULT_SCORECARD_BASELINE_COLOR_DOWN;\n        this.humanize = definition.humanize ?? false;\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, checkKeyValue, checkBaseline);\n    }\n    static getDefinitionFromContextCreation(context) {\n        return {\n            background: context.background,\n            type: \"scorecard\",\n            keyValue: context.range ? context.range[0].dataRange : undefined,\n            title: context.title || { text: \"\" },\n            baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,\n            baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,\n            baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,\n            baseline: context.auxiliaryRange || \"\",\n        };\n    }\n    static transformDefinition(definition, executed) {\n        let baselineZone;\n        let keyValueZone;\n        if (definition.baseline) {\n            baselineZone = transformZone(toUnboundedZone(definition.baseline), executed);\n        }\n        if (definition.keyValue) {\n            keyValueZone = transformZone(toUnboundedZone(definition.keyValue), executed);\n        }\n        return {\n            ...definition,\n            baseline: baselineZone ? zoneToXc(baselineZone) : undefined,\n            keyValue: keyValueZone ? zoneToXc(keyValueZone) : undefined,\n        };\n    }\n    copyForSheetId(sheetId) {\n        const baseline = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.baseline);\n        const keyValue = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.keyValue);\n        const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue, sheetId);\n        return new ScorecardChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue, sheetId);\n        return new ScorecardChart(definition, sheetId, this.getters);\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue);\n    }\n    getContextCreation() {\n        return {\n            ...this,\n            range: this.keyValue\n                ? [{ dataRange: this.getters.getRangeString(this.keyValue, this.sheetId) }]\n                : undefined,\n            auxiliaryRange: this.baseline\n                ? this.getters.getRangeString(this.baseline, this.sheetId)\n                : undefined,\n        };\n    }\n    getDefinitionWithSpecificRanges(baseline, keyValue, targetSheetId) {\n        return {\n            baselineColorDown: this.baselineColorDown,\n            baselineColorUp: this.baselineColorUp,\n            baselineMode: this.baselineMode,\n            title: this.title,\n            type: \"scorecard\",\n            background: this.background,\n            baseline: baseline\n                ? this.getters.getRangeString(baseline, targetSheetId || this.sheetId)\n                : undefined,\n            baselineDescr: this.baselineDescr,\n            keyValue: keyValue\n                ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)\n                : undefined,\n            humanize: this.humanize,\n        };\n    }\n    getDefinitionForExcel() {\n        // This kind of graph is not exportable in Excel\n        return undefined;\n    }\n    updateRanges(applyChange) {\n        const baseline = adaptChartRange(this.baseline, applyChange);\n        const keyValue = adaptChartRange(this.keyValue, applyChange);\n        if (this.baseline === baseline && this.keyValue === keyValue) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue);\n        return new ScorecardChart(definition, this.sheetId, this.getters);\n    }\n};\nfunction drawScoreChart(structure, canvas) {\n    const ctx = canvas.getContext(\"2d\");\n    canvas.width = structure.canvas.width;\n    const availableWidth = canvas.width - DEFAULT_CHART_PADDING;\n    canvas.height = structure.canvas.height;\n    ctx.fillStyle = structure.canvas.backgroundColor;\n    ctx.fillRect(0, 0, structure.canvas.width, structure.canvas.height);\n    if (structure.title) {\n        ctx.font = structure.title.style.font;\n        ctx.fillStyle = structure.title.style.color;\n        const baseline = ctx.textBaseline;\n        ctx.textBaseline = \"middle\";\n        ctx.fillText(clipTextWithEllipsis(ctx, structure.title.text, availableWidth - structure.title.position.x), structure.title.position.x, structure.title.position.y);\n        ctx.textBaseline = baseline;\n    }\n    if (structure.baseline) {\n        ctx.font = structure.baseline.style.font;\n        ctx.fillStyle = structure.baseline.style.color;\n        drawDecoratedText(ctx, structure.baseline.text, structure.baseline.position, structure.baseline.style.underline, structure.baseline.style.strikethrough);\n    }\n    if (structure.baselineArrow && structure.baselineArrow.style.size > 0) {\n        ctx.save();\n        ctx.fillStyle = structure.baselineArrow.style.color;\n        ctx.translate(structure.baselineArrow.position.x, structure.baselineArrow.position.y);\n        // This ratio is computed according to the original svg size and the final size we want\n        const ratio = structure.baselineArrow.style.size / 10;\n        ctx.scale(ratio, ratio);\n        switch (structure.baselineArrow.direction) {\n            case \"down\": {\n                ctx.fill(arrowDownPath);\n                break;\n            }\n            case \"up\": {\n                ctx.fill(arrowUpPath);\n                break;\n            }\n        }\n        ctx.restore();\n    }\n    if (structure.baselineDescr) {\n        const descr = structure.baselineDescr[0];\n        ctx.font = descr.style.font;\n        ctx.fillStyle = descr.style.color;\n        for (const description of structure.baselineDescr) {\n            ctx.fillText(clipTextWithEllipsis(ctx, description.text, availableWidth - description.position.x), description.position.x, description.position.y);\n        }\n    }\n    if (structure.key) {\n        ctx.font = structure.key.style.font;\n        ctx.fillStyle = structure.key.style.color;\n        drawDecoratedText(ctx, clipTextWithEllipsis(ctx, structure.key.text, availableWidth - structure.key.position.x), structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);\n    }\n    if (structure.progressBar) {\n        ctx.fillStyle = structure.progressBar.style.backgroundColor;\n        ctx.beginPath();\n        ctx.roundRect(structure.progressBar.position.x, structure.progressBar.position.y, structure.progressBar.dimension.width, structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);\n        ctx.fill();\n        ctx.fillStyle = structure.progressBar.style.color;\n        ctx.beginPath();\n        if (structure.progressBar.value > 0) {\n            ctx.roundRect(structure.progressBar.position.x, structure.progressBar.position.y, structure.progressBar.dimension.width *\n                Math.max(0, Math.min(1.0, structure.progressBar.value)), structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);\n        }\n        else {\n            const width = structure.progressBar.dimension.width *\n                Math.max(0, Math.min(1.0, -structure.progressBar.value));\n            ctx.roundRect(structure.progressBar.position.x + structure.progressBar.dimension.width - width, structure.progressBar.position.y, width, structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);\n        }\n        ctx.fill();\n    }\n}\nfunction createScorecardChartRuntime(chart, getters) {\n    let formattedKeyValue = \"\";\n    let keyValueCell;\n    const locale = getters.getLocale();\n    if (chart.keyValue) {\n        const keyValuePosition = {\n            sheetId: chart.keyValue.sheetId,\n            col: chart.keyValue.zone.left,\n            row: chart.keyValue.zone.top,\n        };\n        keyValueCell = getters.getEvaluatedCell(keyValuePosition);\n        formattedKeyValue = getKeyValueText(keyValueCell, chart.humanize ?? false, locale);\n    }\n    let baselineCell;\n    const baseline = chart.baseline;\n    if (baseline) {\n        const baselinePosition = {\n            sheetId: baseline.sheetId,\n            col: baseline.zone.left,\n            row: baseline.zone.top,\n        };\n        baselineCell = getters.getEvaluatedCell(baselinePosition);\n    }\n    const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);\n    const baselineDisplay = getBaselineText(baselineCell, keyValueCell, chart.baselineMode, chart.humanize ?? false, locale);\n    const baselineValue = chart.baselineMode === \"progress\" && isNumber(baselineDisplay, locale)\n        ? toNumber(baselineDisplay, locale)\n        : 0;\n    return {\n        title: {\n            ...chart.title,\n            // chart titles are extracted from .json files and they are translated at runtime here\n            text: chart.title.text ? _t(chart.title.text) : \"\",\n        },\n        keyValue: formattedKeyValue,\n        baselineDisplay,\n        baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),\n        baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),\n        baselineDescr: chart.baselineMode !== \"progress\" && chart.baselineDescr\n            ? _t(chart.baselineDescr) // descriptions are extracted from .json files and they are translated at runtime here\n            : \"\",\n        fontColor,\n        background,\n        baselineStyle: chart.baselineMode !== \"percentage\" && chart.baselineMode !== \"progress\" && baseline\n            ? getters.getCellStyle({\n                sheetId: baseline.sheetId,\n                col: baseline.zone.left,\n                row: baseline.zone.top,\n            })\n            : undefined,\n        keyValueStyle: chart.keyValue\n            ? getters.getCellStyle({\n                sheetId: chart.keyValue.sheetId,\n                col: chart.keyValue.zone.left,\n                row: chart.keyValue.zone.top,\n            })\n            : undefined,\n        progressBar: chart.baselineMode === \"progress\"\n            ? {\n                value: baselineValue,\n                color: baselineValue > 0 ? chart.baselineColorUp : chart.baselineColorDown,\n            }\n            : undefined,\n    };\n}\n\n/* Padding at the border of the chart */\nconst CHART_PADDING = SCORECARD_GAUGE_CHART_PADDING;\nconst BOTTOM_PADDING_RATIO = 0.05;\n/* Maximum font sizes of each element */\nconst CHART_TITLE_FONT_SIZE = SCORECARD_GAUGE_CHART_FONT_SIZE;\nconst KEY_VALUE_FONT_SIZE = 32;\nconst BASELINE_MAX_FONT_SIZE = 16;\nfunction formatBaselineDescr(baselineDescr, baseline) {\n    const _baselineDescr = baselineDescr || \"\";\n    return baseline && _baselineDescr ? \" \" + _baselineDescr : _baselineDescr;\n}\nfunction getScorecardConfiguration({ width, height }, runtime) {\n    const designer = new ScorecardChartConfigBuilder({ width, height }, runtime);\n    return designer.computeDesign();\n}\nclass ScorecardChartConfigBuilder {\n    runtime;\n    context;\n    width;\n    height;\n    constructor({ width, height }, runtime) {\n        this.runtime = runtime;\n        const canvas = document.createElement(\"canvas\");\n        this.width = canvas.width = width;\n        this.height = canvas.height = height;\n        this.context = canvas.getContext(\"2d\");\n    }\n    computeDesign() {\n        const structure = {\n            canvas: {\n                width: this.width,\n                height: this.height,\n                backgroundColor: this.backgroundColor,\n            },\n        };\n        const style = this.getTextStyles();\n        let titleHeight = 0;\n        if (this.title) {\n            let x, titleWidth;\n            ({ height: titleHeight, width: titleWidth } = this.getFullTextDimensions(this.title, style.title.font));\n            switch (this.runtime.title.align) {\n                case \"center\":\n                    x = (this.width - titleWidth) / 2;\n                    break;\n                case \"right\":\n                    x = this.width - titleWidth - CHART_PADDING;\n                    break;\n                case \"left\":\n                default:\n                    x = CHART_PADDING;\n            }\n            structure.title = {\n                text: this.title,\n                style: style.title,\n                position: {\n                    x,\n                    y: CHART_PADDING + titleHeight / 2,\n                },\n            };\n        }\n        const baselineArrowSize = style.baselineArrow?.size ?? 0;\n        let { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);\n        if (!this.baseline) {\n            baselineHeight = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).height;\n        }\n        const baselineDescrWidth = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).width;\n        structure.baseline = {\n            text: this.baseline,\n            style: style.baselineValue,\n            position: {\n                x: (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2,\n                y: this.keyValue\n                    ? this.height * (1 - BOTTOM_PADDING_RATIO * (this.runtime.progressBar ? 1 : 2))\n                    : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING,\n            },\n        };\n        const minimalBaselinePosition = baselineArrowSize + DEFAULT_CHART_PADDING;\n        if (structure.baseline.position.x < minimalBaselinePosition) {\n            structure.baseline.position.x = minimalBaselinePosition;\n        }\n        if (style.baselineArrow && !this.runtime.progressBar) {\n            structure.baselineArrow = {\n                direction: this.baselineArrow,\n                style: style.baselineArrow,\n                position: {\n                    x: structure.baseline.position.x - baselineArrowSize,\n                    y: structure.baseline.position.y - (baselineHeight + baselineArrowSize) / 2,\n                },\n            };\n        }\n        if (this.baselineDescr) {\n            const position = {\n                x: structure.baseline.position.x + baselineWidth,\n                y: structure.baseline.position.y,\n            };\n            structure.baselineDescr = [\n                {\n                    text: this.baselineDescr,\n                    style: style.baselineDescr,\n                    position,\n                },\n            ];\n        }\n        let progressBarHeight = 0;\n        if (this.runtime.progressBar) {\n            progressBarHeight = this.height * 0.05;\n            structure.progressBar = {\n                position: {\n                    x: 2 * CHART_PADDING,\n                    y: this.height * (1 - 2 * BOTTOM_PADDING_RATIO) - baselineHeight - progressBarHeight,\n                },\n                dimension: {\n                    height: progressBarHeight,\n                    width: this.width - 4 * CHART_PADDING,\n                },\n                value: this.runtime.progressBar.value,\n                style: {\n                    color: this.runtime.progressBar.color,\n                    backgroundColor: this.secondaryFontColor,\n                },\n            };\n        }\n        const { width: keyWidth, height: keyHeight } = this.getFullTextDimensions(this.keyValue, style.keyValue.font);\n        if (this.keyValue) {\n            structure.key = {\n                text: this.keyValue,\n                style: style.keyValue,\n                position: {\n                    x: Math.max(CHART_PADDING, (this.width - keyWidth) / 2),\n                    y: this.height * (0.5 - BOTTOM_PADDING_RATIO * 2) +\n                        CHART_PADDING / 2 +\n                        (titleHeight + keyHeight / 2) / 2,\n                },\n            };\n        }\n        return structure;\n    }\n    get title() {\n        return this.runtime.title.text ?? \"\";\n    }\n    get keyValue() {\n        return this.runtime.keyValue;\n    }\n    get baseline() {\n        return this.runtime.baselineDisplay;\n    }\n    get baselineDescr() {\n        return formatBaselineDescr(this.runtime.baselineDescr, this.baseline);\n    }\n    get baselineArrow() {\n        return this.runtime.baselineArrow;\n    }\n    get backgroundColor() {\n        return this.runtime.background;\n    }\n    get secondaryFontColor() {\n        return relativeLuminance(this.backgroundColor) > 0.3 ? \"#525252\" : \"#C8C8C8\";\n    }\n    getTextDimensions(text, font) {\n        this.context.font = font;\n        const measure = this.context.measureText(text);\n        return {\n            width: measure.width,\n            height: measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent,\n        };\n    }\n    getFullTextDimensions(text, font) {\n        this.context.font = font;\n        const measure = this.context.measureText(text);\n        return {\n            width: measure.width,\n            height: measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent,\n        };\n    }\n    getTextStyles() {\n        let baselineValueFontSize = BASELINE_MAX_FONT_SIZE;\n        const baselineDescrFontSize = Math.floor(0.9 * baselineValueFontSize);\n        if (this.runtime.progressBar) {\n            baselineValueFontSize /= 1.5;\n        }\n        return {\n            title: {\n                font: getDefaultContextFont(CHART_TITLE_FONT_SIZE, this.runtime.title.bold, this.runtime.title.italic),\n                color: this.runtime.title.color ?? this.secondaryFontColor,\n            },\n            keyValue: {\n                color: this.runtime.keyValueStyle?.textColor || this.runtime.fontColor,\n                font: getDefaultContextFont(KEY_VALUE_FONT_SIZE, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),\n                strikethrough: this.runtime.keyValueStyle?.strikethrough,\n                underline: this.runtime.keyValueStyle?.underline,\n            },\n            baselineValue: {\n                font: getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),\n                strikethrough: this.runtime.baselineStyle?.strikethrough,\n                underline: this.runtime.baselineStyle?.underline,\n                color: this.runtime.baselineStyle?.textColor ||\n                    this.runtime.baselineColor ||\n                    this.secondaryFontColor,\n            },\n            baselineDescr: {\n                font: getDefaultContextFont(baselineDescrFontSize),\n                color: this.secondaryFontColor,\n            },\n            baselineArrow: this.baselineArrow === \"neutral\" || this.runtime.progressBar\n                ? undefined\n                : {\n                    size: this.keyValue ? 0.8 * baselineValueFontSize : 0,\n                    color: this.runtime.baselineColor || this.secondaryFontColor,\n                },\n        };\n    }\n}\n\nclass ScorecardChart extends Component {\n    static template = \"o-spreadsheet-ScorecardChart\";\n    static props = {\n        figure: Object,\n    };\n    canvas = useRef(\"chartContainer\");\n    get runtime() {\n        return this.env.model.getters.getChartRuntime(this.props.figure.id);\n    }\n    get title() {\n        const title = this.env.model.getters.getChartDefinition(this.props.figure.id).title.text ?? \"\";\n        // chart titles are extracted from .json files and they are translated at runtime here\n        return _t(title);\n    }\n    setup() {\n        useEffect(this.createChart.bind(this), () => {\n            const canvas = this.canvas.el;\n            const rect = canvas.getBoundingClientRect();\n            return [rect.width, rect.height, this.runtime, this.canvas.el];\n        });\n    }\n    createChart() {\n        const canvas = this.canvas.el;\n        const config = getScorecardConfiguration(canvas.getBoundingClientRect(), this.runtime);\n        drawScoreChart(config, canvas);\n    }\n}\n\nconst autoCompleteProviders = new Registry();\n\nautoCompleteProviders.add(\"dataValidation\", {\n    getProposals(tokenAtCursor, content) {\n        if (content.startsWith(\"=\")) {\n            return [];\n        }\n        if (!this.composer.currentEditedCell) {\n            return [];\n        }\n        const position = this.composer.currentEditedCell;\n        const rule = this.getters.getValidationRuleForCell(position);\n        if (!rule ||\n            (rule.criterion.type !== \"isValueInList\" && rule.criterion.type !== \"isValueInRange\")) {\n            return [];\n        }\n        let values;\n        if (rule.criterion.type === \"isValueInList\") {\n            values = rule.criterion.values;\n        }\n        else {\n            const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);\n            values = Array.from(new Set(this.getters\n                .getRangeValues(range)\n                .filter(isNotNull)\n                .map((value) => value.toString())\n                .filter((val) => val !== \"\")));\n        }\n        return values.map((value) => ({ text: value }));\n    },\n    selectProposal(tokenAtCursor, value) {\n        this.composer.setCurrentContent(value);\n        this.composer.stopEdition();\n    },\n});\n\nfunction getHtmlContentFromPattern(pattern, value, highlightColor, className) {\n    const pendingHtmlContent = [];\n    pattern = pattern.toLowerCase();\n    for (const patternChar of pattern) {\n        const index = value.toLocaleLowerCase().indexOf(patternChar);\n        if (index === -1) {\n            continue;\n        }\n        pendingHtmlContent.push({ value: value.slice(0, index), color: \"\" }, { value: value[index], color: highlightColor, class: className });\n        value = value.slice(index + 1);\n    }\n    pendingHtmlContent.push({ value });\n    const htmlContent = pendingHtmlContent.filter((content) => content.value);\n    return htmlContent;\n}\n\n//------------------------------------------------------------------------------\n// Arg description DSL\n//------------------------------------------------------------------------------\nconst ARG_REGEXP = /(.*?)\\((.*?)\\)(.*)/;\nconst ARG_TYPES = [\n    \"ANY\",\n    \"BOOLEAN\",\n    \"DATE\",\n    \"NUMBER\",\n    \"STRING\",\n    \"RANGE\",\n    \"RANGE<BOOLEAN>\",\n    \"RANGE<DATE>\",\n    \"RANGE<NUMBER>\",\n    \"RANGE<STRING>\",\n    \"META\",\n];\nfunction arg(definition, description = \"\") {\n    return makeArg(definition, description);\n}\nfunction makeArg(str, description) {\n    let parts = str.match(ARG_REGEXP);\n    let name = parts[1].trim();\n    if (!name) {\n        throw new Error(`Function argument definition is missing a name: '${str}'.`);\n    }\n    let types = [];\n    let isOptional = false;\n    let isRepeating = false;\n    let defaultValue;\n    for (let param of parts[2].split(\",\")) {\n        const key = param.trim().toUpperCase();\n        let type = ARG_TYPES.find((t) => key === t);\n        if (type) {\n            types.push(type);\n        }\n        else if (key === \"RANGE<ANY>\") {\n            types.push(\"RANGE\");\n        }\n        else if (key === \"OPTIONAL\") {\n            isOptional = true;\n        }\n        else if (key === \"REPEATING\") {\n            isRepeating = true;\n        }\n        else if (key.startsWith(\"DEFAULT=\")) {\n            defaultValue = param.trim().slice(8);\n        }\n    }\n    const result = {\n        name,\n        description,\n        type: types,\n    };\n    const acceptErrors = types.includes(\"ANY\") || types.includes(\"RANGE\");\n    if (acceptErrors) {\n        result.acceptErrors = true;\n    }\n    if (isOptional) {\n        result.optional = true;\n    }\n    if (isRepeating) {\n        result.repeating = true;\n    }\n    if (defaultValue !== undefined) {\n        result.default = true;\n        result.defaultValue = defaultValue;\n    }\n    if (types.some((t) => t.startsWith(\"RANGE\"))) {\n        result.acceptMatrix = true;\n    }\n    if (types.every((t) => t.startsWith(\"RANGE\"))) {\n        result.acceptMatrixOnly = true;\n    }\n    return result;\n}\n/**\n * This function adds on description more general information derived from the\n * arguments.\n *\n * This information is useful during compilation.\n */\nfunction addMetaInfoFromArg(addDescr) {\n    let countArg = 0;\n    let minArg = 0;\n    let repeatingArg = 0;\n    for (let arg of addDescr.args) {\n        countArg++;\n        if (!arg.optional && !arg.repeating && !arg.default) {\n            minArg++;\n        }\n        if (arg.repeating) {\n            repeatingArg++;\n        }\n    }\n    const descr = addDescr;\n    descr.minArgRequired = minArg;\n    descr.maxArgPossible = repeatingArg ? Infinity : countArg;\n    descr.nbrArgRepeating = repeatingArg;\n    descr.getArgToFocus = argTargeting(countArg, repeatingArg);\n    descr.hidden = addDescr.hidden || false;\n    return descr;\n}\n/**\n * Returns a function allowing finding which argument corresponds a position\n * in a function. This is particularly useful for functions with repeatable\n * arguments.\n *\n * Indeed the function makes it possible to etablish corespondance between\n * arguments when the number of arguments supplied is greater than the number of\n * arguments defined by the function.\n *\n * Ex:\n *\n * in the formula \"=SUM(11, 55, 66)\" which is defined like this \"SUM(value1, [value2, ...])\"\n * - 11 corresponds to the value1 argument => position will be 1\n * - 55 corresponds to the [value2, ...] argument => position will be 2\n * - 66 corresponds to the [value2, ...] argument => position will be 2\n *\n * in the formula \"=AVERAGE.WEIGHTED(1, 2, 3, 4, 5, 6)\" which is defined like this\n * \"AVERAGE.WEIGHTED(values, weights, [additional_values, ...], [additional_weights, ...])\"\n * - 1 corresponds to the values argument => position will be 1\n * - 2 corresponds to the weights argument => position will be 2\n * - 3 corresponds to the [additional_values, ...] argument => position will be 3\n * - 4 corresponds to the [additional_weights, ...] argument => position will be 4\n * - 5 corresponds to the [additional_values, ...] argument => position will be 3\n * - 6 corresponds to the [additional_weights, ...] argument => position will be 4\n */\nfunction argTargeting(countArg, repeatingArg) {\n    if (!repeatingArg) {\n        return (argPosition) => argPosition;\n    }\n    if (repeatingArg === 1) {\n        return (argPosition) => Math.min(argPosition, countArg);\n    }\n    const argBeforeRepeat = countArg - repeatingArg;\n    return (argPosition) => {\n        if (argPosition <= argBeforeRepeat) {\n            return argPosition;\n        }\n        const argAfterRepeat = (argPosition - argBeforeRepeat) % repeatingArg || repeatingArg;\n        return argBeforeRepeat + argAfterRepeat;\n    };\n}\n//------------------------------------------------------------------------------\n// Argument validation\n//------------------------------------------------------------------------------\nfunction validateArguments(args) {\n    let previousArgRepeating = false;\n    let previousArgOptional = false;\n    let previousArgDefault = false;\n    for (let current of args) {\n        if (current.type.includes(\"META\") && current.type.length > 1) {\n            throw new Error(_t(\"Function ${name} has an argument that has been declared with more than one type whose type 'META'. The 'META' type can only be declared alone.\"));\n        }\n        if (previousArgRepeating && !current.repeating) {\n            throw new Error(_t(\"Function ${name} has no-repeatable arguments declared after repeatable ones. All repeatable arguments must be declared last.\"));\n        }\n        const previousIsOptional = previousArgOptional || previousArgRepeating || previousArgDefault;\n        const currentIsntOptional = !(current.optional || current.repeating || current.default);\n        if (previousIsOptional && currentIsntOptional) {\n            throw new Error(_t(\"Function ${name} has at mandatory arguments declared after optional ones. All optional arguments must be after all mandatory arguments.\"));\n        }\n        previousArgRepeating = current.repeating;\n        previousArgOptional = current.optional;\n        previousArgDefault = current.default;\n    }\n}\n\nfunction assertSingleColOrRow(errorStr, arg) {\n    assert(() => arg.length === 1 || arg[0].length === 1, errorStr);\n}\nfunction assertSameDimensions(errorStr, ...args) {\n    if (args.every(isMatrix)) {\n        const cols = args[0].length;\n        const rows = args[0][0].length;\n        for (const arg of args) {\n            assert(() => arg.length === cols && arg[0].length === rows, errorStr);\n        }\n        return;\n    }\n    if (args.some((arg) => Array.isArray(arg) && (arg.length !== 1 || arg[0].length !== 1))) {\n        throw new EvaluationError(errorStr);\n    }\n}\nfunction assertPositive(errorStr, arg) {\n    assert(() => arg > 0, errorStr);\n}\nfunction assertSquareMatrix(errorStr, arg) {\n    assert(() => arg.length === arg[0].length, errorStr);\n}\n\n// -----------------------------------------------------------------------------\n// ARRAY_CONSTRAIN\n// -----------------------------------------------------------------------------\nconst ARRAY_CONSTRAIN = {\n    description: _t(\"Returns a result array constrained to a specific width and height.\"),\n    args: [\n        arg(\"input_range (any, range<any>)\", _t(\"The range to constrain.\")),\n        arg(\"rows (number)\", _t(\"The number of rows in the constrained array.\")),\n        arg(\"columns (number)\", _t(\"The number of columns in the constrained array.\")),\n    ],\n    compute: function (array, rows, columns) {\n        const _array = toMatrix(array);\n        const _rowsArg = toInteger(rows?.value, this.locale);\n        const _columnsArg = toInteger(columns?.value, this.locale);\n        assertPositive(_t(\"The rows argument (%s) must be strictly positive.\", _rowsArg.toString()), _rowsArg);\n        assertPositive(_t(\"The columns argument (%s) must be strictly positive.\", _rowsArg.toString()), _columnsArg);\n        const _nbRows = Math.min(_rowsArg, _array[0].length);\n        const _nbColumns = Math.min(_columnsArg, _array.length);\n        return generateMatrix(_nbColumns, _nbRows, (col, row) => _array[col][row]);\n    },\n    isExported: false,\n};\n// -----------------------------------------------------------------------------\n// CHOOSECOLS\n// -----------------------------------------------------------------------------\nconst CHOOSECOLS = {\n    description: _t(\"Creates a new array from the selected columns in the existing range.\"),\n    args: [\n        arg(\"array (any, range<any>)\", _t(\"The array that contains the columns to be returned.\")),\n        arg(\"col_num (number, range<number>)\", _t(\"The first column index of the columns to be returned.\")),\n        arg(\"col_num2 (number, range<number>, repeating)\", _t(\"The columns indexes of the columns to be returned.\")),\n    ],\n    compute: function (array, ...columns) {\n        const _array = toMatrix(array);\n        const _columns = flattenRowFirst(columns, (item) => toInteger(item?.value, this.locale));\n        const argOutOfRange = _columns.filter((col) => col === 0 || _array.length < Math.abs(col));\n        assert(() => argOutOfRange.length === 0, _t(\"The columns arguments must be between -%s and %s (got %s), excluding 0.\", _array.length.toString(), _array.length.toString(), argOutOfRange.join(\",\")));\n        const result = Array(_columns.length);\n        for (let col = 0; col < _columns.length; col++) {\n            if (_columns[col] > 0) {\n                result[col] = _array[_columns[col] - 1]; // -1 because columns arguments are 1-indexed\n            }\n            else {\n                result[col] = _array[_array.length + _columns[col]];\n            }\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CHOOSEROWS\n// -----------------------------------------------------------------------------\nconst CHOOSEROWS = {\n    description: _t(\"Creates a new array from the selected rows in the existing range.\"),\n    args: [\n        arg(\"array (any, range<any>)\", _t(\"The array that contains the rows to be returned.\")),\n        arg(\"row_num (number, range<number>)\", _t(\"The first row index of the rows to be returned.\")),\n        arg(\"row_num2 (number, range<number>, repeating)\", _t(\"The rows indexes of the rows to be returned.\")),\n    ],\n    compute: function (array, ...rows) {\n        const _array = toMatrix(array);\n        const _rows = flattenRowFirst(rows, (item) => toInteger(item?.value, this.locale));\n        const _nbColumns = _array.length;\n        const argOutOfRange = _rows.filter((row) => row === 0 || _array[0].length < Math.abs(row));\n        assert(() => argOutOfRange.length === 0, _t(\"The rows arguments must be between -%s and %s (got %s), excluding 0.\", _array[0].length.toString(), _array[0].length.toString(), argOutOfRange.join(\",\")));\n        return generateMatrix(_nbColumns, _rows.length, (col, row) => {\n            if (_rows[row] > 0) {\n                return _array[col][_rows[row] - 1]; // -1 because columns arguments are 1-indexed\n            }\n            return _array[col][_array[col].length + _rows[row]];\n        });\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// EXPAND\n// -----------------------------------------------------------------------------\nconst EXPAND = {\n    description: _t(\"Expands or pads an array to specified row and column dimensions.\"),\n    args: [\n        arg(\"array (any, range<any>)\", _t(\"The array to expand.\")),\n        arg(\"rows (number)\", _t(\"The number of rows in the expanded array. If missing, rows will not be expanded.\")),\n        arg(\"columns (number, optional)\", _t(\"The number of columns in the expanded array. If missing, columns will not be expanded.\")),\n        arg(\"pad_with (any, default=0)\", _t(\"The value with which to pad.\")), // @compatibility: on Excel, pad with #N/A\n    ],\n    compute: function (arg, rows, columns, padWith = { value: 0 } // TODO : Replace with #N/A errors once it's supported\n    ) {\n        const _array = toMatrix(arg);\n        const _nbRows = toInteger(rows?.value, this.locale);\n        const _nbColumns = columns !== undefined ? toInteger(columns.value, this.locale) : _array.length;\n        assert(() => _nbRows >= _array[0].length, _t(\"The rows arguments (%s) must be greater or equal than the number of rows of the array.\", _nbRows.toString()));\n        assert(() => _nbColumns >= _array.length, _t(\"The columns arguments (%s) must be greater or equal than the number of columns of the array.\", _nbColumns.toString()));\n        return generateMatrix(_nbColumns, _nbRows, (col, row) => col >= _array.length || row >= _array[col].length ? padWith : _array[col][row]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FLATTEN\n// -----------------------------------------------------------------------------\nconst FLATTEN = {\n    description: _t(\"Flattens all the values from one or more ranges into a single column.\"),\n    args: [\n        arg(\"range (any, range<any>)\", _t(\"The first range to flatten.\")),\n        arg(\"range2 (any, range<any>, repeating)\", _t(\"Additional ranges to flatten.\")),\n    ],\n    compute: function (...ranges) {\n        return [flattenRowFirst(ranges, (val) => (val === undefined ? { value: \"\" } : val))];\n    },\n    isExported: false,\n};\n// -----------------------------------------------------------------------------\n// FREQUENCY\n// -----------------------------------------------------------------------------\nconst FREQUENCY = {\n    description: _t(\"Calculates the frequency distribution of a range.\"),\n    args: [\n        arg(\"data (range<number>)\", _t(\"The array of ranges containing the values to be counted.\")),\n        arg(\"classes (number, range<number>)\", _t(\"The range containing the set of classes.\")),\n    ],\n    compute: function (data, classes) {\n        const _data = flattenRowFirst([data], (data) => data.value).filter((val) => typeof val === \"number\");\n        const _classes = flattenRowFirst([classes], (data) => data.value).filter((val) => typeof val === \"number\");\n        /**\n         * Returns the frequency distribution of the data in the classes, ie. the number of elements in the range\n         * between each classes.\n         *\n         * For example:\n         * - data = [1, 3, 2, 5, 4]\n         * - classes = [3, 5, 1]\n         *\n         * The result will be:\n         * - 2 ==> number of elements 1 > el >= 3\n         * - 2 ==> number of elements 3 > el >= 5\n         * - 1 ==> number of elements <= 1\n         * - 0 ==> number of elements > 5\n         *\n         * @compatibility: GSheet sort the input classes. We do the implemntation of Excel, where we kee the classes unsorted.\n         */\n        const sortedClasses = _classes\n            .map((value, index) => ({ initialIndex: index, value, count: 0 }))\n            .sort((a, b) => a.value - b.value);\n        sortedClasses.push({ initialIndex: sortedClasses.length, value: Infinity, count: 0 });\n        const sortedData = _data.sort((a, b) => a - b);\n        let index = 0;\n        for (const val of sortedData) {\n            while (val > sortedClasses[index].value && index < sortedClasses.length - 1) {\n                index++;\n            }\n            sortedClasses[index].count++;\n        }\n        const result = sortedClasses\n            .sort((a, b) => a.initialIndex - b.initialIndex)\n            .map((val) => val.count);\n        return [result];\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// HSTACK\n// -----------------------------------------------------------------------------\nconst HSTACK = {\n    description: _t(\"Appends ranges horizontally and in sequence to return a larger array.\"),\n    args: [\n        arg(\"range1 (any, range<any>)\", _t(\"The first range to be appended.\")),\n        arg(\"range2 (any, range<any>, repeating)\", _t(\"Additional ranges to add to range1.\")),\n    ],\n    compute: function (...ranges) {\n        const nbRows = Math.max(...ranges.map((r) => r?.[0]?.length ?? 0));\n        const result = [];\n        for (const range of ranges) {\n            const _range = toMatrix(range);\n            for (let col = 0; col < _range.length; col++) {\n                //TODO: fill with #N/A for unavailable values instead of zeroes\n                const array = Array(nbRows).fill({ value: null });\n                for (let row = 0; row < _range[col].length; row++) {\n                    array[row] = _range[col][row];\n                }\n                result.push(array);\n            }\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MDETERM\n// -----------------------------------------------------------------------------\nconst MDETERM = {\n    description: _t(\"Returns the matrix determinant of a square matrix.\"),\n    args: [\n        arg(\"square_matrix (number, range<number>)\", _t(\"An range with an equal number of rows and columns representing a matrix whose determinant will be calculated.\")),\n    ],\n    compute: function (matrix) {\n        const _matrix = toNumberMatrix(matrix, \"square_matrix\");\n        assertSquareMatrix(_t(\"The argument square_matrix must have the same number of columns and rows.\"), _matrix);\n        return invertMatrix(_matrix).determinant;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MINVERSE\n// -----------------------------------------------------------------------------\nconst MINVERSE = {\n    description: _t(\"Returns the multiplicative inverse of a square matrix.\"),\n    args: [\n        arg(\"square_matrix (number, range<number>)\", _t(\"An range with an equal number of rows and columns representing a matrix whose multiplicative inverse will be calculated.\")),\n    ],\n    compute: function (matrix) {\n        const _matrix = toNumberMatrix(matrix, \"square_matrix\");\n        assertSquareMatrix(_t(\"The argument square_matrix must have the same number of columns and rows.\"), _matrix);\n        const { inverted } = invertMatrix(_matrix);\n        if (!inverted) {\n            throw new EvaluationError(_t(\"The matrix is not invertible.\"));\n        }\n        return inverted;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MMULT\n// -----------------------------------------------------------------------------\nconst MMULT = {\n    description: _t(\"Calculates the matrix product of two matrices.\"),\n    args: [\n        arg(\"matrix1 (number, range<number>)\", _t(\"The first matrix in the matrix multiplication operation.\")),\n        arg(\"matrix2 (number, range<number>)\", _t(\"The second matrix in the matrix multiplication operation.\")),\n    ],\n    compute: function (matrix1, matrix2) {\n        const _matrix1 = toNumberMatrix(matrix1, \"matrix1\");\n        const _matrix2 = toNumberMatrix(matrix2, \"matrix2\");\n        assert(() => _matrix1.length === _matrix2[0].length, _t(\"In [[FUNCTION_NAME]], the number of columns of the first matrix (%s) must be equal to the \\\n        number of rows of the second matrix (%s).\", _matrix1.length.toString(), _matrix2[0].length.toString()));\n        return multiplyMatrices(_matrix1, _matrix2);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SUMPRODUCT\n// -----------------------------------------------------------------------------\nconst SUMPRODUCT = {\n    description: _t(\"Calculates the sum of the products of corresponding entries in equal-sized ranges.\"),\n    args: [\n        arg(\"range1 (number, range<number>)\", _t(\"The first range whose entries will be multiplied with corresponding entries in the other ranges.\")),\n        arg(\"range2 (number, range<number>, repeating)\", _t(\"The other range whose entries will be multiplied with corresponding entries in the other ranges.\")),\n    ],\n    compute: function (...args) {\n        assertSameDimensions(_t(\"All the ranges must have the same dimensions.\"), ...args);\n        const _args = args.map(toMatrix);\n        let result = 0;\n        for (let col = 0; col < _args[0].length; col++) {\n            for (let row = 0; row < _args[0][col].length; row++) {\n                if (!_args.every((range) => typeof range[col][row].value === \"number\")) {\n                    continue;\n                }\n                let product = 1;\n                for (const range of _args) {\n                    product *= toNumber(range[col][row], this.locale);\n                }\n                result += product;\n            }\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SUMX2MY2\n// -----------------------------------------------------------------------------\n/**\n * Return the sum of the callback applied to each pair of values in the two arrays.\n *\n * Ignore the pairs X,Y where one of the value isn't a number. Throw an error if no pair of numbers is found.\n */\nfunction getSumXAndY(arrayX, arrayY, cb) {\n    assertSameDimensions(\"The arguments array_x and array_y must have the same dimensions.\", arrayX, arrayY);\n    const _arrayX = toMatrix(arrayX);\n    const _arrayY = toMatrix(arrayY);\n    let validPairFound = false;\n    let result = 0;\n    for (const col in _arrayX) {\n        for (const row in _arrayX[col]) {\n            const arrayXValue = _arrayX[col][row].value;\n            const arrayYValue = _arrayY[col][row].value;\n            if (typeof arrayXValue !== \"number\" || typeof arrayYValue !== \"number\") {\n                continue;\n            }\n            validPairFound = true;\n            result += cb(arrayXValue, arrayYValue);\n        }\n    }\n    if (!validPairFound) {\n        throw new EvaluationError(_t(\"The arguments array_x and array_y must contain at least one pair of numbers.\"));\n    }\n    return result;\n}\nconst SUMX2MY2 = {\n    description: _t(\"Calculates the sum of the difference of the squares of the values in two array.\"),\n    args: [\n        arg(\"array_x (number, range<number>)\", _t(\"The array or range of values whose squares will be reduced by the squares of corresponding entries in array_y and added together.\")),\n        arg(\"array_y (number, range<number>)\", _t(\"The array or range of values whose squares will be subtracted from the squares of corresponding entries in array_x and added together.\")),\n    ],\n    compute: function (arrayX, arrayY) {\n        return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 - y ** 2);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SUMX2PY2\n// -----------------------------------------------------------------------------\nconst SUMX2PY2 = {\n    description: _t(\"Calculates the sum of the sum of the squares of the values in two array.\"),\n    args: [\n        arg(\"array_x (number, range<number>)\", _t(\"The array or range of values whose squares will be added to the squares of corresponding entries in array_y and added together.\")),\n        arg(\"array_y (number, range<number>)\", _t(\"The array or range of values whose squares will be added to the squares of corresponding entries in array_x and added together.\")),\n    ],\n    compute: function (arrayX, arrayY) {\n        return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 + y ** 2);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SUMXMY2\n// -----------------------------------------------------------------------------\nconst SUMXMY2 = {\n    description: _t(\"Calculates the sum of squares of the differences of values in two array.\"),\n    args: [\n        arg(\"array_x (number, range<number>)\", _t(\"The array or range of values that will be reduced by corresponding entries in array_y, squared, and added together.\")),\n        arg(\"array_y (number, range<number>)\", _t(\"The array or range of values that will be subtracted from corresponding entries in array_x, the result squared, and all such results added together.\")),\n    ],\n    compute: function (arrayX, arrayY) {\n        return getSumXAndY(arrayX, arrayY, (x, y) => (x - y) ** 2);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TOCOL\n// -----------------------------------------------------------------------------\nconst TO_COL_ROW_DEFAULT_IGNORE = 0;\nconst TO_COL_ROW_DEFAULT_SCAN = false;\nconst TO_COL_ROW_ARGS = [\n    arg(\"array (any, range<any>)\", _t(\"The array which will be transformed.\")),\n    arg(`ignore (number, default=${TO_COL_ROW_DEFAULT_IGNORE})`, _t(\"The control to ignore blanks and errors. 0 (default) is to keep all values, 1 is to ignore blanks, 2 is to ignore errors, and 3 is to ignore blanks and errors.\")),\n    arg(`scan_by_column (number, default=${TO_COL_ROW_DEFAULT_SCAN})`, _t(\"Whether the array should be scanned by column. True scans the array by column and false (default) \\\n      scans the array by row.\")),\n];\nfunction shouldKeepValue(ignore) {\n    const _ignore = Math.trunc(ignore);\n    if (_ignore === 0) {\n        return () => true;\n    }\n    if (_ignore === 1) {\n        return (data) => data.value !== null;\n    }\n    if (_ignore === 2) {\n        return (data) => !isEvaluationError(data.value);\n    }\n    if (_ignore === 3) {\n        return (data) => data.value !== null && !isEvaluationError(data.value);\n    }\n    throw new EvaluationError(_t(\"Argument ignore must be between 0 and 3\"));\n}\nconst TOCOL = {\n    description: _t(\"Transforms a range of cells into a single column.\"),\n    args: TO_COL_ROW_ARGS,\n    compute: function (array, ignore = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn = { value: TO_COL_ROW_DEFAULT_SCAN }) {\n        const _array = toMatrix(array);\n        const _ignore = toNumber(ignore.value, this.locale);\n        const _scanByColumn = toBoolean(scanByColumn.value);\n        const result = (_scanByColumn ? _array : transposeMatrix(_array))\n            .flat()\n            .filter(shouldKeepValue(_ignore));\n        if (result.length === 0) {\n            throw new NotAvailableError(_t(\"No results for the given arguments of TOCOL.\"));\n        }\n        return [result];\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TOROW\n// -----------------------------------------------------------------------------\nconst TOROW = {\n    description: _t(\"Transforms a range of cells into a single row.\"),\n    args: TO_COL_ROW_ARGS,\n    compute: function (array, ignore = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn = { value: TO_COL_ROW_DEFAULT_SCAN }) {\n        const _array = toMatrix(array);\n        const _ignore = toNumber(ignore.value, this.locale);\n        const _scanByColumn = toBoolean(scanByColumn.value);\n        const result = (_scanByColumn ? _array : transposeMatrix(_array))\n            .flat()\n            .filter(shouldKeepValue(_ignore))\n            .map((item) => [item]);\n        if (result.length === 0 || result[0].length === 0) {\n            throw new NotAvailableError(_t(\"No results for the given arguments of TOROW.\"));\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TRANSPOSE\n// -----------------------------------------------------------------------------\nconst TRANSPOSE = {\n    description: _t(\"Transposes the rows and columns of a range.\"),\n    args: [arg(\"range (any, range<any>)\", _t(\"The range to be transposed.\"))],\n    compute: function (arg) {\n        const _array = toMatrix(arg);\n        const nbColumns = _array[0].length;\n        const nbRows = _array.length;\n        return generateMatrix(nbColumns, nbRows, (col, row) => _array[row][col]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// VSTACK\n// -----------------------------------------------------------------------------\nconst VSTACK = {\n    description: _t(\"Appends ranges vertically and in sequence to return a larger array.\"),\n    args: [\n        arg(\"range1 (any, range<any>)\", _t(\"The first range to be appended.\")),\n        arg(\"range2 (any, range<any>, repeating)\", _t(\"Additional ranges to add to range1.\")),\n    ],\n    compute: function (...ranges) {\n        const nbColumns = Math.max(...ranges.map((range) => toMatrix(range).length));\n        const nbRows = ranges.reduce((acc, range) => acc + toMatrix(range)[0].length, 0);\n        const result = Array(nbColumns)\n            .fill([])\n            .map(() => Array(nbRows).fill({ value: 0 })); // TODO fill with #N/A\n        let currentRow = 0;\n        for (const range of ranges) {\n            const _array = toMatrix(range);\n            for (let col = 0; col < _array.length; col++) {\n                for (let row = 0; row < _array[col].length; row++) {\n                    result[col][currentRow + row] = _array[col][row];\n                }\n            }\n            currentRow += _array[0].length;\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// WRAPCOLS\n// -----------------------------------------------------------------------------\nconst WRAPCOLS = {\n    description: _t(\"Wraps the provided row or column of cells by columns after a specified number of elements to form a new array.\"),\n    args: [\n        arg(\"range (any, range<any>)\", _t(\"The range to wrap.\")),\n        arg(\"wrap_count (number)\", _t(\"The maximum number of cells for each column, rounded down to the nearest whole number.\")),\n        arg(\"pad_with  (any, default=0)\", // TODO : replace with #N/A\n        _t(\"The value with which to fill the extra cells in the range.\")),\n    ],\n    compute: function (range, wrapCount, padWith = { value: 0 }) {\n        const _array = toMatrix(range);\n        const nbRows = toInteger(wrapCount?.value, this.locale);\n        assertSingleColOrRow(_t(\"Argument range must be a single row or column.\"), _array);\n        const array = _array.flat();\n        const nbColumns = Math.ceil(array.length / nbRows);\n        return generateMatrix(nbColumns, nbRows, (col, row) => {\n            const index = col * nbRows + row;\n            return index < array.length ? array[index] : padWith;\n        });\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// WRAPROWS\n// -----------------------------------------------------------------------------\nconst WRAPROWS = {\n    description: _t(\"Wraps the provided row or column of cells by rows after a specified number of elements to form a new array.\"),\n    args: [\n        arg(\"range (any, range<any>)\", _t(\"The range to wrap.\")),\n        arg(\"wrap_count (number)\", _t(\"The maximum number of cells for each row, rounded down to the nearest whole number.\")),\n        arg(\"pad_with  (any, default=0)\", // TODO : replace with #N/A\n        _t(\"The value with which to fill the extra cells in the range.\")),\n    ],\n    compute: function (range, wrapCount, padWith = { value: 0 }) {\n        const _array = toMatrix(range);\n        const nbColumns = toInteger(wrapCount?.value, this.locale);\n        assertSingleColOrRow(_t(\"Argument range must be a single row or column.\"), _array);\n        const array = _array.flat();\n        const nbRows = Math.ceil(array.length / nbColumns);\n        return generateMatrix(nbColumns, nbRows, (col, row) => {\n            const index = row * nbColumns + col;\n            return index < array.length ? array[index] : padWith;\n        });\n    },\n    isExported: true,\n};\n\nvar array = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    ARRAY_CONSTRAIN: ARRAY_CONSTRAIN,\n    CHOOSECOLS: CHOOSECOLS,\n    CHOOSEROWS: CHOOSEROWS,\n    EXPAND: EXPAND,\n    FLATTEN: FLATTEN,\n    FREQUENCY: FREQUENCY,\n    HSTACK: HSTACK,\n    MDETERM: MDETERM,\n    MINVERSE: MINVERSE,\n    MMULT: MMULT,\n    SUMPRODUCT: SUMPRODUCT,\n    SUMX2MY2: SUMX2MY2,\n    SUMX2PY2: SUMX2PY2,\n    SUMXMY2: SUMXMY2,\n    TOCOL: TOCOL,\n    TOROW: TOROW,\n    TRANSPOSE: TRANSPOSE,\n    VSTACK: VSTACK,\n    WRAPCOLS: WRAPCOLS,\n    WRAPROWS: WRAPROWS\n});\n\n// -----------------------------------------------------------------------------\n// FORMAT.LARGE.NUMBER\n// -----------------------------------------------------------------------------\nconst FORMAT_LARGE_NUMBER = {\n    description: _t(\"Apply a large number format\"),\n    args: [\n        arg(\"value (number)\", _t(\"The number.\")),\n        arg(\"unit (string, optional)\", _t(\"The formatting unit. Use 'k', 'm', or 'b' to force the unit\")),\n    ],\n    compute: function (value, unite) {\n        return {\n            value: toNumber(value, this.locale),\n            format: formatLargeNumber(value, unite, this.locale),\n        };\n    },\n};\n\nvar misc = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    FORMAT_LARGE_NUMBER: FORMAT_LARGE_NUMBER\n});\n\nconst DEFAULT_FACTOR = 1;\nconst DEFAULT_MODE = 0;\nconst DEFAULT_PLACES = 0;\nconst DEFAULT_SIGNIFICANCE = 1;\nconst DECIMAL_REPRESENTATION = /^-?[a-z0-9]+$/i;\n// -----------------------------------------------------------------------------\n// ABS\n// -----------------------------------------------------------------------------\nconst ABS = {\n    description: _t(\"Absolute value of a number.\"),\n    args: [arg(\"value (number)\", _t(\"The number of which to return the absolute value.\"))],\n    compute: function (value) {\n        return Math.abs(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ACOS\n// -----------------------------------------------------------------------------\nconst ACOS = {\n    description: _t(\"Inverse cosine of a value, in radians.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value for which to calculate the inverse cosine. Must be between -1 and 1, inclusive.\")),\n    ],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assert(() => Math.abs(_value) <= 1, _t(\"The value (%s) must be between -1 and 1 inclusive.\", _value.toString()));\n        return Math.acos(_value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ACOSH\n// -----------------------------------------------------------------------------\nconst ACOSH = {\n    description: _t(\"Inverse hyperbolic cosine of a number.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value for which to calculate the inverse hyperbolic cosine. Must be greater than or equal to 1.\")),\n    ],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assert(() => _value >= 1, _t(\"The value (%s) must be greater than or equal to 1.\", _value.toString()));\n        return Math.acosh(_value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ACOT\n// -----------------------------------------------------------------------------\nconst ACOT = {\n    description: _t(\"Inverse cotangent of a value.\"),\n    args: [arg(\"value (number)\", _t(\"The value for which to calculate the inverse cotangent.\"))],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        const sign = Math.sign(_value) || 1;\n        // ACOT has two possible configurations:\n        // @compatibility Excel: return Math.PI / 2 - Math.atan(toNumber(_value, this.locale));\n        // @compatibility Google: return sign * Math.PI / 2 - Math.atan(toNumber(_value, this.locale));\n        return (sign * Math.PI) / 2 - Math.atan(_value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ACOTH\n// -----------------------------------------------------------------------------\nconst ACOTH = {\n    description: _t(\"Inverse hyperbolic cotangent of a value.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value for which to calculate the inverse hyperbolic cotangent. Must not be between -1 and 1, inclusive.\")),\n    ],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assert(() => Math.abs(_value) > 1, _t(\"The value (%s) cannot be between -1 and 1 inclusive.\", _value.toString()));\n        return Math.log((_value + 1) / (_value - 1)) / 2;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ASIN\n// -----------------------------------------------------------------------------\nconst ASIN = {\n    description: _t(\"Inverse sine of a value, in radians.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value for which to calculate the inverse sine. Must be between -1 and 1, inclusive.\")),\n    ],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assert(() => Math.abs(_value) <= 1, _t(\"The value (%s) must be between -1 and 1 inclusive.\", _value.toString()));\n        return Math.asin(_value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ASINH\n// -----------------------------------------------------------------------------\nconst ASINH = {\n    description: _t(\"Inverse hyperbolic sine of a number.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value for which to calculate the inverse hyperbolic sine.\")),\n    ],\n    compute: function (value) {\n        return Math.asinh(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ATAN\n// -----------------------------------------------------------------------------\nconst ATAN = {\n    description: _t(\"Inverse tangent of a value, in radians.\"),\n    args: [arg(\"value (number)\", _t(\"The value for which to calculate the inverse tangent.\"))],\n    compute: function (value) {\n        return Math.atan(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ATAN2\n// -----------------------------------------------------------------------------\nconst ATAN2 = {\n    description: _t(\"Angle from the X axis to a point (x,y), in radians.\"),\n    args: [\n        arg(\"x (number)\", _t(\"The x coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.\")),\n        arg(\"y (number)\", _t(\"The y coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.\")),\n    ],\n    compute: function (x, y) {\n        const _x = toNumber(x, this.locale);\n        const _y = toNumber(y, this.locale);\n        assert(() => _x !== 0 || _y !== 0, _t(\"Function [[FUNCTION_NAME]] caused a divide by zero error.\"), CellErrorType.DivisionByZero);\n        return Math.atan2(_y, _x);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ATANH\n// -----------------------------------------------------------------------------\nconst ATANH = {\n    description: _t(\"Inverse hyperbolic tangent of a number.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value for which to calculate the inverse hyperbolic tangent. Must be between -1 and 1, exclusive.\")),\n    ],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assert(() => Math.abs(_value) < 1, _t(\"The value (%s) must be between -1 and 1 exclusive.\", _value.toString()));\n        return Math.atanh(_value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CEILING\n// -----------------------------------------------------------------------------\nconst CEILING = {\n    description: _t(\"Rounds number up to nearest multiple of factor.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value to round up to the nearest integer multiple of factor.\")),\n        arg(`factor (number, default=${DEFAULT_FACTOR})`, _t(\"The number to whose multiples value will be rounded.\")),\n    ],\n    compute: function (value, factor = { value: DEFAULT_FACTOR }) {\n        const _value = toNumber(value, this.locale);\n        const _factor = toNumber(factor, this.locale);\n        assert(() => _factor >= 0 || _value <= 0, _t(\"The factor (%s) must be positive when the value (%s) is positive.\", _factor.toString(), _value.toString()));\n        return {\n            value: _factor ? Math.ceil(_value / _factor) * _factor : 0,\n            format: value?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CEILING.MATH\n// -----------------------------------------------------------------------------\nfunction ceilingMath(number, significance, mode = 0) {\n    if (significance === 0) {\n        return 0;\n    }\n    significance = Math.abs(significance);\n    if (number >= 0) {\n        return Math.ceil(number / significance) * significance;\n    }\n    if (mode === 0) {\n        return -Math.floor(Math.abs(number) / significance) * significance;\n    }\n    return -Math.ceil(Math.abs(number) / significance) * significance;\n}\nconst CEILING_MATH = {\n    description: _t(\"Rounds number up to nearest multiple of factor.\"),\n    args: [\n        arg(\"number (number)\", _t(\"The value to round up to the nearest integer multiple of significance.\")),\n        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t(\"The number to whose multiples number will be rounded. The sign of significance will be ignored.\")),\n        arg(`mode (number, default=${DEFAULT_MODE})`, _t(\"If number is negative, specifies the rounding direction. If 0 or blank, it is rounded towards zero. Otherwise, it is rounded away from zero.\")),\n    ],\n    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }, mode = { value: DEFAULT_MODE }) {\n        const _significance = toNumber(significance, this.locale);\n        const _number = toNumber(number, this.locale);\n        const _mode = toNumber(mode, this.locale);\n        return {\n            value: ceilingMath(_number, _significance, _mode),\n            format: number?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CEILING.PRECISE\n// -----------------------------------------------------------------------------\nconst CEILING_PRECISE = {\n    description: _t(\"Rounds number up to nearest multiple of factor.\"),\n    args: [\n        arg(\"number (number)\", _t(\"The value to round up to the nearest integer multiple of significance.\")),\n        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t(\"The number to whose multiples number will be rounded.\")),\n    ],\n    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }) {\n        const _significance = toNumber(significance, this.locale);\n        const _number = toNumber(number, this.locale);\n        return {\n            value: ceilingMath(_number, _significance),\n            format: number?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COS\n// -----------------------------------------------------------------------------\nconst COS = {\n    description: _t(\"Cosine of an angle provided in radians.\"),\n    args: [arg(\"angle (number)\", _t(\"The angle to find the cosine of, in radians.\"))],\n    compute: function (angle) {\n        return Math.cos(toNumber(angle, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COSH\n// -----------------------------------------------------------------------------\nconst COSH = {\n    description: _t(\"Hyperbolic cosine of any real number.\"),\n    args: [arg(\"value (number)\", _t(\"Any real value to calculate the hyperbolic cosine of.\"))],\n    compute: function (value) {\n        return Math.cosh(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COT\n// -----------------------------------------------------------------------------\nconst COT = {\n    description: _t(\"Cotangent of an angle provided in radians.\"),\n    args: [arg(\"angle (number)\", _t(\"The angle to find the cotangent of, in radians.\"))],\n    compute: function (angle) {\n        const _angle = toNumber(angle, this.locale);\n        assertNotZero(_angle);\n        return 1 / Math.tan(_angle);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COTH\n// -----------------------------------------------------------------------------\nconst COTH = {\n    description: _t(\"Hyperbolic cotangent of any real number.\"),\n    args: [arg(\"value (number)\", _t(\"Any real value to calculate the hyperbolic cotangent of.\"))],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assertNotZero(_value);\n        return 1 / Math.tanh(_value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUNTBLANK\n// -----------------------------------------------------------------------------\nconst COUNTBLANK = {\n    description: _t(\"Number of empty values.\"),\n    args: [\n        arg(\"value1 (any, range)\", _t(\"The first value or range in which to count the number of blanks.\")),\n        arg(\"value2 (any, range, repeating)\", _t(\"Additional values or ranges in which to count the number of blanks.\")),\n    ],\n    compute: function (...args) {\n        return reduceAny(args, (acc, a) => {\n            if (a === undefined) {\n                return acc + 1;\n            }\n            if (a.value === null) {\n                return acc + 1;\n            }\n            if (a.value === \"\") {\n                return acc + 1;\n            }\n            return acc;\n        }, 0);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUNTIF\n// -----------------------------------------------------------------------------\nconst COUNTIF = {\n    description: _t(\"A conditional count across a range.\"),\n    args: [\n        arg(\"range (range)\", _t(\"The range that is tested against criterion.\")),\n        arg(\"criterion (string)\", _t(\"The pattern or test to apply to range.\")),\n    ],\n    compute: function (...args) {\n        let count = 0;\n        visitMatchingRanges(args, (i, j) => {\n            count += 1;\n        }, this.locale);\n        return count;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUNTIFS\n// -----------------------------------------------------------------------------\nconst COUNTIFS = {\n    description: _t(\"Count values depending on multiple criteria.\"),\n    args: [\n        arg(\"criteria_range1 (range)\", _t(\"The range to check against criterion1.\")),\n        arg(\"criterion1 (string)\", _t(\"The pattern or test to apply to criteria_range1.\")),\n        arg(\"criteria_range2 (any, range, repeating)\", _t(\"Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.\")),\n        arg(\"criterion2 (string, repeating)\", _t(\"Additional criteria to check.\")),\n    ],\n    compute: function (...args) {\n        let count = 0;\n        visitMatchingRanges(args, (i, j) => {\n            count += 1;\n        }, this.locale);\n        return count;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUNTUNIQUE\n// -----------------------------------------------------------------------------\nconst COUNTUNIQUE = {\n    description: _t(\"Counts number of unique values in a range.\"),\n    args: [\n        arg(\"value1 (any, range)\", _t(\"The first value or range to consider for uniqueness.\")),\n        arg(\"value2 (any, range, repeating)\", _t(\"Additional values or ranges to consider for uniqueness.\")),\n    ],\n    compute: function (...args) {\n        return countUnique(args);\n    },\n};\n// -----------------------------------------------------------------------------\n// COUNTUNIQUEIFS\n// -----------------------------------------------------------------------------\nconst COUNTUNIQUEIFS = {\n    description: _t(\"Counts number of unique values in a range, filtered by a set of criteria.\"),\n    args: [\n        arg(\"range (range)\", _t(\"The range of cells from which the number of unique values will be counted.\")),\n        arg(\"criteria_range1 (range)\", _t(\"The range of cells over which to evaluate criterion1.\")),\n        arg(\"criterion1 (string)\", _t(\"The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.\")),\n        arg(\"criteria_range2 (any, range, repeating)\", _t(\"Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.\")),\n        arg(\"criterion2 (string, repeating)\", _t(\"The pattern or test to apply to criteria_range2.\")),\n    ],\n    compute: function (range, ...args) {\n        let uniqueValues = new Set();\n        visitMatchingRanges(args, (i, j) => {\n            const data = range[i]?.[j];\n            if (isDataNonEmpty(data)) {\n                uniqueValues.add(data.value);\n            }\n        }, this.locale);\n        return uniqueValues.size;\n    },\n};\n// -----------------------------------------------------------------------------\n// CSC\n// -----------------------------------------------------------------------------\nconst CSC = {\n    description: _t(\"Cosecant of an angle provided in radians.\"),\n    args: [arg(\"angle (number)\", _t(\"The angle to find the cosecant of, in radians.\"))],\n    compute: function (angle) {\n        const _angle = toNumber(angle, this.locale);\n        assertNotZero(_angle);\n        return 1 / Math.sin(_angle);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CSCH\n// -----------------------------------------------------------------------------\nconst CSCH = {\n    description: _t(\"Hyperbolic cosecant of any real number.\"),\n    args: [arg(\"value (number)\", _t(\"Any real value to calculate the hyperbolic cosecant of.\"))],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assertNotZero(_value);\n        return 1 / Math.sinh(_value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DECIMAL\n// -----------------------------------------------------------------------------\nconst DECIMAL = {\n    description: _t(\"Converts from another base to decimal.\"),\n    args: [\n        arg(\"value (string)\", _t(\"The number to convert.\")),\n        arg(\"base (number)\", _t(\"The base to convert the value from.\")),\n    ],\n    compute: function (value, base) {\n        let _base = toNumber(base, this.locale);\n        _base = Math.floor(_base);\n        assert(() => 2 <= _base && _base <= 36, _t(\"The base (%s) must be between 2 and 36 inclusive.\", _base.toString()));\n        const _value = toString(value);\n        if (_value === \"\") {\n            return 0;\n        }\n        /**\n         * @compatibility: on Google sheets, expects the parameter 'value' to be positive.\n         * Return error if 'value' is positive.\n         * Remove '-?' in the next regex to catch this error.\n         */\n        assert(() => !!DECIMAL_REPRESENTATION.test(_value), _t(\"The value (%s) must be a valid base %s representation.\", _value, _base.toString()));\n        const deci = parseInt(_value, _base);\n        assert(() => !isNaN(deci), _t(\"The value (%s) must be a valid base %s representation.\", _value, _base.toString()));\n        return deci;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DEGREES\n// -----------------------------------------------------------------------------\nconst DEGREES = {\n    description: _t(\"Converts an angle value in radians to degrees.\"),\n    args: [arg(\"angle (number)\", _t(\"The angle to convert from radians to degrees.\"))],\n    compute: function (angle) {\n        return (toNumber(angle, this.locale) * 180) / Math.PI;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// EXP\n// -----------------------------------------------------------------------------\nconst EXP = {\n    description: _t(\"Euler's number, e (~2.718) raised to a power.\"),\n    args: [arg(\"value (number)\", _t(\"The exponent to raise e.\"))],\n    compute: function (value) {\n        return Math.exp(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FLOOR\n// -----------------------------------------------------------------------------\nconst FLOOR = {\n    description: _t(\"Rounds number down to nearest multiple of factor.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value to round down to the nearest integer multiple of factor.\")),\n        arg(`factor (number, default=${DEFAULT_FACTOR})`, _t(\"The number to whose multiples value will be rounded.\")),\n    ],\n    compute: function (value, factor = { value: DEFAULT_FACTOR }) {\n        const _value = toNumber(value, this.locale);\n        const _factor = toNumber(factor, this.locale);\n        assert(() => _factor >= 0 || _value <= 0, _t(\"The factor (%s) must be positive when the value (%s) is positive.\", _factor.toString(), _value.toString()));\n        return {\n            value: _factor ? Math.floor(_value / _factor) * _factor : 0,\n            format: value?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FLOOR.MATH\n// -----------------------------------------------------------------------------\nfunction floorMath(number, significance, mode = 0) {\n    if (significance === 0) {\n        return 0;\n    }\n    significance = Math.abs(significance);\n    if (number >= 0) {\n        return Math.floor(number / significance) * significance;\n    }\n    if (mode === 0) {\n        return -Math.ceil(Math.abs(number) / significance) * significance;\n    }\n    return -Math.floor(Math.abs(number) / significance) * significance;\n}\nconst FLOOR_MATH = {\n    description: _t(\"Rounds number down to nearest multiple of factor.\"),\n    args: [\n        arg(\"number (number)\", _t(\"The value to round down to the nearest integer multiple of significance.\")),\n        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t(\"The number to whose multiples number will be rounded. The sign of significance will be ignored.\")),\n        arg(`mode (number, default=${DEFAULT_MODE})`, _t(\"If number is negative, specifies the rounding direction. If 0 or blank, it is rounded away from zero. Otherwise, it is rounded towards zero.\")),\n    ],\n    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }, mode = { value: DEFAULT_MODE }) {\n        const _significance = toNumber(significance, this.locale);\n        const _number = toNumber(number, this.locale);\n        const _mode = toNumber(mode, this.locale);\n        return {\n            value: floorMath(_number, _significance, _mode),\n            format: number?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FLOOR.PRECISE\n// -----------------------------------------------------------------------------\nconst FLOOR_PRECISE = {\n    description: _t(\"Rounds number down to nearest multiple of factor.\"),\n    args: [\n        arg(\"number (number)\", _t(\"The value to round down to the nearest integer multiple of significance.\")),\n        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t(\"The number to whose multiples number will be rounded.\")),\n    ],\n    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }) {\n        const _significance = toNumber(significance, this.locale);\n        const _number = toNumber(number, this.locale);\n        return {\n            value: floorMath(_number, _significance),\n            format: number?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISEVEN\n// -----------------------------------------------------------------------------\nconst ISEVEN = {\n    description: _t(\"Whether the provided value is even.\"),\n    args: [arg(\"value (number)\", _t(\"The value to be verified as even.\"))],\n    compute: function (value) {\n        const _value = strictToNumber(value, this.locale);\n        return Math.floor(Math.abs(_value)) & 1 ? false : true;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISO.CEILING\n// -----------------------------------------------------------------------------\nconst ISO_CEILING = {\n    description: _t(\"Rounds number up to nearest multiple of factor.\"),\n    args: [\n        arg(\"number (number)\", _t(\"The value to round up to the nearest integer multiple of significance.\")),\n        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t(\"The number to whose multiples number will be rounded.\")),\n    ],\n    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }) {\n        const _number = toNumber(number, this.locale);\n        const _significance = toNumber(significance, this.locale);\n        return {\n            value: ceilingMath(_number, _significance),\n            format: number?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISODD\n// -----------------------------------------------------------------------------\nconst ISODD = {\n    description: _t(\"Whether the provided value is even.\"),\n    args: [arg(\"value (number)\", _t(\"The value to be verified as even.\"))],\n    compute: function (value) {\n        const _value = strictToNumber(value, this.locale);\n        return Math.floor(Math.abs(_value)) & 1 ? true : false;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// LN\n// -----------------------------------------------------------------------------\nconst LN = {\n    description: _t(\"The logarithm of a number, base e (euler's number).\"),\n    args: [arg(\"value (number)\", _t(\"The value for which to calculate the logarithm, base e.\"))],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assert(() => _value > 0, _t(\"The value (%s) must be strictly positive.\", _value.toString()));\n        return Math.log(_value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MOD\n// -----------------------------------------------------------------------------\nfunction mod(dividend, divisor) {\n    assert(() => divisor !== 0, _t(\"The divisor must be different from 0.\"), CellErrorType.DivisionByZero);\n    const modulus = dividend % divisor;\n    // -42 % 10 = -2 but we want 8, so need the code below\n    if ((modulus > 0 && divisor < 0) || (modulus < 0 && divisor > 0)) {\n        return modulus + divisor;\n    }\n    return modulus;\n}\nconst MOD = {\n    description: _t(\"Modulo (remainder) operator.\"),\n    args: [\n        arg(\"dividend (number)\", _t(\"The number to be divided to find the remainder.\")),\n        arg(\"divisor (number)\", _t(\"The number to divide by.\")),\n    ],\n    compute: function (dividend, divisor) {\n        const _divisor = toNumber(divisor, this.locale);\n        const _dividend = toNumber(dividend, this.locale);\n        return {\n            value: mod(_dividend, _divisor),\n            format: dividend?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MUNIT\n// -----------------------------------------------------------------------------\nconst MUNIT = {\n    description: _t(\"Returns a n x n unit matrix, where n is the input dimension.\"),\n    args: [\n        arg(\"dimension (number)\", _t(\"An integer specifying the dimension size of the unit matrix. It must be positive.\")),\n    ],\n    compute: function (n) {\n        const _n = toInteger(n, this.locale);\n        assertPositive(_t(\"The argument dimension must be positive\"), _n);\n        return getUnitMatrix(_n);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ODD\n// -----------------------------------------------------------------------------\nconst ODD = {\n    description: _t(\"Rounds a number up to the nearest odd integer.\"),\n    args: [arg(\"value (number)\", _t(\"The value to round to the next greatest odd number.\"))],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        let temp = Math.ceil(Math.abs(_value));\n        temp = temp & 1 ? temp : temp + 1;\n        return {\n            value: _value < 0 ? -temp : temp,\n            format: value?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PI\n// -----------------------------------------------------------------------------\nconst PI = {\n    description: _t(\"The number pi.\"),\n    args: [],\n    compute: function () {\n        return Math.PI;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// POWER\n// -----------------------------------------------------------------------------\nconst POWER = {\n    description: _t(\"A number raised to a power.\"),\n    args: [\n        arg(\"base (number)\", _t(\"The number to raise to the exponent power.\")),\n        arg(\"exponent (number)\", _t(\"The exponent to raise base to.\")),\n    ],\n    compute: function (base, exponent) {\n        const _base = toNumber(base, this.locale);\n        const _exponent = toNumber(exponent, this.locale);\n        assert(() => _base >= 0 || Number.isInteger(_exponent), _t(\"The exponent (%s) must be an integer when the base is negative.\", _exponent.toString()));\n        return { value: Math.pow(_base, _exponent), format: base?.format };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PRODUCT\n// -----------------------------------------------------------------------------\nconst PRODUCT = {\n    description: _t(\"Result of multiplying a series of numbers together.\"),\n    args: [\n        arg(\"factor1 (number, range<number>)\", _t(\"The first number or range to calculate for the product.\")),\n        arg(\"factor2 (number, range<number>, repeating)\", _t(\"More numbers or ranges to calculate for the product.\")),\n    ],\n    compute: function (...factors) {\n        let count = 0;\n        let acc = 1;\n        for (let n of factors) {\n            if (isMatrix(n)) {\n                for (let i of n) {\n                    for (let j of i) {\n                        const f = j.value;\n                        if (typeof f === \"number\") {\n                            acc *= f;\n                            count += 1;\n                        }\n                        if (isEvaluationError(f)) {\n                            throw j;\n                        }\n                    }\n                }\n            }\n            else if (n !== undefined && n.value !== null) {\n                acc *= strictToNumber(n, this.locale);\n                count += 1;\n            }\n        }\n        return {\n            value: count === 0 ? 0 : acc,\n            format: inferFormat(factors[0]),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// RAND\n// -----------------------------------------------------------------------------\nconst RAND = {\n    description: _t(\"A random number between 0 inclusive and 1 exclusive.\"),\n    args: [],\n    compute: function () {\n        return Math.random();\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// RANDARRAY\n// -----------------------------------------------------------------------------\nconst RANDARRAY = {\n    description: _t(\"Returns a grid of random numbers between 0 inclusive and 1 exclusive.\"),\n    args: [\n        arg(\"rows (number, default=1)\", _t(\"The number of rows to be returned.\")),\n        arg(\"columns (number, default=1)\", _t(\"The number of columns to be returned.\")),\n        arg(\"min (number, default=0)\", _t(\"The minimum number you would like returned.\")),\n        arg(\"max (number, default=1)\", _t(\"The maximum number you would like returned.\")),\n        arg(\"whole_number (number, default=FALSE)\", _t(\"Return a whole number or a decimal value.\")),\n    ],\n    compute: function (rows = { value: 1 }, columns = { value: 1 }, min = { value: 0 }, max = { value: 1 }, wholeNumber = { value: false }) {\n        const _cols = toInteger(columns, this.locale);\n        const _rows = toInteger(rows, this.locale);\n        const _min = toNumber(min, this.locale);\n        const _max = toNumber(max, this.locale);\n        const _whole_number = toBoolean(wholeNumber);\n        assertPositive(_t(\"The number of columns (%s) must be positive.\", _cols.toString()), _cols);\n        assertPositive(_t(\"The number of rows (%s) must be positive.\", _rows.toString()), _rows);\n        assert(() => _min <= _max, _t(\"The maximum (%s) must be greater than or equal to the minimum (%s).\", _max.toString(), _min.toString()));\n        if (_whole_number) {\n            assert(() => Number.isInteger(_min) && Number.isInteger(_max), _t(\"The maximum (%s) and minimum (%s) must be integers when whole_number is TRUE.\", _max.toString(), _min.toString()));\n        }\n        const result = Array(_cols);\n        for (let col = 0; col < _cols; col++) {\n            result[col] = Array(_rows);\n            for (let row = 0; row < _rows; row++) {\n                if (!_whole_number) {\n                    result[col][row] = _min + Math.random() * (_max - _min);\n                }\n                else {\n                    result[col][row] = Math.floor(Math.random() * (_max - _min + 1) + _min);\n                }\n            }\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// RANDBETWEEN\n// -----------------------------------------------------------------------------\nconst RANDBETWEEN = {\n    description: _t(\"Random integer between two values, inclusive.\"),\n    args: [\n        arg(\"low (number)\", _t(\"The low end of the random range.\")),\n        arg(\"high (number)\", _t(\"The high end of the random range.\")),\n    ],\n    compute: function (low, high) {\n        let _low = toNumber(low, this.locale);\n        if (!Number.isInteger(_low)) {\n            _low = Math.ceil(_low);\n        }\n        let _high = toNumber(high, this.locale);\n        if (!Number.isInteger(_high)) {\n            _high = Math.floor(_high);\n        }\n        assert(() => _low <= _high, _t(\"The high (%s) must be greater than or equal to the low (%s).\", _high.toString(), _low.toString()));\n        return {\n            value: _low + Math.ceil((_high - _low + 1) * Math.random()) - 1,\n            format: low?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ROUND\n// -----------------------------------------------------------------------------\nconst ROUND = {\n    description: _t(\"Rounds a number according to standard rules.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value to round to places number of places.\")),\n        arg(`places (number, default=${DEFAULT_PLACES})`, _t(\"The number of decimal places to which to round.\")),\n    ],\n    compute: function (value, places = { value: DEFAULT_PLACES }) {\n        const _value = toNumber(value, this.locale);\n        let _places = toNumber(places, this.locale);\n        const absValue = Math.abs(_value);\n        let tempResult;\n        if (_places === 0) {\n            tempResult = Math.round(absValue);\n        }\n        else {\n            if (!Number.isInteger(_places)) {\n                _places = Math.trunc(_places);\n            }\n            tempResult = Math.round(absValue * Math.pow(10, _places)) / Math.pow(10, _places);\n        }\n        return {\n            value: _value >= 0 ? tempResult : -tempResult,\n            format: value?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ROUNDDOWN\n// -----------------------------------------------------------------------------\nconst ROUNDDOWN = {\n    description: _t(\"Rounds down a number.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value to round to places number of places, always rounding down.\")),\n        arg(`places (number, default=${DEFAULT_PLACES})`, _t(\"The number of decimal places to which to round.\")),\n    ],\n    compute: function (value, places = { value: DEFAULT_PLACES }) {\n        const _value = toNumber(value, this.locale);\n        let _places = toNumber(places, this.locale);\n        const absValue = Math.abs(_value);\n        let tempResult;\n        if (_places === 0) {\n            tempResult = Math.floor(absValue);\n        }\n        else {\n            if (!Number.isInteger(_places)) {\n                _places = Math.trunc(_places);\n            }\n            tempResult = Math.floor(absValue * Math.pow(10, _places)) / Math.pow(10, _places);\n        }\n        return {\n            value: _value >= 0 ? tempResult : -tempResult,\n            format: value?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ROUNDUP\n// -----------------------------------------------------------------------------\nconst ROUNDUP = {\n    description: _t(\"Rounds up a number.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value to round to places number of places, always rounding up.\")),\n        arg(`places (number, default=${DEFAULT_PLACES})`, _t(\"The number of decimal places to which to round.\")),\n    ],\n    compute: function (value, places = { value: DEFAULT_PLACES }) {\n        const _value = toNumber(value, this.locale);\n        let _places = toNumber(places, this.locale);\n        const absValue = Math.abs(_value);\n        let tempResult;\n        if (_places === 0) {\n            tempResult = Math.ceil(absValue);\n        }\n        else {\n            if (!Number.isInteger(_places)) {\n                _places = Math.trunc(_places);\n            }\n            tempResult = Math.ceil(absValue * Math.pow(10, _places)) / Math.pow(10, _places);\n        }\n        return {\n            value: _value >= 0 ? tempResult : -tempResult,\n            format: value?.format,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SEC\n// -----------------------------------------------------------------------------\nconst SEC = {\n    description: _t(\"Secant of an angle provided in radians.\"),\n    args: [arg(\"angle (number)\", _t(\"The angle to find the secant of, in radians.\"))],\n    compute: function (angle) {\n        return 1 / Math.cos(toNumber(angle, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SECH\n// -----------------------------------------------------------------------------\nconst SECH = {\n    description: _t(\"Hyperbolic secant of any real number.\"),\n    args: [arg(\"value (number)\", _t(\"Any real value to calculate the hyperbolic secant of.\"))],\n    compute: function (value) {\n        return 1 / Math.cosh(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SEQUENCE\n// -----------------------------------------------------------------------------\nconst SEQUENCE = {\n    description: _t(\"Returns a sequence of numbers.\"),\n    args: [\n        arg(\"rows (number)\", _t(\"The number of rows to return\")),\n        arg(\"columns (number, optional, default=1)\", _t(\"The number of columns to return\")),\n        arg(\"start (number, optional, default=1)\", _t(\"The first number in the sequence\")),\n        arg(\"step (number, optional, default=1)\", _t(\"The amount to increment each value in the sequence\")),\n    ],\n    compute: function (rows, columns = { value: 1 }, start = { value: 1 }, step = { value: 1 }) {\n        const _start = toNumber(start, this.locale);\n        const _step = toNumber(step, this.locale);\n        const _rows = toInteger(rows, this.locale);\n        const _columns = toInteger(columns, this.locale);\n        assertPositive(_t(\"The number of columns (%s) must be positive.\", _columns), _columns);\n        assertPositive(_t(\"The number of rows (%s) must be positive.\", _rows), _rows);\n        return generateMatrix(_columns, _rows, (col, row) => {\n            return {\n                value: _start + row * _columns * _step + col * _step,\n            };\n        });\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SIN\n// -----------------------------------------------------------------------------\nconst SIN = {\n    description: _t(\"Sine of an angle provided in radians.\"),\n    args: [arg(\"angle (number)\", _t(\"The angle to find the sine of, in radians.\"))],\n    compute: function (angle) {\n        return Math.sin(toNumber(angle, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SINH\n// -----------------------------------------------------------------------------\nconst SINH = {\n    description: _t(\"Hyperbolic sine of any real number.\"),\n    args: [arg(\"value (number)\", _t(\"Any real value to calculate the hyperbolic sine of.\"))],\n    compute: function (value) {\n        return Math.sinh(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SQRT\n// -----------------------------------------------------------------------------\nconst SQRT = {\n    description: _t(\"Positive square root of a positive number.\"),\n    args: [arg(\"value (number)\", _t(\"The number for which to calculate the positive square root.\"))],\n    compute: function (value) {\n        const _value = toNumber(value, this.locale);\n        assert(() => _value >= 0, _t(\"The value (%s) must be positive or null.\", _value.toString()));\n        return { value: Math.sqrt(_value), format: value?.format };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SUM\n// -----------------------------------------------------------------------------\nconst SUM = {\n    description: _t(\"Sum of a series of numbers and/or cells.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first number or range to add together.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional numbers or ranges to add to value1.\")),\n    ],\n    compute: function (...values) {\n        const v1 = values[0];\n        return {\n            value: sum(values, this.locale),\n            format: inferFormat(v1),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SUMIF\n// -----------------------------------------------------------------------------\nconst SUMIF = {\n    description: _t(\"A conditional sum across a range.\"),\n    args: [\n        arg(\"criteria_range (range)\", _t(\"The range which is tested against criterion.\")),\n        arg(\"criterion (string)\", _t(\"The pattern or test to apply to range.\")),\n        arg(\"sum_range (range, default=criteria_range)\", _t(\"The range to be summed, if different from range.\")),\n    ],\n    compute: function (criteriaRange, criterion, sumRange) {\n        if (sumRange === undefined) {\n            sumRange = criteriaRange;\n        }\n        let sum = 0;\n        visitMatchingRanges([criteriaRange, criterion], (i, j) => {\n            const value = sumRange[i]?.[j]?.value;\n            if (typeof value === \"number\") {\n                sum += value;\n            }\n        }, this.locale);\n        return sum;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SUMIFS\n// -----------------------------------------------------------------------------\nconst SUMIFS = {\n    description: _t(\"Sums a range depending on multiple criteria.\"),\n    args: [\n        arg(\"sum_range (range)\", _t(\"The range to sum.\")),\n        arg(\"criteria_range1 (range)\", _t(\"The range to check against criterion1.\")),\n        arg(\"criterion1 (string)\", _t(\"The pattern or test to apply to criteria_range1.\")),\n        arg(\"criteria_range2 (any, range, repeating)\", _t(\"Additional ranges to check.\")),\n        arg(\"criterion2 (string, repeating)\", _t(\"Additional criteria to check.\")),\n    ],\n    compute: function (sumRange, ...criters) {\n        let sum = 0;\n        visitMatchingRanges(criters, (i, j) => {\n            const value = sumRange[i]?.[j]?.value;\n            if (typeof value === \"number\") {\n                sum += value;\n            }\n        }, this.locale);\n        return sum;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TAN\n// -----------------------------------------------------------------------------\nconst TAN = {\n    description: _t(\"Tangent of an angle provided in radians.\"),\n    args: [arg(\"angle (number)\", _t(\"The angle to find the tangent of, in radians.\"))],\n    compute: function (angle) {\n        return Math.tan(toNumber(angle, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TANH\n// -----------------------------------------------------------------------------\nconst TANH = {\n    description: _t(\"Hyperbolic tangent of any real number.\"),\n    args: [arg(\"value (number)\", _t(\"Any real value to calculate the hyperbolic tangent of.\"))],\n    compute: function (value) {\n        return Math.tanh(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TRUNC\n// -----------------------------------------------------------------------------\nfunction trunc(value, places) {\n    if (places === 0) {\n        return Math.trunc(value);\n    }\n    if (!Number.isInteger(places)) {\n        places = Math.trunc(places);\n    }\n    return Math.trunc(value * Math.pow(10, places)) / Math.pow(10, places);\n}\nconst TRUNC = {\n    description: _t(\"Truncates a number.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value to be truncated.\")),\n        arg(`places (number, default=${DEFAULT_PLACES})`, _t(\"The number of significant digits to the right of the decimal point to retain.\")),\n    ],\n    compute: function (value, places = { value: DEFAULT_PLACES }) {\n        const _value = toNumber(value, this.locale);\n        const _places = toNumber(places, this.locale);\n        return { value: trunc(_value, _places), format: value?.format };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// INT\n// -----------------------------------------------------------------------------\nconst INT = {\n    description: _t(\"Rounds a number down to the nearest integer that is less than or equal to it.\"),\n    args: [arg(\"value (number)\", _t(\"The number to round down to the nearest integer.\"))],\n    compute: function (value) {\n        return Math.floor(toNumber(value, this.locale));\n    },\n    isExported: true,\n};\n\nvar math = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    ABS: ABS,\n    ACOS: ACOS,\n    ACOSH: ACOSH,\n    ACOT: ACOT,\n    ACOTH: ACOTH,\n    ASIN: ASIN,\n    ASINH: ASINH,\n    ATAN: ATAN,\n    ATAN2: ATAN2,\n    ATANH: ATANH,\n    CEILING: CEILING,\n    CEILING_MATH: CEILING_MATH,\n    CEILING_PRECISE: CEILING_PRECISE,\n    COS: COS,\n    COSH: COSH,\n    COT: COT,\n    COTH: COTH,\n    COUNTBLANK: COUNTBLANK,\n    COUNTIF: COUNTIF,\n    COUNTIFS: COUNTIFS,\n    COUNTUNIQUE: COUNTUNIQUE,\n    COUNTUNIQUEIFS: COUNTUNIQUEIFS,\n    CSC: CSC,\n    CSCH: CSCH,\n    DECIMAL: DECIMAL,\n    DEGREES: DEGREES,\n    EXP: EXP,\n    FLOOR: FLOOR,\n    FLOOR_MATH: FLOOR_MATH,\n    FLOOR_PRECISE: FLOOR_PRECISE,\n    INT: INT,\n    ISEVEN: ISEVEN,\n    ISODD: ISODD,\n    ISO_CEILING: ISO_CEILING,\n    LN: LN,\n    MOD: MOD,\n    MUNIT: MUNIT,\n    ODD: ODD,\n    PI: PI,\n    POWER: POWER,\n    PRODUCT: PRODUCT,\n    RAND: RAND,\n    RANDARRAY: RANDARRAY,\n    RANDBETWEEN: RANDBETWEEN,\n    ROUND: ROUND,\n    ROUNDDOWN: ROUNDDOWN,\n    ROUNDUP: ROUNDUP,\n    SEC: SEC,\n    SECH: SECH,\n    SEQUENCE: SEQUENCE,\n    SIN: SIN,\n    SINH: SINH,\n    SQRT: SQRT,\n    SUM: SUM,\n    SUMIF: SUMIF,\n    SUMIFS: SUMIFS,\n    TAN: TAN,\n    TANH: TANH,\n    TRUNC: TRUNC\n});\n\nfunction filterAndFlatData(dataY, dataX) {\n    const _flatDataY = [];\n    const _flatDataX = [];\n    let lenY = 0;\n    let lenX = 0;\n    visitAny([dataY], (y) => {\n        _flatDataY.push(y);\n        lenY += 1;\n    });\n    visitAny([dataX], (x) => {\n        _flatDataX.push(x);\n        lenX += 1;\n    });\n    assert(() => lenY === lenX, _t(\"[[FUNCTION_NAME]] has mismatched argument count %s vs %s.\", lenY, lenX));\n    const flatDataX = [];\n    const flatDataY = [];\n    for (let i = 0; i < lenY; i++) {\n        const valueY = _flatDataY[i]?.value;\n        const valueX = _flatDataX[i]?.value;\n        if (typeof valueY === \"number\" && typeof valueX === \"number\") {\n            flatDataY.push(valueY);\n            flatDataX.push(valueX);\n        }\n    }\n    return { flatDataX, flatDataY };\n}\n// Note: dataY and dataX may not have the same dimension\nfunction covariance(dataY, dataX, isSample) {\n    const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n    const count = flatDataY.length;\n    assert(() => count !== 0 && (!isSample || count !== 1), _t(\"Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.\"), CellErrorType.DivisionByZero);\n    let sumY = 0;\n    let sumX = 0;\n    for (let i = 0; i < count; i++) {\n        sumY += flatDataY[i];\n        sumX += flatDataX[i];\n    }\n    const averageY = sumY / count;\n    const averageX = sumX / count;\n    let acc = 0;\n    for (let i = 0; i < count; i++) {\n        acc += (flatDataY[i] - averageY) * (flatDataX[i] - averageX);\n    }\n    return acc / (count - (isSample ? 1 : 0));\n}\nfunction variance(args, isSample, textAs0, locale) {\n    let count = 0;\n    let sum = 0;\n    const reduceFunction = textAs0 ? reduceNumbersTextAs0 : reduceNumbers;\n    sum = reduceFunction(args, (acc, a) => {\n        count += 1;\n        return acc + a;\n    }, 0, locale);\n    assert(() => count !== 0 && (!isSample || count !== 1), _t(\"Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.\"), CellErrorType.DivisionByZero);\n    const average = sum / count;\n    return (reduceFunction(args, (acc, a) => acc + Math.pow(a - average, 2), 0, locale) /\n        (count - (isSample ? 1 : 0)));\n}\nfunction centile(data, percent, isInclusive, locale) {\n    const _percent = toNumber(percent, locale);\n    assert(() => (isInclusive ? 0 <= _percent && _percent <= 1 : 0 < _percent && _percent < 1), _t(\"Function [[FUNCTION_NAME]] parameter 2 value is out of range.\"));\n    let sortedArray = [];\n    let index;\n    let count = 0;\n    visitAny(data, (d) => {\n        const value = d?.value;\n        if (typeof value === \"number\") {\n            index = dichotomicSearch(sortedArray, d, \"nextSmaller\", \"asc\", sortedArray.length, (array, i) => array[i]);\n            sortedArray.splice(index + 1, 0, value);\n            count++;\n        }\n    });\n    assert(() => count !== 0, _t(\"[[FUNCTION_NAME]] has no valid input data.\"));\n    if (!isInclusive) {\n        // 2nd argument must be between 1/(n+1) and n/(n+1) with n the number of data\n        assert(() => 1 / (count + 1) <= _percent && _percent <= count / (count + 1), _t(\"Function [[FUNCTION_NAME]] parameter 2 value is out of range.\"));\n    }\n    return percentile(sortedArray, _percent, isInclusive);\n}\n// -----------------------------------------------------------------------------\n// AVEDEV\n// -----------------------------------------------------------------------------\nconst AVEDEV = {\n    description: _t(\"Average magnitude of deviations from mean.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the sample.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the sample.\")),\n    ],\n    compute: function (...values) {\n        let count = 0;\n        const sum = reduceNumbers(values, (acc, a) => {\n            count += 1;\n            return acc + a;\n        }, 0, this.locale);\n        assertNotZero(count);\n        const average = sum / count;\n        return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// AVERAGE\n// -----------------------------------------------------------------------------\nconst AVERAGE = {\n    description: _t(\"Numerical average value in a dataset, ignoring text.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range to consider when calculating the average value.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to consider when calculating the average value.\")),\n    ],\n    compute: function (...values) {\n        return {\n            value: average(values, this.locale),\n            format: inferFormat(values[0]),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// AVERAGE.WEIGHTED\n// -----------------------------------------------------------------------------\nconst rangeError = _t(\"[[FUNCTION_NAME]] has mismatched range sizes.\");\nconst negativeWeightError = _t(\"[[FUNCTION_NAME]] expects the weight to be positive or equal to 0.\");\nconst AVERAGE_WEIGHTED = {\n    description: _t(\"Weighted average.\"),\n    args: [\n        arg(\"values (number, range<number>)\", _t(\"Values to average.\")),\n        arg(\"weights (number, range<number>)\", _t(\"Weights for each corresponding value.\")),\n        arg(\"additional_values (number, range<number>, repeating)\", _t(\"Additional values to average.\")),\n        arg(\"additional_weights (number, range<number>, repeating)\", _t(\"Additional weights.\")),\n    ],\n    compute: function (...args) {\n        let sum = 0;\n        let count = 0;\n        for (let n = 0; n < args.length - 1; n += 2) {\n            const argN = args[n];\n            const argN1 = args[n + 1];\n            assertSameDimensions(rangeError, argN, argN1);\n            if (isMatrix(argN)) {\n                for (let i = 0; i < argN.length; i++) {\n                    for (let j = 0; j < argN[0].length; j++) {\n                        const value = argN[i][j].value;\n                        const weight = isMatrix(argN1) ? argN1?.[i][j].value : toNumber(argN1, this.locale);\n                        const valueIsNumber = typeof value === \"number\";\n                        const weightIsNumber = typeof weight === \"number\";\n                        if (valueIsNumber && weightIsNumber) {\n                            assert(() => weight >= 0, negativeWeightError);\n                            sum += value * weight;\n                            count += weight;\n                            continue;\n                        }\n                        assert(() => valueIsNumber === weightIsNumber, _t(\"[[FUNCTION_NAME]] expects number values.\"));\n                    }\n                }\n            }\n            else {\n                const value = toNumber(argN, this.locale);\n                const weight = isMatrix(argN1) ? argN1?.[0][0].value : toNumber(argN1, this.locale);\n                if (typeof weight === \"number\") {\n                    assert(() => weight >= 0, negativeWeightError);\n                    sum += value * weight;\n                    count += weight;\n                }\n            }\n        }\n        assertNotZero(count);\n        return { value: sum / count, format: inferFormat(args[0]) };\n    },\n};\n// -----------------------------------------------------------------------------\n// AVERAGEA\n// -----------------------------------------------------------------------------\nconst AVERAGEA = {\n    description: _t(\"Numerical average value in a dataset.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range to consider when calculating the average value.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to consider when calculating the average value.\")),\n    ],\n    compute: function (...args) {\n        let count = 0;\n        const sum = reduceNumbersTextAs0(args, (acc, a) => {\n            count += 1;\n            return acc + a;\n        }, 0, this.locale);\n        assertNotZero(count);\n        return {\n            value: sum / count,\n            format: inferFormat(args[0]),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// AVERAGEIF\n// -----------------------------------------------------------------------------\nconst AVERAGEIF = {\n    description: _t(\"Average of values depending on criteria.\"),\n    args: [\n        arg(\"criteria_range (number, range<number>)\", _t(\"The range to check against criterion.\")),\n        arg(\"criterion (string)\", _t(\"The pattern or test to apply to criteria_range.\")),\n        arg(\"average_range (number, range<number>, default=criteria_range)\", _t(\"The range to average. If not included, criteria_range is used for the average instead.\")),\n    ],\n    compute: function (criteriaRange, criterion, averageRange) {\n        const _averageRange = averageRange === undefined ? toMatrix(criteriaRange) : toMatrix(averageRange);\n        let count = 0;\n        let sum = 0;\n        visitMatchingRanges([criteriaRange, criterion], (i, j) => {\n            const value = _averageRange[i]?.[j]?.value;\n            if (typeof value === \"number\") {\n                count += 1;\n                sum += value;\n            }\n        }, this.locale);\n        assertNotZero(count);\n        return sum / count;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// AVERAGEIFS\n// -----------------------------------------------------------------------------\nconst AVERAGEIFS = {\n    description: _t(\"Average of values depending on multiple criteria.\"),\n    args: [\n        arg(\"average_range (range)\", _t(\"The range to average.\")),\n        arg(\"criteria_range1 (range)\", _t(\"The range to check against criterion1.\")),\n        arg(\"criterion1 (string)\", _t(\"The pattern or test to apply to criteria_range1.\")),\n        arg(\"criteria_range2 (any, range, repeating)\", _t(\"Additional criteria_range and criterion to check.\")),\n        arg(\"criterion2 (string, repeating)\", _t(\"The pattern or test to apply to criteria_range2.\")),\n    ],\n    compute: function (averageRange, ...args) {\n        const _averageRange = toMatrix(averageRange);\n        let count = 0;\n        let sum = 0;\n        visitMatchingRanges(args, (i, j) => {\n            const value = _averageRange[i]?.[j]?.value;\n            if (typeof value === \"number\") {\n                count += 1;\n                sum += value;\n            }\n        }, this.locale);\n        assertNotZero(count);\n        return sum / count;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUNT\n// -----------------------------------------------------------------------------\nconst COUNT = {\n    description: _t(\"The number of numeric values in dataset.\"),\n    args: [\n        arg(\"value1 (number, any, range<number>)\", _t(\"The first value or range to consider when counting.\")),\n        arg(\"value2 (number, any, range<number>, repeating)\", _t(\"Additional values or ranges to consider when counting.\")),\n    ],\n    compute: function (...values) {\n        return countNumbers(values, this.locale);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUNTA\n// -----------------------------------------------------------------------------\nconst COUNTA = {\n    description: _t(\"The number of values in a dataset.\"),\n    args: [\n        arg(\"value1 (any, range)\", _t(\"The first value or range to consider when counting.\")),\n        arg(\"value2 (any, range, repeating)\", _t(\"Additional values or ranges to consider when counting.\")),\n    ],\n    compute: function (...values) {\n        return countAny(values);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COVAR\n// -----------------------------------------------------------------------------\n// Note: Unlike the VAR function which corresponds to the variance over a sample (VAR.S),\n// the COVAR function corresponds to the covariance over an entire population (COVAR.P)\nconst COVAR = {\n    description: _t(\"The covariance of a dataset.\"),\n    args: [\n        arg(\"data_y (any, range)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (any, range)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataY, dataX) {\n        return covariance(dataY, dataX, false);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COVARIANCE.P\n// -----------------------------------------------------------------------------\nconst COVARIANCE_P = {\n    description: _t(\"The covariance of a dataset.\"),\n    args: [\n        arg(\"data_y (any, range)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (any, range)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataY, dataX) {\n        return covariance(dataY, dataX, false);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COVARIANCE.S\n// -----------------------------------------------------------------------------\nconst COVARIANCE_S = {\n    description: _t(\"The sample covariance of a dataset.\"),\n    args: [\n        arg(\"data_y (any, range)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (any, range)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataY, dataX) {\n        return covariance(dataY, dataX, true);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FORECAST\n// -----------------------------------------------------------------------------\nconst FORECAST = {\n    description: _t(\"Calculates the expected y-value for a specified x based on a linear regression of a dataset.\"),\n    args: [\n        arg(\"x (number, range<number>)\", _t(\"The value(s) on the x-axis to forecast.\")),\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (x, dataY, dataX) {\n        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n        return predictLinearValues([flatDataY], [flatDataX], matrixMap(toMatrix(x), (value) => toNumber(value, this.locale)), true);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// GROWTH\n// -----------------------------------------------------------------------------\nconst GROWTH = {\n    description: _t(\"Fits points to exponential growth trend.\"),\n    args: [\n        arg(\"known_data_y (range<number>)\", _t(\"The array or range containing dependent (y) values that are already known, used to curve fit an ideal exponential growth curve.\")),\n        arg(\"known_data_x (range<number>, default={1;2;3;...})\", _t(\"The values of the independent variable(s) corresponding with known_data_y.\")),\n        arg(\"new_data_x (any, range, default=known_data_x)\", _t(\"The data points to return the y values for on the ideal curve fit.\")),\n        arg(\"b (boolean, default=TRUE)\", _t(\"Given a general exponential form of y = b*m^x for a curve fit, calculates b if TRUE or forces b to be 1 and only calculates the m values if FALSE.\")),\n    ],\n    compute: function (knownDataY, knownDataX = [[]], newDataX = [[]], b = { value: true }) {\n        return expM(predictLinearValues(logM(toNumberMatrix(knownDataY, \"the first argument (known_data_y)\")), toNumberMatrix(knownDataX, \"the second argument (known_data_x)\"), toNumberMatrix(newDataX, \"the third argument (new_data_y)\"), toBoolean(b)));\n    },\n};\n// -----------------------------------------------------------------------------\n// INTERCEPT\n// -----------------------------------------------------------------------------\nconst INTERCEPT = {\n    description: _t(\"Compute the intercept of the linear regression.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataY, dataX) {\n        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n        const [[], [intercept]] = fullLinearRegression([flatDataX], [flatDataY]);\n        return intercept;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// LARGE\n// -----------------------------------------------------------------------------\nconst LARGE = {\n    description: _t(\"Nth largest element from a data set.\"),\n    args: [\n        arg(\"data (any, range)\", _t(\"Array or range containing the dataset to consider.\")),\n        arg(\"n (number)\", _t(\"The rank from largest to smallest of the element to return.\")),\n    ],\n    compute: function (data, n) {\n        const _n = Math.trunc(toNumber(n?.value, this.locale));\n        let largests = [];\n        let index;\n        let count = 0;\n        visitAny([data], (d) => {\n            if (typeof d?.value === \"number\") {\n                index = dichotomicSearch(largests, d, \"nextSmaller\", \"asc\", largests.length, (array, i) => array[i].value);\n                largests.splice(index + 1, 0, d);\n                count++;\n                if (count > _n) {\n                    largests.shift();\n                    count--;\n                }\n            }\n        });\n        const result = largests.shift();\n        assert(() => result !== undefined, _t(\"[[FUNCTION_NAME]] has no valid input data.\"));\n        assert(() => count >= _n, _t(\"Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.\", _n));\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// LINEST\n// -----------------------------------------------------------------------------\nconst LINEST = {\n    description: _t(\"Given partial data about a linear trend, calculates various parameters about the ideal linear trend using the least-squares method.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>, default={1;2;3;...})\", _t(\"The range representing the array or matrix of independent data.\")),\n        arg(\"calculate_b (boolean, default=TRUE)\", _t(\"A flag specifying wheter to compute the slope or not\")),\n        arg(\"verbose (boolean, default=FALSE)\", _t(\"A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept\")),\n    ],\n    compute: function (dataY, dataX = [[]], calculateB = { value: true }, verbose = { value: false }) {\n        return fullLinearRegression(toNumberMatrix(dataX, \"the first argument (data_y)\"), toNumberMatrix(dataY, \"the second argument (data_x)\"), toBoolean(calculateB), toBoolean(verbose));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// LOGEST\n// -----------------------------------------------------------------------------\nconst LOGEST = {\n    description: _t(\"Given partial data about an exponential growth curve, calculates various parameters about the best fit ideal exponential growth curve.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>, optional, default={1;2;3;...})\", _t(\"The range representing the array or matrix of independent data.\")),\n        arg(\"calculate_b (boolean, default=TRUE)\", _t(\"A flag specifying wheter to compute the slope or not\")),\n        arg(\"verbose (boolean, default=FALSE)\", _t(\"A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept\")),\n    ],\n    compute: function (dataY, dataX = [[]], calculateB = { value: true }, verbose = { value: false }) {\n        const coeffs = fullLinearRegression(toNumberMatrix(dataX, \"the second argument (data_x)\"), logM(toNumberMatrix(dataY, \"the first argument (data_y)\")), toBoolean(calculateB), toBoolean(verbose));\n        for (let i = 0; i < coeffs.length; i++) {\n            coeffs[i][0] = Math.exp(coeffs[i][0]);\n        }\n        return coeffs;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MATTHEWS\n// -----------------------------------------------------------------------------\nconst MATTHEWS = {\n    description: _t(\"Compute the Matthews correlation coefficient of a dataset.\"),\n    args: [\n        arg(\"data_x (range)\", _t(\"The range representing the array or matrix of observed data.\")),\n        arg(\"data_y (range)\", _t(\"The range representing the array or matrix of predicted data.\")),\n    ],\n    compute: function (dataX, dataY) {\n        const flatX = dataX.flat();\n        const flatY = dataY.flat();\n        assertSameNumberOfElements(flatX, flatY);\n        if (flatX.length === 0) {\n            throw new EvaluationError(_t(\"[[FUNCTION_NAME]] expects non-empty ranges for both parameters.\"));\n        }\n        const n = flatX.length;\n        let trueN = 0, trueP = 0, falseP = 0, falseN = 0;\n        for (let i = 0; i < n; ++i) {\n            const isTrue1 = toBoolean(flatX[i]);\n            const isTrue2 = toBoolean(flatY[i]);\n            if (isTrue1 === isTrue2) {\n                if (isTrue1) {\n                    trueP++;\n                }\n                else {\n                    trueN++;\n                }\n            }\n            else {\n                if (isTrue1) {\n                    falseN++;\n                }\n                else {\n                    falseP++;\n                }\n            }\n        }\n        return ((trueP * trueN - falseP * falseN) /\n            Math.sqrt((trueP + falseP) * (trueP + falseN) * (trueN + falseP) * (trueN + falseN)));\n    },\n    isExported: false,\n};\n// -----------------------------------------------------------------------------\n// MAX\n// -----------------------------------------------------------------------------\nconst MAX = {\n    description: _t(\"Maximum value in a numeric dataset.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range to consider when calculating the maximum value.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to consider when calculating the maximum value.\")),\n    ],\n    compute: function (...values) {\n        return max(values, this.locale);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MAXA\n// -----------------------------------------------------------------------------\nconst MAXA = {\n    description: _t(\"Maximum numeric value in a dataset.\"),\n    args: [\n        arg(\"value1 (any, range)\", _t(\"The first value or range to consider when calculating the maximum value.\")),\n        arg(\"value2 (any, range, repeating)\", _t(\"Additional values or ranges to consider when calculating the maximum value.\")),\n    ],\n    compute: function (...args) {\n        const maxa = reduceNumbersTextAs0(args, (acc, a) => {\n            return Math.max(a, acc);\n        }, -Infinity, this.locale);\n        return { value: maxa === -Infinity ? 0 : maxa, format: inferFormat(args[0]) };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MAXIFS\n// -----------------------------------------------------------------------------\nconst MAXIFS = {\n    description: _t(\"Returns the maximum value in a range of cells, filtered by a set of criteria.\"),\n    args: [\n        arg(\"range (range)\", _t(\"The range of cells from which the maximum will be determined.\")),\n        arg(\"criteria_range1 (range)\", _t(\"The range of cells over which to evaluate criterion1.\")),\n        arg(\"criterion1 (string)\", _t(\"The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.\")),\n        arg(\"criteria_range2 (any, range, repeating)\", _t(\"Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.\")),\n        arg(\"criterion2 (string, repeating)\", _t(\"The pattern or test to apply to criteria_range2.\")),\n    ],\n    compute: function (range, ...args) {\n        let result = -Infinity;\n        visitMatchingRanges(args, (i, j) => {\n            const value = range[i]?.[j]?.value;\n            if (typeof value === \"number\") {\n                result = result < value ? value : result;\n            }\n        }, this.locale);\n        return result === -Infinity ? 0 : result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MEDIAN\n// -----------------------------------------------------------------------------\nconst MEDIAN = {\n    description: _t(\"Median value in a numeric dataset.\"),\n    args: [\n        arg(\"value1 (any, range)\", _t(\"The first value or range to consider when calculating the median value.\")),\n        arg(\"value2 (any, range, repeating)\", _t(\"Additional values or ranges to consider when calculating the median value.\")),\n    ],\n    compute: function (...values) {\n        let data = [];\n        visitNumbers(values, (value) => {\n            data.push(value);\n        }, this.locale);\n        return {\n            value: centile(data, { value: 0.5 }, true, this.locale),\n            format: inferFormat(data[0]),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MIN\n// -----------------------------------------------------------------------------\nconst MIN = {\n    description: _t(\"Minimum value in a numeric dataset.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range to consider when calculating the minimum value.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to consider when calculating the minimum value.\")),\n    ],\n    compute: function (...values) {\n        return min(values, this.locale);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MINA\n// -----------------------------------------------------------------------------\nconst MINA = {\n    description: _t(\"Minimum numeric value in a dataset.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range to consider when calculating the minimum value.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to consider when calculating the minimum value.\")),\n    ],\n    compute: function (...args) {\n        const mina = reduceNumbersTextAs0(args, (acc, a) => {\n            return Math.min(a, acc);\n        }, Infinity, this.locale);\n        return { value: mina === Infinity ? 0 : mina, format: inferFormat(args[0]) };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MINIFS\n// -----------------------------------------------------------------------------\nconst MINIFS = {\n    description: _t(\"Returns the minimum value in a range of cells, filtered by a set of criteria.\"),\n    args: [\n        arg(\"range (range)\", _t(\"The range of cells from which the minimum will be determined.\")),\n        arg(\"criteria_range1 (range)\", _t(\"The range of cells over which to evaluate criterion1.\")),\n        arg(\"criterion1 (string)\", _t(\"The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.\")),\n        arg(\"criteria_range2 (any, range, repeating)\", _t(\"Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.\")),\n        arg(\"criterion2 (string, repeating)\", _t(\"The pattern or test to apply to criteria_range2.\")),\n    ],\n    compute: function (range, ...args) {\n        let result = Infinity;\n        visitMatchingRanges(args, (i, j) => {\n            const value = range[i]?.[j]?.value;\n            if (typeof value === \"number\") {\n                result = result > value ? value : result;\n            }\n        }, this.locale);\n        return result === Infinity ? 0 : result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PEARSON\n// -----------------------------------------------------------------------------\nfunction pearson(dataY, dataX) {\n    const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n    if (flatDataX.length === 0) {\n        throw new EvaluationError(_t(\"[[FUNCTION_NAME]] expects non-empty ranges for both parameters.\"));\n    }\n    if (flatDataX.length < 2) {\n        throw new EvaluationError(_t(\"[[FUNCTION_NAME]] needs at least two values for both parameters.\"));\n    }\n    const n = flatDataX.length;\n    let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0;\n    for (let i = 0; i < n; i++) {\n        const xij = flatDataX[i];\n        const yij = flatDataY[i];\n        sumX += xij;\n        sumY += yij;\n        sumXY += xij * yij;\n        sumXX += xij * xij;\n        sumYY += yij * yij;\n    }\n    return ((n * sumXY - sumX * sumY) / Math.sqrt((n * sumXX - sumX * sumX) * (n * sumYY - sumY * sumY)));\n}\nconst PEARSON = {\n    description: _t(\"Compute the Pearson product-moment correlation coefficient of a dataset.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataY, dataX) {\n        return pearson(dataY, dataX);\n    },\n    isExported: true,\n};\n// CORREL\n// In GSheet, CORREL is just an alias to PEARSON\nconst CORREL = PEARSON;\n// -----------------------------------------------------------------------------\n// PERCENTILE\n// -----------------------------------------------------------------------------\nconst PERCENTILE = {\n    description: _t(\"Value at a given percentile of a dataset.\"),\n    args: [\n        arg(\"data (any, range)\", _t(\"The array or range containing the dataset to consider.\")),\n        arg(\"percentile (number)\", _t(\"The percentile whose value within data will be calculated and returned.\")),\n    ],\n    compute: function (data, percentile) {\n        return PERCENTILE_INC.compute.bind(this)(data, percentile);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PERCENTILE.EXC\n// -----------------------------------------------------------------------------\nconst PERCENTILE_EXC = {\n    description: _t(\"Value at a given percentile of a dataset exclusive of 0 and 1.\"),\n    args: [\n        arg(\"data (any, range)\", _t(\"The array or range containing the dataset to consider.\")),\n        arg(\"percentile (number)\", _t(\"The percentile, exclusive of 0 and 1, whose value within 'data' will be calculated and returned.\")),\n    ],\n    compute: function (data, percentile) {\n        return {\n            value: centile([data], percentile, false, this.locale),\n            format: inferFormat(data),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PERCENTILE.INC\n// -----------------------------------------------------------------------------\nconst PERCENTILE_INC = {\n    description: _t(\"Value at a given percentile of a dataset.\"),\n    args: [\n        arg(\"data (any, range)\", _t(\"The array or range containing the dataset to consider.\")),\n        arg(\"percentile (number)\", _t(\"The percentile whose value within data will be calculated and returned.\")),\n    ],\n    compute: function (data, percentile) {\n        return {\n            value: centile([data], percentile, true, this.locale),\n            format: inferFormat(data),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// POLYFIT\n// -----------------------------------------------------------------------------\nconst POLYFIT_COEFFS = {\n    description: _t(\"Compute the coefficients of polynomial regression of the dataset.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n        arg(\"order (number)\", _t(\"The order of the polynomial to fit the data, between 1 and 6.\")),\n        arg(\"intercept (boolean, default=TRUE)\", _t(\"A flag specifying whether to compute the intercept or not.\")),\n    ],\n    compute: function (dataY, dataX, order, intercept = { value: true }) {\n        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n        return polynomialRegression(flatDataY, flatDataX, toNumber(order, this.locale), toBoolean(intercept));\n    },\n    isExported: false,\n};\n// -----------------------------------------------------------------------------\n// POLYFIT.FORECAST\n// -----------------------------------------------------------------------------\nconst POLYFIT_FORECAST = {\n    description: _t(\"Predict value by computing a polynomial regression of the dataset.\"),\n    args: [\n        arg(\"x (number, range<number>)\", _t(\"The value(s) on the x-axis to forecast.\")),\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n        arg(\"order (number)\", _t(\"The order of the polynomial to fit the data, between 1 and 6.\")),\n        arg(\"intercept (boolean, default=TRUE)\", _t(\"A flag specifying whether to compute the intercept or not.\")),\n    ],\n    compute: function (x, dataY, dataX, order, intercept = { value: true }) {\n        const _order = toNumber(order, this.locale);\n        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n        const coeffs = polynomialRegression(flatDataY, flatDataX, _order, toBoolean(intercept)).flat();\n        return matrixMap(toMatrix(x), (xij) => evaluatePolynomial(coeffs, toNumber(xij, this.locale), _order));\n    },\n    isExported: false,\n};\n// -----------------------------------------------------------------------------\n// QUARTILE\n// -----------------------------------------------------------------------------\nconst QUARTILE = {\n    description: _t(\"Value nearest to a specific quartile of a dataset.\"),\n    args: [\n        arg(\"data (any, range)\", _t(\"The array or range containing the dataset to consider.\")),\n        arg(\"quartile_number (number)\", _t(\"Which quartile value to return.\")),\n    ],\n    compute: function (data, quartileNumber) {\n        return QUARTILE_INC.compute.bind(this)(data, quartileNumber);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// QUARTILE.EXC\n// -----------------------------------------------------------------------------\nconst QUARTILE_EXC = {\n    description: _t(\"Value nearest to a specific quartile of a dataset exclusive of 0 and 4.\"),\n    args: [\n        arg(\"data (any, range)\", _t(\"The array or range containing the dataset to consider.\")),\n        arg(\"quartile_number (number)\", _t(\"Which quartile value, exclusive of 0 and 4, to return.\")),\n    ],\n    compute: function (data, quartileNumber) {\n        const _quartileNumber = Math.trunc(toNumber(quartileNumber, this.locale));\n        const percent = { value: 0.25 * _quartileNumber };\n        return {\n            value: centile([data], percent, false, this.locale),\n            format: inferFormat(data),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// QUARTILE.INC\n// -----------------------------------------------------------------------------\nconst QUARTILE_INC = {\n    description: _t(\"Value nearest to a specific quartile of a dataset.\"),\n    args: [\n        arg(\"data (any, range)\", _t(\"The array or range containing the dataset to consider.\")),\n        arg(\"quartile_number (number)\", _t(\"Which quartile value to return.\")),\n    ],\n    compute: function (data, quartileNumber) {\n        const percent = { value: 0.25 * Math.trunc(toNumber(quartileNumber, this.locale)) };\n        return {\n            value: centile([data], percent, true, this.locale),\n            format: inferFormat(data),\n        };\n    },\n    isExported: true,\n};\n// RANK\n// -----------------------------------------------------------------------------\nconst RANK = {\n    description: _t(\"Returns the rank of a specified value in a dataset.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The value whose rank will be determined.\")),\n        arg(\"data (range)\", _t(\"The range containing the dataset to consider.\")),\n        arg(\"is_ascending (boolean, default=FALSE)\", _t(\"Whether to consider the values in data in descending or ascending order.\")),\n    ],\n    compute: function (value, data, isAscending = { value: false }) {\n        const _isAscending = toBoolean(isAscending);\n        const _value = toNumber(value, this.locale);\n        let rank = 1;\n        let found = false;\n        for (const row of data) {\n            for (const cell of row) {\n                if (typeof cell.value !== \"number\") {\n                    continue;\n                }\n                const _cell = toNumber(cell, this.locale);\n                if (_cell === _value) {\n                    found = true;\n                }\n                else if (_cell > _value !== _isAscending) {\n                    rank++;\n                }\n            }\n        }\n        if (!found) {\n            throw new NotAvailableError(_t(\"Value not found in the given data.\"));\n        }\n        return rank;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// RSQ\n// -----------------------------------------------------------------------------\nconst RSQ = {\n    description: _t(\"Compute the square of r, the Pearson product-moment correlation coefficient of a dataset.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataY, dataX) {\n        return Math.pow(pearson(dataX, dataY), 2.0);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SLOPE\n// -----------------------------------------------------------------------------\nconst SLOPE = {\n    description: _t(\"Compute the slope of the linear regression.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataY, dataX) {\n        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n        const [[slope]] = fullLinearRegression([flatDataX], [flatDataY]);\n        return slope;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SMALL\n// -----------------------------------------------------------------------------\nconst SMALL = {\n    description: _t(\"Nth smallest element in a data set.\"),\n    args: [\n        arg(\"data (any, range)\", _t(\"The array or range containing the dataset to consider.\")),\n        arg(\"n (number)\", _t(\"The rank from smallest to largest of the element to return.\")),\n    ],\n    compute: function (data, n) {\n        const _n = Math.trunc(toNumber(n?.value, this.locale));\n        let largests = [];\n        let index;\n        let count = 0;\n        visitAny([data], (d) => {\n            if (typeof d?.value === \"number\") {\n                index = dichotomicSearch(largests, d, \"nextSmaller\", \"asc\", largests.length, (array, i) => array[i].value);\n                largests.splice(index + 1, 0, d);\n                count++;\n                if (count > _n) {\n                    largests.pop();\n                    count--;\n                }\n            }\n        });\n        const result = largests.pop();\n        assert(() => result !== undefined, _t(\"[[FUNCTION_NAME]] has no valid input data.\"));\n        assert(() => count >= _n, _t(\"Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.\", _n));\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SPEARMAN\n// -----------------------------------------------------------------------------\nconst SPEARMAN = {\n    description: _t(\"Compute the Spearman rank correlation coefficient of a dataset.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataX, dataY) {\n        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n        const n = flatDataX.length;\n        const order = flatDataX.map((e, i) => [e, flatDataY[i]]);\n        order.sort((a, b) => a[0] - b[0]);\n        for (let i = 0; i < n; ++i) {\n            order[i][0] = i;\n        }\n        order.sort((a, b) => a[1] - b[1]);\n        let sum = 0.0;\n        for (let i = 0; i < n; ++i) {\n            sum += (order[i][0] - i) ** 2;\n        }\n        return 1 - (6 * sum) / (n ** 3 - n);\n    },\n    isExported: false,\n};\n// -----------------------------------------------------------------------------\n// STDEV\n// -----------------------------------------------------------------------------\nconst STDEV = {\n    description: _t(\"Standard deviation.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the sample.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the sample.\")),\n    ],\n    compute: function (...args) {\n        return Math.sqrt(VAR.compute.bind(this)(...args));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// STDEV.P\n// -----------------------------------------------------------------------------\nconst STDEV_P = {\n    description: _t(\"Standard deviation of entire population.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the population.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the population.\")),\n    ],\n    compute: function (...args) {\n        return Math.sqrt(VAR_P.compute.bind(this)(...args));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// STDEV.S\n// -----------------------------------------------------------------------------\nconst STDEV_S = {\n    description: _t(\"Standard deviation.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the sample.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the sample.\")),\n    ],\n    compute: function (...args) {\n        return Math.sqrt(VAR_S.compute.bind(this)(...args));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// STDEVA\n// -----------------------------------------------------------------------------\nconst STDEVA = {\n    description: _t(\"Standard deviation of sample (text as 0).\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the sample.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the sample.\")),\n    ],\n    compute: function (...args) {\n        return Math.sqrt(VARA.compute.bind(this)(...args));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// STDEVP\n// -----------------------------------------------------------------------------\nconst STDEVP = {\n    description: _t(\"Standard deviation of entire population.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the population.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the population.\")),\n    ],\n    compute: function (...args) {\n        return Math.sqrt(VARP.compute.bind(this)(...args));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// STDEVPA\n// -----------------------------------------------------------------------------\nconst STDEVPA = {\n    description: _t(\"Standard deviation of entire population (text as 0).\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the population.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the population.\")),\n    ],\n    compute: function (...args) {\n        return Math.sqrt(VARPA.compute.bind(this)(...args));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// STEYX\n// -----------------------------------------------------------------------------\nconst STEYX = {\n    description: _t(\"Calculates the standard error of the predicted y-value for each x in the regression of a dataset.\"),\n    args: [\n        arg(\"data_y (range<number>)\", _t(\"The range representing the array or matrix of dependent data.\")),\n        arg(\"data_x (range<number>)\", _t(\"The range representing the array or matrix of independent data.\")),\n    ],\n    compute: function (dataY, dataX) {\n        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);\n        const data = fullLinearRegression([flatDataX], [flatDataY], true, true);\n        return data[1][2];\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TREND\n// -----------------------------------------------------------------------------\nconst TREND = {\n    description: _t(\"Fits points to linear trend derived via least-squares.\"),\n    args: [\n        arg(\"known_data_y (number, range<number>)\", _t(\"The array or range containing dependent (y) values that are already known, used to curve fit an ideal linear trend.\")),\n        arg(\"known_data_x (number, range<number>, optional, default={1;2;3;...})\", _t(\"The values of the independent variable(s) corresponding with known_data_y.\")),\n        arg(\"new_data_x (number, range<number>, optional, default=known_data_x)\", _t(\"The data points to return the y values for on the ideal curve fit.\")),\n        arg(\"b (boolean, optional, default=TRUE)\", _t(\"Given a general linear form of y = m*x+b for a curve fit, calculates b if TRUE or forces b to be 0 and only calculates the m values if FALSE, i.e. forces the curve fit to pass through the origin.\")),\n    ],\n    compute: function (knownDataY, knownDataX = [[]], newDataX = [[]], b = { value: true }) {\n        return predictLinearValues(toNumberMatrix(knownDataY, \"the first argument (known_data_y)\"), toNumberMatrix(knownDataX, \"the second argument (known_data_x)\"), toNumberMatrix(newDataX, \"the third argument (new_data_y)\"), toBoolean(b));\n    },\n};\n// -----------------------------------------------------------------------------\n// VAR\n// -----------------------------------------------------------------------------\nconst VAR = {\n    description: _t(\"Variance.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the sample.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the sample.\")),\n    ],\n    compute: function (...args) {\n        return variance(args, true, false, this.locale);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// VAR.P\n// -----------------------------------------------------------------------------\nconst VAR_P = {\n    description: _t(\"Variance of entire population.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the population.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the population.\")),\n    ],\n    compute: function (...args) {\n        return variance(args, false, false, this.locale);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// VAR.S\n// -----------------------------------------------------------------------------\nconst VAR_S = {\n    description: _t(\"Variance.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the sample.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the sample.\")),\n    ],\n    compute: function (...args) {\n        return variance(args, true, false, this.locale);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// VARA\n// -----------------------------------------------------------------------------\nconst VARA = {\n    description: _t(\"Variance of sample (text as 0).\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the sample.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the sample.\")),\n    ],\n    compute: function (...args) {\n        return variance(args, true, true, this.locale);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// VARP\n// -----------------------------------------------------------------------------\nconst VARP = {\n    description: _t(\"Variance of entire population.\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the population.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the population.\")),\n    ],\n    compute: function (...args) {\n        return variance(args, false, false, this.locale);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// VARPA\n// -----------------------------------------------------------------------------\nconst VARPA = {\n    description: _t(\"Variance of entire population (text as 0).\"),\n    args: [\n        arg(\"value1 (number, range<number>)\", _t(\"The first value or range of the population.\")),\n        arg(\"value2 (number, range<number>, repeating)\", _t(\"Additional values or ranges to include in the population.\")),\n    ],\n    compute: function (...args) {\n        return variance(args, false, true, this.locale);\n    },\n    isExported: true,\n};\n\nvar statistical = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    AVEDEV: AVEDEV,\n    AVERAGE: AVERAGE,\n    AVERAGEA: AVERAGEA,\n    AVERAGEIF: AVERAGEIF,\n    AVERAGEIFS: AVERAGEIFS,\n    AVERAGE_WEIGHTED: AVERAGE_WEIGHTED,\n    CORREL: CORREL,\n    COUNT: COUNT,\n    COUNTA: COUNTA,\n    COVAR: COVAR,\n    COVARIANCE_P: COVARIANCE_P,\n    COVARIANCE_S: COVARIANCE_S,\n    FORECAST: FORECAST,\n    GROWTH: GROWTH,\n    INTERCEPT: INTERCEPT,\n    LARGE: LARGE,\n    LINEST: LINEST,\n    LOGEST: LOGEST,\n    MATTHEWS: MATTHEWS,\n    MAX: MAX,\n    MAXA: MAXA,\n    MAXIFS: MAXIFS,\n    MEDIAN: MEDIAN,\n    MIN: MIN,\n    MINA: MINA,\n    MINIFS: MINIFS,\n    PEARSON: PEARSON,\n    PERCENTILE: PERCENTILE,\n    PERCENTILE_EXC: PERCENTILE_EXC,\n    PERCENTILE_INC: PERCENTILE_INC,\n    POLYFIT_COEFFS: POLYFIT_COEFFS,\n    POLYFIT_FORECAST: POLYFIT_FORECAST,\n    QUARTILE: QUARTILE,\n    QUARTILE_EXC: QUARTILE_EXC,\n    QUARTILE_INC: QUARTILE_INC,\n    RANK: RANK,\n    RSQ: RSQ,\n    SLOPE: SLOPE,\n    SMALL: SMALL,\n    SPEARMAN: SPEARMAN,\n    STDEV: STDEV,\n    STDEVA: STDEVA,\n    STDEVP: STDEVP,\n    STDEVPA: STDEVPA,\n    STDEV_P: STDEV_P,\n    STDEV_S: STDEV_S,\n    STEYX: STEYX,\n    TREND: TREND,\n    VAR: VAR,\n    VARA: VARA,\n    VARP: VARP,\n    VARPA: VARPA,\n    VAR_P: VAR_P,\n    VAR_S: VAR_S\n});\n\nfunction getMatchingCells(database, field, criteria, locale) {\n    // Example\n    // # DATABASE             # CRITERIA          # field = \"C\"\n    //\n    // | A | B | C |          | A | C |\n    // |===========|          |=======|\n    // | 1 | x | j |          |<2 | j |\n    // | 1 | Z | k |          |   | 7 |\n    // | 5 | y | 7 |\n    // 1 - Select coordinates of database columns ----------------------------------------------------\n    const indexColNameDB = new Map();\n    const dimRowDB = database.length;\n    for (let indexCol = dimRowDB - 1; indexCol >= 0; indexCol--) {\n        indexColNameDB.set(toString(database[indexCol][0]).toUpperCase(), indexCol);\n    }\n    // Example continuation: indexColNameDB = {\"A\" => 0, \"B\" => 1, \"C\" => 2}\n    // 2 - Check if the field parameter exists in the column names of the database -------------------\n    // field may either be a text label corresponding to a column header in the\n    // first row of database or a numeric index indicating which column to consider,\n    // where the first column has the value 1.\n    const fieldValue = field?.value;\n    if (typeof fieldValue !== \"number\" && typeof fieldValue !== \"string\") {\n        throw new EvaluationError(_t(\"The field must be a number or a string\"));\n    }\n    let index;\n    if (typeof fieldValue === \"number\") {\n        index = Math.trunc(fieldValue) - 1;\n        if (index < 0 || dimRowDB - 1 < index) {\n            throw new EvaluationError(_t(\"The field (%(fieldValue)s) must be one of %(dimRowDB)s or must be a number between 1 and %s inclusive.\", {\n                fieldValue: fieldValue.toString(),\n                dimRowDB: dimRowDB.toString(),\n            }));\n        }\n    }\n    else {\n        const colName = toString(field).toUpperCase();\n        index = indexColNameDB.get(colName) ?? -1;\n        if (index === -1) {\n            throw new EvaluationError(_t(\"The field (%s) must be one of %s.\", toString(field), [...indexColNameDB.keys()].toString()));\n        }\n    }\n    // Example continuation: index = 2\n    // 3 - For each criteria row, find database row that correspond ----------------------------------\n    const dimColCriteria = criteria[0].length;\n    if (dimColCriteria < 2) {\n        throw new EvaluationError(_t(\"The criteria range contains %s row, it must be at least 2 rows.\", dimColCriteria.toString()));\n    }\n    let matchingRows = new Set();\n    const dimColDB = database[0].length;\n    for (let indexRow = 1; indexRow < dimColCriteria; indexRow++) {\n        let args = [];\n        let existColNameDB = true;\n        for (let indexCol = 0; indexCol < criteria.length; indexCol++) {\n            const currentName = toString(criteria[indexCol][0]).toUpperCase();\n            const indexColDB = indexColNameDB.get(currentName);\n            const criter = criteria[indexCol][indexRow];\n            if (criter.value !== null) {\n                if (indexColDB !== undefined) {\n                    args.push([database[indexColDB].slice(1, dimColDB)]);\n                    args.push(criter);\n                }\n                else {\n                    existColNameDB = false;\n                    break;\n                }\n            }\n        }\n        // Example continuation: args1 = [[1,1,5], \"<2\", [\"j\",\"k\",7], \"j\"]\n        // Example continuation: args2 = [[\"j\",\"k\",7], \"7\"]\n        if (existColNameDB) {\n            if (args.length > 0) {\n                visitMatchingRanges(args, (i, j) => {\n                    matchingRows.add(j);\n                }, locale, true);\n            }\n            else {\n                // return indices of each database row when a criteria table row is void\n                matchingRows = new Set(Array(dimColDB - 1).keys());\n                break;\n            }\n        }\n    }\n    // Example continuation: matchingRows = {0, 2}\n    // 4 - return for each database row corresponding, the cells corresponding to the field parameter\n    const fieldCol = database[index];\n    // Example continuation:: fieldCol = [\"C\", \"j\", \"k\", 7]\n    const matchingCells = [...matchingRows].map((x) => fieldCol[x + 1]);\n    // Example continuation:: matchingCells = [\"j\", 7]\n    return matchingCells;\n}\nconst databaseArgs = [\n    arg(\"database (range)\", _t(\"The array or range containing the data to consider, structured in such a way that the first row contains the labels for each column's values.\")),\n    arg(\"field (number, string)\", _t(\"Indicates which column in database contains the values to be extracted and operated on.\")),\n    arg(\"criteria (range)\", _t(\"An array or range containing zero or more criteria to filter the database values by before operating.\")),\n];\n// -----------------------------------------------------------------------------\n// DAVERAGE\n// -----------------------------------------------------------------------------\nconst DAVERAGE = {\n    description: _t(\"Average of a set of values from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return AVERAGE.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DCOUNT\n// -----------------------------------------------------------------------------\nconst DCOUNT = {\n    description: _t(\"Counts values from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return COUNT.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DCOUNTA\n// -----------------------------------------------------------------------------\nconst DCOUNTA = {\n    description: _t(\"Counts values and text from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return COUNTA.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DGET\n// -----------------------------------------------------------------------------\nconst DGET = {\n    description: _t(\"Single value from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        assert(() => cells.length === 1, _t(\"More than one match found in DGET evaluation.\"));\n        return cells[0];\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DMAX\n// -----------------------------------------------------------------------------\nconst DMAX = {\n    description: _t(\"Maximum of values from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return MAX.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DMIN\n// -----------------------------------------------------------------------------\nconst DMIN = {\n    description: _t(\"Minimum of values from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return MIN.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DPRODUCT\n// -----------------------------------------------------------------------------\nconst DPRODUCT = {\n    description: _t(\"Product of values from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return PRODUCT.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DSTDEV\n// -----------------------------------------------------------------------------\nconst DSTDEV = {\n    description: _t(\"Standard deviation of population sample from table.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return STDEV.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DSTDEVP\n// -----------------------------------------------------------------------------\nconst DSTDEVP = {\n    description: _t(\"Standard deviation of entire population from table.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return STDEVP.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DSUM\n// -----------------------------------------------------------------------------\nconst DSUM = {\n    description: _t(\"Sum of values from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return SUM.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DVAR\n// -----------------------------------------------------------------------------\nconst DVAR = {\n    description: _t(\"Variance of population sample from table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return VAR.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DVARP\n// -----------------------------------------------------------------------------\nconst DVARP = {\n    description: _t(\"Variance of a population from a table-like range.\"),\n    args: databaseArgs,\n    compute: function (database, field, criteria) {\n        const cells = getMatchingCells(database, field, criteria, this.locale);\n        return VARP.compute.bind(this)([cells]);\n    },\n    isExported: true,\n};\n\nvar database = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    DAVERAGE: DAVERAGE,\n    DCOUNT: DCOUNT,\n    DCOUNTA: DCOUNTA,\n    DGET: DGET,\n    DMAX: DMAX,\n    DMIN: DMIN,\n    DPRODUCT: DPRODUCT,\n    DSTDEV: DSTDEV,\n    DSTDEVP: DSTDEVP,\n    DSUM: DSUM,\n    DVAR: DVAR,\n    DVARP: DVARP\n});\n\nconst DEFAULT_TYPE = 1;\nconst DEFAULT_WEEKEND = 1;\nvar TIME_UNIT;\n(function (TIME_UNIT) {\n    TIME_UNIT[\"WHOLE_YEARS\"] = \"Y\";\n    TIME_UNIT[\"WHOLE_MONTHS\"] = \"M\";\n    TIME_UNIT[\"WHOLE_DAYS\"] = \"D\";\n    TIME_UNIT[\"DAYS_WITHOUT_WHOLE_MONTHS\"] = \"MD\";\n    TIME_UNIT[\"MONTH_WITHOUT_WHOLE_YEARS\"] = \"YM\";\n    TIME_UNIT[\"DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR\"] = \"YD\";\n})(TIME_UNIT || (TIME_UNIT = {}));\n// -----------------------------------------------------------------------------\n// DATE\n// -----------------------------------------------------------------------------\nconst DATE = {\n    description: _t(\"Converts year/month/day into a date.\"),\n    args: [\n        arg(\"year (number)\", _t(\"The year component of the date.\")),\n        arg(\"month (number)\", _t(\"The month component of the date.\")),\n        arg(\"day (number)\", _t(\"The day component of the date.\")),\n    ],\n    compute: function (year, month, day) {\n        let _year = Math.trunc(toNumber(year, this.locale));\n        const _month = Math.trunc(toNumber(month, this.locale));\n        const _day = Math.trunc(toNumber(day, this.locale));\n        // For years less than 0 or greater than 10000, return #ERROR.\n        assert(() => 0 <= _year && _year <= 9999, _t(\"The year (%s) must be between 0 and 9999 inclusive.\", _year.toString()));\n        // Between 0 and 1899, we add that value to 1900 to calculate the year\n        if (_year < 1900) {\n            _year += 1900;\n        }\n        const jsDate = new DateTime(_year, _month - 1, _day);\n        const result = jsDateToRoundNumber(jsDate);\n        assert(() => result >= 0, _t(\"The function [[FUNCTION_NAME]] result must be greater than or equal 01/01/1900.\"));\n        return {\n            value: result,\n            format: this.locale.dateFormat,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DATEDIF\n// -----------------------------------------------------------------------------\nconst DATEDIF = {\n    description: _t(\"Calculates the number of days, months, or years between two dates.\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The start date to consider in the calculation. Must be a reference to a cell containing a DATE, a function returning a DATE type, or a number.\")),\n        arg(\"end_date (date)\", _t(\"The end date to consider in the calculation. Must be a reference to a cell containing a DATE, a function returning a DATE type, or a number.\")),\n        arg(\"unit (string)\", _t('A text abbreviation for unit of time. Accepted values are \"Y\" (the number of whole years between start_date and end_date), \"M\" (the number of whole months between start_date and end_date), \"D\" (the number of days between start_date and end_date), \"MD\" (the number of days between start_date and end_date after subtracting whole months), \"YM\" (the number of whole months between start_date and end_date after subtracting whole years), \"YD\" (the number of days between start_date and end_date, assuming start_date and end_date were no more than one year apart).')),\n    ],\n    compute: function (startDate, endDate, unit) {\n        const _unit = toString(unit).toUpperCase();\n        assert(() => Object.values(TIME_UNIT).includes(_unit), expectStringSetError(Object.values(TIME_UNIT), toString(unit)));\n        const _startDate = Math.trunc(toNumber(startDate, this.locale));\n        const _endDate = Math.trunc(toNumber(endDate, this.locale));\n        const jsStartDate = numberToJsDate(_startDate);\n        const jsEndDate = numberToJsDate(_endDate);\n        assert(() => _endDate >= _startDate, _t(\"start_date (%s) should be on or before end_date (%s).\", jsStartDate.toLocaleDateString(), jsEndDate.toLocaleDateString()));\n        switch (_unit) {\n            case TIME_UNIT.WHOLE_YEARS:\n                return getTimeDifferenceInWholeYears(jsStartDate, jsEndDate);\n            case TIME_UNIT.WHOLE_MONTHS:\n                return getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate);\n            case TIME_UNIT.WHOLE_DAYS: {\n                return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate);\n            }\n            case TIME_UNIT.MONTH_WITHOUT_WHOLE_YEARS: {\n                return (getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate) -\n                    getTimeDifferenceInWholeYears(jsStartDate, jsEndDate) * 12);\n            }\n            case TIME_UNIT.DAYS_WITHOUT_WHOLE_MONTHS:\n                // Using \"MD\" may get incorrect result in Excel\n                // See: https://support.microsoft.com/en-us/office/datedif-function-25dba1a4-2812-480b-84dd-8b32a451b35c\n                let days = jsEndDate.getDate() - jsStartDate.getDate();\n                if (days < 0) {\n                    const monthBeforeEndMonth = new DateTime(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1);\n                    const daysInMonthBeforeEndMonth = getDaysInMonth(monthBeforeEndMonth);\n                    days = daysInMonthBeforeEndMonth - Math.abs(days);\n                }\n                return days;\n            case TIME_UNIT.DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR: {\n                if (areTwoDatesWithinOneYear(_startDate, _endDate)) {\n                    return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate);\n                }\n                const endDateWithinOneYear = new DateTime(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate());\n                let days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear);\n                if (days < 0) {\n                    endDateWithinOneYear.setFullYear(jsStartDate.getFullYear() + 1);\n                    days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear);\n                }\n                return days;\n            }\n        }\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DATEVALUE\n// -----------------------------------------------------------------------------\nconst DATEVALUE = {\n    description: _t(\"Converts a date string to a date value.\"),\n    args: [arg(\"date_string (string)\", _t(\"The string representing the date.\"))],\n    compute: function (dateString) {\n        const _dateString = toString(dateString);\n        const internalDate = parseDateTime(_dateString, this.locale);\n        assert(() => internalDate !== null, _t(\"The date_string (%s) cannot be parsed to date/time.\", _dateString.toString()));\n        return Math.trunc(internalDate.value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DAY\n// -----------------------------------------------------------------------------\nconst DAY = {\n    description: _t(\"Day of the month that a specific date falls on.\"),\n    args: [arg(\"date (string)\", _t(\"The date from which to extract the day.\"))],\n    compute: function (date) {\n        return toJsDate(date, this.locale).getDate();\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DAYS\n// -----------------------------------------------------------------------------\nconst DAYS = {\n    description: _t(\"Number of days between two dates.\"),\n    args: [\n        arg(\"end_date (date)\", _t(\"The end of the date range.\")),\n        arg(\"start_date (date)\", _t(\"The start of the date range.\")),\n    ],\n    compute: function (endDate, startDate) {\n        const _endDate = toJsDate(endDate, this.locale);\n        const _startDate = toJsDate(startDate, this.locale);\n        const dateDif = _endDate.getTime() - _startDate.getTime();\n        return Math.round(dateDif / MS_PER_DAY);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DAYS360\n// -----------------------------------------------------------------------------\nconst DEFAULT_DAY_COUNT_METHOD = 0;\nconst DAYS360 = {\n    description: _t(\"Number of days between two dates on a 360-day year (months of 30 days).\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The start date to consider in the calculation.\")),\n        arg(\"end_date (date)\", _t(\"The end date to consider in the calculation.\")),\n        arg(`method (number, default=${DEFAULT_DAY_COUNT_METHOD})`, _t(\"An indicator of what day count method to use. (0) US NASD method (1) European method\")),\n    ],\n    compute: function (startDate, endDate, method = { value: DEFAULT_DAY_COUNT_METHOD }) {\n        const _startDate = Math.trunc(toNumber(startDate, this.locale));\n        const _endDate = Math.trunc(toNumber(endDate, this.locale));\n        const dayCountConvention = toBoolean(method) ? 4 : 0;\n        const yearFrac = getYearFrac(_startDate, _endDate, dayCountConvention);\n        return Math.sign(_endDate - _startDate) * Math.round(yearFrac * 360);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// EDATE\n// -----------------------------------------------------------------------------\nconst EDATE = {\n    description: _t(\"Date a number of months before/after another date.\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The date from which to calculate the result.\")),\n        arg(\"months (number)\", _t(\"The number of months before (negative) or after (positive) 'start_date' to calculate.\")),\n    ],\n    compute: function (startDate, months) {\n        const _startDate = toJsDate(startDate, this.locale);\n        const _months = Math.trunc(toNumber(months, this.locale));\n        const jsDate = addMonthsToDate(_startDate, _months, false);\n        return {\n            value: jsDateToRoundNumber(jsDate),\n            format: this.locale.dateFormat,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// EOMONTH\n// -----------------------------------------------------------------------------\nconst EOMONTH = {\n    description: _t(\"Last day of a month before or after a date.\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The date from which to calculate the result.\")),\n        arg(\"months (number)\", _t(\"The number of months before (negative) or after (positive) 'start_date' to consider.\")),\n    ],\n    compute: function (startDate, months) {\n        const _startDate = toJsDate(startDate, this.locale);\n        const _months = Math.trunc(toNumber(months, this.locale));\n        const yStart = _startDate.getFullYear();\n        const mStart = _startDate.getMonth();\n        const jsDate = new DateTime(yStart, mStart + _months + 1, 0);\n        return {\n            value: jsDateToRoundNumber(jsDate),\n            format: this.locale.dateFormat,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// HOUR\n// -----------------------------------------------------------------------------\nconst HOUR = {\n    description: _t(\"Hour component of a specific time.\"),\n    args: [arg(\"time (date)\", _t(\"The time from which to calculate the hour component.\"))],\n    compute: function (date) {\n        return toJsDate(date, this.locale).getHours();\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISOWEEKNUM\n// -----------------------------------------------------------------------------\nconst ISOWEEKNUM = {\n    description: _t(\"ISO week number of the year.\"),\n    args: [\n        arg(\"date (date)\", _t(\"The date for which to determine the ISO week number. Must be a reference to a cell containing a date, a function returning a date type, or a number.\")),\n    ],\n    compute: function (date) {\n        const _date = toJsDate(date, this.locale);\n        const y = _date.getFullYear();\n        // 1 - As the 1st week of a year can start the previous year or after the 1st\n        // january we first look if the date is in the weeks of the current year, previous\n        // year or year after.\n        // A - We look for the current year, the first days of the first week\n        // and the last days of the last week\n        // The first week of the year is the week that contains the first\n        // Thursday of the year.\n        let firstThursday = 1;\n        while (new DateTime(y, 0, firstThursday).getDay() !== 4) {\n            firstThursday += 1;\n        }\n        const firstDayOfFirstWeek = new DateTime(y, 0, firstThursday - 3);\n        // The last week of the year is the week that contains the last Thursday of\n        // the year.\n        let lastThursday = 31;\n        while (new DateTime(y, 11, lastThursday).getDay() !== 4) {\n            lastThursday -= 1;\n        }\n        const lastDayOfLastWeek = new DateTime(y, 11, lastThursday + 3);\n        // B - If our date > lastDayOfLastWeek then it's in the weeks of the year after\n        // If our date < firstDayOfFirstWeek then it's in the weeks of the year before\n        let offsetYear;\n        if (firstDayOfFirstWeek.getTime() <= _date.getTime()) {\n            if (_date.getTime() <= lastDayOfLastWeek.getTime()) {\n                offsetYear = 0;\n            }\n            else {\n                offsetYear = 1;\n            }\n        }\n        else {\n            offsetYear = -1;\n        }\n        // 2 - now that the year is known, we are looking at the difference between\n        // the first day of this year and the date. The difference in days divided by\n        // 7 gives us the week number\n        let firstDay;\n        switch (offsetYear) {\n            case 0:\n                firstDay = firstDayOfFirstWeek;\n                break;\n            case 1:\n                // firstDay is the 1st day of the 1st week of the year after\n                // firstDay = lastDayOfLastWeek + 1 Day\n                firstDay = new DateTime(y, 11, lastThursday + 3 + 1);\n                break;\n            case -1:\n                // firstDay is the 1st day of the 1st week of the previous year.\n                // The first week of the previous year is the week that contains the\n                // first Thursday of the previous year.\n                let firstThursdayPreviousYear = 1;\n                while (new DateTime(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) {\n                    firstThursdayPreviousYear += 1;\n                }\n                firstDay = new DateTime(y - 1, 0, firstThursdayPreviousYear - 3);\n                break;\n        }\n        const diff = (_date.getTime() - firstDay.getTime()) / MS_PER_DAY;\n        return Math.floor(diff / 7) + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MINUTE\n// -----------------------------------------------------------------------------\nconst MINUTE = {\n    description: _t(\"Minute component of a specific time.\"),\n    args: [arg(\"time (date)\", _t(\"The time from which to calculate the minute component.\"))],\n    compute: function (date) {\n        return toJsDate(date, this.locale).getMinutes();\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MONTH\n// -----------------------------------------------------------------------------\nconst MONTH = {\n    description: _t(\"Month of the year a specific date falls in\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to extract the month.\"))],\n    compute: function (date) {\n        return toJsDate(date, this.locale).getMonth() + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// NETWORKDAYS\n// -----------------------------------------------------------------------------\nconst NETWORKDAYS = {\n    description: _t(\"Net working days between two provided days.\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The start date of the period from which to calculate the number of net working days.\")),\n        arg(\"end_date (date)\", _t(\"The end date of the period from which to calculate the number of net working days.\")),\n        arg(\"holidays (date, range<date>, optional)\", _t(\"A range or array constant containing the date serial numbers to consider holidays.\")),\n    ],\n    compute: function (startDate, endDate, holidays) {\n        return NETWORKDAYS_INTL.compute.bind(this)(startDate, endDate, { value: 1 }, holidays);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// NETWORKDAYS.INTL\n// -----------------------------------------------------------------------------\n/**\n * Transform weekend Spreadsheet information into Date Day JavaScript information.\n * Take string (String method) or number (Number method), return array of numbers.\n *\n * String method: weekends can be specified using seven 0\u2019s and 1\u2019s, where the\n * first number in the set represents Monday and the last number is for Sunday.\n * A zero means that the day is a work day, a 1 means that the day is a weekend.\n * For example, \u201c0000011\u201d would mean Saturday and Sunday are weekends.\n *\n * Number method: instead of using the string method above, a single number can\n * be used. 1 = Saturday/Sunday are weekends, 2 = Sunday/Monday, and this pattern\n * repeats until 7 = Friday/Saturday. 11 = Sunday is the only weekend, 12 = Monday\n * is the only weekend, and this pattern repeats until 17 = Saturday is the only\n * weekend.\n *\n * Example:\n * - 11 return [0] (correspond to Sunday)\n * - 12 return [1] (correspond to Monday)\n * - 3 return [1,2] (correspond to Monday and Tuesday)\n * - \"0101010\" return [2,4,6] (correspond to Tuesday, Thursday and Saturday)\n */\nfunction weekendToDayNumber(data) {\n    const weekend = data?.value;\n    // case \"string\"\n    if (typeof weekend === \"string\") {\n        assert(() => {\n            if (weekend.length !== 7) {\n                return false;\n            }\n            for (let day of weekend) {\n                if (day !== \"0\" && day !== \"1\") {\n                    return false;\n                }\n            }\n            return true;\n        }, _t('When weekend is a string (%s) it must be composed of \"0\" or \"1\".', weekend));\n        let result = [];\n        for (let i = 0; i < 7; i++) {\n            if (weekend[i] === \"1\") {\n                result.push((i + 1) % 7);\n            }\n        }\n        return result;\n    }\n    //case \"number\"\n    if (typeof weekend === \"number\") {\n        assert(() => (1 <= weekend && weekend <= 7) || (11 <= weekend && weekend <= 17), _t(\"The weekend (%s) must be a string or a number in the range 1-7 or 11-17.\", weekend.toString()));\n        // case 1 <= weekend <= 7\n        if (weekend <= 7) {\n            // 1 = Saturday/Sunday are weekends\n            // 2 = Sunday/Monday\n            // ...\n            // 7 = Friday/Saturday.\n            return [weekend - 2 === -1 ? 6 : weekend - 2, weekend - 1];\n        }\n        // case 11 <= weekend <= 17\n        // 11 = Sunday is the only weekend\n        // 12 = Monday is the only weekend\n        // ...\n        // 17 = Saturday is the only weekend.\n        return [weekend - 11];\n    }\n    throw new EvaluationError(_t(\"The weekend must be a number or a string.\"));\n}\nconst NETWORKDAYS_INTL = {\n    description: _t(\"Net working days between two dates (specifying weekends).\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The start date of the period from which to calculate the number of net working days.\")),\n        arg(\"end_date (date)\", _t(\"The end date of the period from which to calculate the number of net working days.\")),\n        arg(`weekend (any, default=${DEFAULT_WEEKEND})`, _t(\"A number or string representing which days of the week are considered weekends.\")),\n        arg(\"holidays (date, range<date>, optional)\", _t(\"A range or array constant containing the dates to consider as holidays.\")),\n    ],\n    compute: function (startDate, endDate, weekend = { value: DEFAULT_WEEKEND }, holidays) {\n        const _startDate = toJsDate(startDate, this.locale);\n        const _endDate = toJsDate(endDate, this.locale);\n        const daysWeekend = weekendToDayNumber(weekend);\n        let timesHoliday = new Set();\n        if (holidays !== undefined) {\n            visitAny([holidays], (h) => {\n                const holiday = toJsDate(h, this.locale);\n                timesHoliday.add(holiday.getTime());\n            });\n        }\n        const invertDate = _startDate.getTime() > _endDate.getTime();\n        const stopDate = DateTime.fromTimestamp((invertDate ? _startDate : _endDate).getTime());\n        let stepDate = DateTime.fromTimestamp((invertDate ? _endDate : _startDate).getTime());\n        const timeStopDate = stopDate.getTime();\n        let timeStepDate = stepDate.getTime();\n        let netWorkingDay = 0;\n        while (timeStepDate <= timeStopDate) {\n            if (!daysWeekend.includes(stepDate.getDay()) && !timesHoliday.has(timeStepDate)) {\n                netWorkingDay += 1;\n            }\n            stepDate.setDate(stepDate.getDate() + 1);\n            timeStepDate = stepDate.getTime();\n        }\n        return invertDate ? -netWorkingDay : netWorkingDay;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// NOW\n// -----------------------------------------------------------------------------\nconst NOW = {\n    description: _t(\"Current date and time as a date value.\"),\n    args: [],\n    compute: function () {\n        const today = DateTime.now();\n        const delta = today.getTime() - INITIAL_1900_DAY.getTime();\n        const time = today.getHours() / 24 + today.getMinutes() / 1440 + today.getSeconds() / 86400;\n        return {\n            value: Math.floor(delta / MS_PER_DAY) + time,\n            format: getDateTimeFormat(this.locale),\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SECOND\n// -----------------------------------------------------------------------------\nconst SECOND = {\n    description: _t(\"Minute component of a specific time.\"),\n    args: [arg(\"time (date)\", _t(\"The time from which to calculate the second component.\"))],\n    compute: function (date) {\n        return toJsDate(date, this.locale).getSeconds();\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TIME\n// -----------------------------------------------------------------------------\nconst TIME = {\n    description: _t(\"Converts hour/minute/second into a time.\"),\n    args: [\n        arg(\"hour (number)\", _t(\"The hour component of the time.\")),\n        arg(\"minute (number)\", _t(\"The minute component of the time.\")),\n        arg(\"second (number)\", _t(\"The second component of the time.\")),\n    ],\n    compute: function (hour, minute, second) {\n        let _hour = Math.trunc(toNumber(hour, this.locale));\n        let _minute = Math.trunc(toNumber(minute, this.locale));\n        let _second = Math.trunc(toNumber(second, this.locale));\n        _minute += Math.floor(_second / 60);\n        _second = (_second % 60) + (_second < 0 ? 60 : 0);\n        _hour += Math.floor(_minute / 60);\n        _minute = (_minute % 60) + (_minute < 0 ? 60 : 0);\n        _hour %= 24;\n        assert(() => _hour >= 0, _t(\"The function [[FUNCTION_NAME]] result cannot be negative\"));\n        return {\n            value: _hour / 24 + _minute / (24 * 60) + _second / (24 * 60 * 60),\n            format: this.locale.timeFormat,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TIMEVALUE\n// -----------------------------------------------------------------------------\nconst TIMEVALUE = {\n    description: _t(\"Converts a time string into its serial number representation.\"),\n    args: [arg(\"time_string (string)\", _t(\"The string that holds the time representation.\"))],\n    compute: function (timeString) {\n        const _timeString = toString(timeString);\n        const internalDate = parseDateTime(_timeString, this.locale);\n        assert(() => internalDate !== null, _t(\"The time_string (%s) cannot be parsed to date/time.\", _timeString));\n        const result = internalDate.value - Math.trunc(internalDate.value);\n        return result < 0 ? 1 + result : result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TODAY\n// -----------------------------------------------------------------------------\nconst TODAY = {\n    description: _t(\"Current date as a date value.\"),\n    args: [],\n    compute: function () {\n        const today = DateTime.now();\n        const jsDate = new DateTime(today.getFullYear(), today.getMonth(), today.getDate());\n        return {\n            value: jsDateToRoundNumber(jsDate),\n            format: this.locale.dateFormat,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// WEEKDAY\n// -----------------------------------------------------------------------------\nconst WEEKDAY = {\n    description: _t(\"Day of the week of the date provided (as number).\"),\n    args: [\n        arg(\"date (date)\", _t(\"The date for which to determine the day of the week. Must be a reference to a cell containing a date, a function returning a date type, or a number.\")),\n        arg(`type (number, default=${DEFAULT_TYPE})`, _t(\"A number indicating which numbering system to use to represent weekdays. By default, counts starting with Sunday = 1.\")),\n    ],\n    compute: function (date, type = { value: DEFAULT_TYPE }) {\n        const _date = toJsDate(date, this.locale);\n        const _type = Math.round(toNumber(type, this.locale));\n        const m = _date.getDay();\n        assert(() => [1, 2, 3].includes(_type), _t(\"The type (%s) must be 1, 2 or 3.\", _type.toString()));\n        if (_type === 1)\n            return m + 1;\n        if (_type === 2)\n            return m === 0 ? 7 : m;\n        return m === 0 ? 6 : m - 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// WEEKNUM\n// -----------------------------------------------------------------------------\nconst WEEKNUM = {\n    description: _t(\"Week number of the year.\"),\n    args: [\n        arg(\"date (date)\", _t(\"The date for which to determine the week number. Must be a reference to a cell containing a date, a function returning a date type, or a number.\")),\n        arg(`type (number, default=${DEFAULT_TYPE})`, _t(\"A number representing the day that a week starts on. Sunday = 1.\")),\n    ],\n    compute: function (date, type = { value: DEFAULT_TYPE }) {\n        const _date = toJsDate(date, this.locale);\n        const _type = Math.round(toNumber(type, this.locale));\n        assert(() => _type === 1 || _type === 2 || (11 <= _type && _type <= 17) || _type === 21, _t(\"The type (%s) is out of range.\", _type.toString()));\n        if (_type === 21) {\n            return ISOWEEKNUM.compute.bind(this)(date);\n        }\n        let startDayOfWeek;\n        if (_type === 1 || _type === 2) {\n            startDayOfWeek = _type - 1;\n        }\n        else {\n            // case 11 <= _type <= 17\n            startDayOfWeek = _type - 10 === 7 ? 0 : _type - 10;\n        }\n        const y = _date.getFullYear();\n        let dayStart = 1;\n        let startDayOfFirstWeek = new DateTime(y, 0, dayStart);\n        while (startDayOfFirstWeek.getDay() !== startDayOfWeek) {\n            dayStart += 1;\n            startDayOfFirstWeek = new DateTime(y, 0, dayStart);\n        }\n        const dif = (_date.getTime() - startDayOfFirstWeek.getTime()) / MS_PER_DAY;\n        if (dif < 0) {\n            return 1;\n        }\n        return Math.floor(dif / 7) + (dayStart === 1 ? 1 : 2);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// WORKDAY\n// -----------------------------------------------------------------------------\nconst WORKDAY = {\n    description: _t(\"Date after a number of workdays.\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The date from which to begin counting.\")),\n        arg(\"num_days (number)\", _t(\"The number of working days to advance from start_date. If negative, counts backwards.\")),\n        arg(\"holidays (date, range<date>, optional)\", _t(\"A range or array constant containing the dates to consider holidays.\")),\n    ],\n    compute: function (startDate, numDays, holidays = { value: null }) {\n        return WORKDAY_INTL.compute.bind(this)(startDate, numDays, { value: 1 }, holidays);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// WORKDAY.INTL\n// -----------------------------------------------------------------------------\nconst WORKDAY_INTL = {\n    description: _t(\"Date after a number of workdays (specifying weekends).\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The date from which to begin counting.\")),\n        arg(\"num_days (number)\", _t(\"The number of working days to advance from start_date. If negative, counts backwards.\")),\n        arg(`weekend (any, default=${DEFAULT_WEEKEND})`, _t(\"A number or string representing which days of the week are considered weekends.\")),\n        arg(\"holidays (date, range<date>, optional)\", _t(\"A range or array constant containing the dates to consider holidays.\")),\n    ],\n    compute: function (startDate, numDays, weekend = { value: DEFAULT_WEEKEND }, holidays) {\n        let _startDate = toJsDate(startDate, this.locale);\n        let _numDays = Math.trunc(toNumber(numDays, this.locale));\n        if (typeof weekend.value === \"string\") {\n            assert(() => weekend.value !== \"1111111\", _t(\"The weekend must be different from '1111111'.\"));\n        }\n        const daysWeekend = weekendToDayNumber(weekend);\n        let timesHoliday = new Set();\n        if (holidays !== undefined) {\n            visitAny([holidays], (h) => {\n                const holiday = toJsDate(h, this.locale);\n                timesHoliday.add(holiday.getTime());\n            });\n        }\n        let stepDate = DateTime.fromTimestamp(_startDate.getTime());\n        let timeStepDate = stepDate.getTime();\n        const unitDay = Math.sign(_numDays);\n        let stepDay = Math.abs(_numDays);\n        while (stepDay > 0) {\n            stepDate.setDate(stepDate.getDate() + unitDay);\n            timeStepDate = stepDate.getTime();\n            if (!daysWeekend.includes(stepDate.getDay()) && !timesHoliday.has(timeStepDate)) {\n                stepDay -= 1;\n            }\n        }\n        const delta = timeStepDate - INITIAL_1900_DAY.getTime();\n        return {\n            value: Math.round(delta / MS_PER_DAY),\n            format: this.locale.dateFormat,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// YEAR\n// -----------------------------------------------------------------------------\nconst YEAR = {\n    description: _t(\"Year specified by a given date.\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to extract the year.\"))],\n    compute: function (date) {\n        return toJsDate(date, this.locale).getFullYear();\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// YEARFRAC\n// -----------------------------------------------------------------------------\nconst DEFAULT_DAY_COUNT_CONVENTION$1 = 0;\nconst YEARFRAC = {\n    description: _t(\"Exact number of years between two dates.\"),\n    args: [\n        arg(\"start_date (date)\", _t(\"The start date to consider in the calculation. Must be a reference to a cell containing a date, a function returning a date type, or a number.\")),\n        arg(\"end_date (date)\", _t(\"The end date to consider in the calculation. Must be a reference to a cell containing a date, a function returning a date type, or a number.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION$1})`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (startDate, endDate, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION$1 }) {\n        let _startDate = Math.trunc(toNumber(startDate, this.locale));\n        let _endDate = Math.trunc(toNumber(endDate, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assert(() => _startDate >= 0, _t(\"The start_date (%s) must be positive or null.\", _startDate.toString()));\n        assert(() => _endDate >= 0, _t(\"The end_date (%s) must be positive or null.\", _endDate.toString()));\n        assert(() => 0 <= _dayCountConvention && _dayCountConvention <= 4, _t(\"The day_count_convention (%s) must be between 0 and 4 inclusive.\", _dayCountConvention.toString()));\n        return getYearFrac(_startDate, _endDate, _dayCountConvention);\n    },\n};\n// -----------------------------------------------------------------------------\n// MONTH.START\n// -----------------------------------------------------------------------------\nconst MONTH_START = {\n    description: _t(\"First day of the month preceding a date.\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to calculate the result.\"))],\n    compute: function (date) {\n        const _startDate = toJsDate(date, this.locale);\n        const yStart = _startDate.getFullYear();\n        const mStart = _startDate.getMonth();\n        const jsDate = new DateTime(yStart, mStart, 1);\n        return {\n            value: jsDateToRoundNumber(jsDate),\n            format: this.locale.dateFormat,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// MONTH.END\n// -----------------------------------------------------------------------------\nconst MONTH_END = {\n    description: _t(\"Last day of the month following a date.\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to calculate the result.\"))],\n    compute: function (date) {\n        return EOMONTH.compute.bind(this)(date, { value: 0 });\n    },\n};\n// -----------------------------------------------------------------------------\n// QUARTER\n// -----------------------------------------------------------------------------\nconst QUARTER = {\n    description: _t(\"Quarter of the year a specific date falls in\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to extract the quarter.\"))],\n    compute: function (date) {\n        return Math.ceil((toJsDate(date, this.locale).getMonth() + 1) / 3);\n    },\n};\n// -----------------------------------------------------------------------------\n// QUARTER.START\n// -----------------------------------------------------------------------------\nconst QUARTER_START = {\n    description: _t(\"First day of the quarter of the year a specific date falls in.\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to calculate the start of quarter.\"))],\n    compute: function (date) {\n        const quarter = QUARTER.compute.bind(this)(date);\n        const year = YEAR.compute.bind(this)(date);\n        const jsDate = new DateTime(year, (quarter - 1) * 3, 1);\n        return {\n            value: jsDateToRoundNumber(jsDate),\n            format: this.locale.dateFormat,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// QUARTER.END\n// -----------------------------------------------------------------------------\nconst QUARTER_END = {\n    description: _t(\"Last day of the quarter of the year a specific date falls in.\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to calculate the end of quarter.\"))],\n    compute: function (date) {\n        const quarter = QUARTER.compute.bind(this)(date);\n        const year = YEAR.compute.bind(this)(date);\n        const jsDate = new DateTime(year, quarter * 3, 0);\n        return {\n            value: jsDateToRoundNumber(jsDate),\n            format: this.locale.dateFormat,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// YEAR.START\n// -----------------------------------------------------------------------------\nconst YEAR_START = {\n    description: _t(\"First day of the year a specific date falls in.\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to calculate the start of the year.\"))],\n    compute: function (date) {\n        const year = YEAR.compute.bind(this)(date);\n        const jsDate = new DateTime(year, 0, 1);\n        return {\n            value: jsDateToRoundNumber(jsDate),\n            format: this.locale.dateFormat,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// YEAR.END\n// -----------------------------------------------------------------------------\nconst YEAR_END = {\n    description: _t(\"Last day of the year a specific date falls in.\"),\n    args: [arg(\"date (date)\", _t(\"The date from which to calculate the end of the year.\"))],\n    compute: function (date) {\n        const year = YEAR.compute.bind(this)(date);\n        const jsDate = new DateTime(year + 1, 0, 0);\n        return {\n            value: jsDateToRoundNumber(jsDate),\n            format: this.locale.dateFormat,\n        };\n    },\n};\n\nvar date = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    DATE: DATE,\n    DATEDIF: DATEDIF,\n    DATEVALUE: DATEVALUE,\n    DAY: DAY,\n    DAYS: DAYS,\n    DAYS360: DAYS360,\n    EDATE: EDATE,\n    EOMONTH: EOMONTH,\n    HOUR: HOUR,\n    ISOWEEKNUM: ISOWEEKNUM,\n    MINUTE: MINUTE,\n    MONTH: MONTH,\n    MONTH_END: MONTH_END,\n    MONTH_START: MONTH_START,\n    NETWORKDAYS: NETWORKDAYS,\n    NETWORKDAYS_INTL: NETWORKDAYS_INTL,\n    NOW: NOW,\n    QUARTER: QUARTER,\n    QUARTER_END: QUARTER_END,\n    QUARTER_START: QUARTER_START,\n    SECOND: SECOND,\n    TIME: TIME,\n    TIMEVALUE: TIMEVALUE,\n    TODAY: TODAY,\n    WEEKDAY: WEEKDAY,\n    WEEKNUM: WEEKNUM,\n    WORKDAY: WORKDAY,\n    WORKDAY_INTL: WORKDAY_INTL,\n    YEAR: YEAR,\n    YEARFRAC: YEARFRAC,\n    YEAR_END: YEAR_END,\n    YEAR_START: YEAR_START\n});\n\nconst DEFAULT_DELTA_ARG = 0;\n// -----------------------------------------------------------------------------\n// DELTA\n// -----------------------------------------------------------------------------\nconst DELTA = {\n    description: _t(\"Compare two numeric values, returning 1 if they're equal.\"),\n    args: [\n        arg(\"number1 (number)\", _t(\"The first number to compare.\")),\n        arg(`number2 (number, default=${DEFAULT_DELTA_ARG})`, _t(\"The second number to compare.\")),\n    ],\n    compute: function (number1, number2 = { value: DEFAULT_DELTA_ARG }) {\n        const _number1 = toNumber(number1, this.locale);\n        const _number2 = toNumber(number2, this.locale);\n        return _number1 === _number2 ? 1 : 0;\n    },\n    isExported: true,\n};\n\nvar engineering = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    DELTA: DELTA\n});\n\nconst SORT_TYPES = [\n    CellValueType.number,\n    CellValueType.error,\n    CellValueType.text,\n    CellValueType.boolean,\n];\nfunction cellsSortingCriterion(sortingOrder) {\n    const inverse = sortingOrder === \"ascending\" ? 1 : -1;\n    return (left, right) => {\n        if (left.type === CellValueType.empty) {\n            return right.type === CellValueType.empty ? 0 : 1;\n        }\n        else if (right.type === CellValueType.empty) {\n            return -1;\n        }\n        let typeOrder = SORT_TYPES.indexOf(left.type) - SORT_TYPES.indexOf(right.type);\n        if (typeOrder === 0) {\n            if (left.type === CellValueType.text || left.type === CellValueType.error) {\n                typeOrder = left.value.localeCompare(right.value);\n            }\n            else {\n                typeOrder = left.value - right.value;\n            }\n        }\n        return inverse * typeOrder;\n    };\n}\nfunction sortCells(cells, sortDirection, emptyCellAsZero) {\n    const cellsWithIndex = cells.map((cell, index) => ({\n        index,\n        type: cell.type,\n        value: cell.value,\n    }));\n    const cellsToSort = emptyCellAsZero\n        ? cellsWithIndex.map((cell) => cell.type === CellValueType.empty ? { ...cell, type: CellValueType.number, value: 0 } : cell)\n        : cellsWithIndex;\n    return cellsToSort.sort(cellsSortingCriterion(sortDirection));\n}\nfunction interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {\n    let result = DispatchResult.Success;\n    //several columns => bypass the contiguity check\n    let multiColumns = zone.right > zone.left;\n    if (env.model.getters.doesIntersectMerge(sheetId, zone)) {\n        multiColumns = false;\n        let table;\n        for (let row = zone.top; row <= zone.bottom; row++) {\n            table = [];\n            for (let col = zone.left; col <= zone.right; col++) {\n                let merge = env.model.getters.getMerge({ sheetId, col, row });\n                if (merge && !table.includes(merge.id.toString())) {\n                    table.push(merge.id.toString());\n                }\n            }\n            if (table.length >= 2) {\n                multiColumns = true;\n                break;\n            }\n        }\n    }\n    const { col, row } = anchor;\n    if (multiColumns) {\n        result = env.model.dispatch(\"SORT_CELLS\", { sheetId, col, row, zone, sortDirection });\n    }\n    else {\n        // check contiguity\n        const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);\n        if (isEqual(contiguousZone, zone)) {\n            // merge as it is\n            result = env.model.dispatch(\"SORT_CELLS\", {\n                sheetId,\n                col,\n                row,\n                zone,\n                sortDirection,\n            });\n        }\n        else {\n            env.askConfirmation(_t(\"We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?\"), () => {\n                zone = contiguousZone;\n                result = env.model.dispatch(\"SORT_CELLS\", {\n                    sheetId,\n                    col,\n                    row,\n                    zone,\n                    sortDirection,\n                });\n            }, () => {\n                result = env.model.dispatch(\"SORT_CELLS\", {\n                    sheetId,\n                    col,\n                    row,\n                    zone,\n                    sortDirection,\n                });\n            });\n        }\n    }\n    if (result.isCancelledBecause(\"InvalidSortZone\" /* CommandResult.InvalidSortZone */)) {\n        const { col, row } = anchor;\n        env.model.selection.selectZone({ cell: { col, row }, zone });\n        env.raiseError(_t(\"Cannot sort. To sort, select only cells or only merges that have the same size.\"));\n    }\n}\n\nfunction sortMatrix(matrix, locale, ...criteria) {\n    for (const [i, value] of criteria.entries()) {\n        assert(() => value !== undefined, _t(\"Value for parameter %d is missing, while the function [[FUNCTION_NAME]] expect a number or a range.\", i + 1));\n    }\n    const sortingOrders = [];\n    const sortColumns = [];\n    const nRows = matrix.length;\n    for (let i = 0; i < criteria.length; i += 2) {\n        sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? \"ascending\" : \"descending\");\n        const sortColumn = criteria[i];\n        if (isMatrix(sortColumn) && (sortColumn.length > 1 || sortColumn[0].length > 1)) {\n            assert(() => sortColumn.length === 1 && sortColumn[0].length === nRows, _t(\"Wrong size for %s. Expected a range of size 1x%s. Got %sx%s.\", `sort_column${i + 1}`, nRows, sortColumn.length, sortColumn[0].length));\n            sortColumns.push(sortColumn.flat().map((c) => c.value));\n        }\n        else {\n            const colIndex = toNumber(toScalar(sortColumn)?.value, locale);\n            if (colIndex < 1 || colIndex > matrix[0].length) {\n                return matrix;\n            }\n            sortColumns.push(matrix.map((row) => row[colIndex - 1].value));\n        }\n    }\n    if (sortColumns.length === 0) {\n        for (let i = 0; i < matrix[0].length; i++) {\n            sortColumns.push(matrix.map((row) => row[i].value));\n            sortingOrders.push(\"ascending\");\n        }\n    }\n    const sortingCriteria = {\n        descending: cellsSortingCriterion(\"descending\"),\n        ascending: cellsSortingCriterion(\"ascending\"),\n    };\n    const indexes = range(0, matrix.length);\n    indexes.sort((a, b) => {\n        for (const [i, sortColumn] of sortColumns.entries()) {\n            const left = sortColumn[a];\n            const right = sortColumn[b];\n            const leftCell = {\n                value: left,\n                type: left === null\n                    ? CellValueType.empty\n                    : typeof left === \"string\"\n                        ? CellValueType.text\n                        : typeof left,\n            };\n            const rightCell = {\n                value: right,\n                type: right === null\n                    ? CellValueType.empty\n                    : typeof right === \"string\"\n                        ? CellValueType.text\n                        : typeof right,\n            };\n            const result = sortingCriteria[sortingOrders[i]](leftCell, rightCell);\n            if (result !== 0) {\n                return result;\n            }\n        }\n        return 0;\n    });\n    return indexes.map((i) => matrix[i]);\n}\n// -----------------------------------------------------------------------------\n// FILTER\n// -----------------------------------------------------------------------------\nconst FILTER = {\n    description: _t(\"Returns a filtered version of the source range, returning only rows or columns that meet the specified conditions.\"),\n    // TODO modify args description when vectorization on formulas is available\n    args: [\n        arg(\"range (any, range<any>)\", _t(\"The data to be filtered.\")),\n        arg(\"condition1 (boolean, range<boolean>)\", _t(\"A column or row containing true or false values corresponding to the first column or row of range.\")),\n        arg(\"condition2 (boolean, range<boolean>, repeating)\", _t(\"Additional column or row containing true or false values.\")),\n    ],\n    compute: function (range, ...conditions) {\n        let _array = toMatrix(range);\n        const _conditionsMatrices = conditions.map((cond) => matrixMap(toMatrix(cond), (data) => data.value));\n        _conditionsMatrices.map((c) => assertSingleColOrRow(_t(\"The arguments condition must be a single column or row.\"), c));\n        assertSameDimensions(_t(\"The arguments conditions must have the same dimensions.\"), ...conditions);\n        const _conditions = _conditionsMatrices.map((c) => c.flat());\n        const mode = _conditionsMatrices[0].length === 1 ? \"row\" : \"col\";\n        _array = mode === \"row\" ? transposeMatrix(_array) : _array;\n        assert(() => _conditions.every((cond) => cond.length === _array.length), _t(\"FILTER has mismatched sizes on the range and conditions.\"));\n        const result = [];\n        for (let i = 0; i < _array.length; i++) {\n            const row = _array[i];\n            if (_conditions.every((c) => (typeof c[i] === \"boolean\" || typeof c[i] === \"number\") && c[i])) {\n                result.push(row);\n            }\n        }\n        if (!result.length) {\n            throw new NotAvailableError(_t(\"No match found in FILTER evaluation\"));\n        }\n        return mode === \"row\" ? transposeMatrix(result) : result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SORT\n// -----------------------------------------------------------------------------\nconst SORT = {\n    description: _t(\"Sorts the rows of a given array or range by the values in one or more columns.\"),\n    args: [\n        arg(\"range (range)\", _t(\"The data to be sorted.\")),\n        arg(\"sort_column (any, range<number>, repeating)\", _t(\"The index of the column in range or a range outside of range containing the values by which to sort.\")),\n        arg(\"is_ascending (boolean, repeating)\", _t(\"TRUE or FALSE indicating whether to sort sort_column in ascending order. FALSE sorts in descending order.\")),\n    ],\n    compute: function (range, ...sortingCriteria) {\n        const _range = transposeMatrix(range);\n        return transposeMatrix(sortMatrix(_range, this.locale, ...sortingCriteria));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SORTN\n// -----------------------------------------------------------------------------\nconst SORTN = {\n    description: _t(\"Returns the first n items in a data set after performing a sort.\"),\n    args: [\n        arg(\"range (range)\", _t(\"The data to be sorted.\")),\n        arg(\"n (number, default=1)\", _t(\"The number of items to return.\")),\n        arg(\"display_ties_mode (number, default=0)\", _t(\"A number representing the way to display ties.\")),\n        arg(\"sort_column (number, range<number>, repeating)\", _t(\"The index of the column in range or a range outside of range containing the values by which to sort.\")),\n        arg(\"is_ascending (boolean, repeating)\", _t(\"TRUE or FALSE indicating whether to sort sort_column in ascending order. FALSE sorts in descending order.\")),\n    ],\n    compute: function (range, n, displayTiesMode, ...sortingCriteria) {\n        const _n = toNumber(n?.value ?? 1, this.locale);\n        assert(() => _n >= 0, _t(\"Wrong value of 'n'. Expected a positive number. Got %s.\", _n));\n        const _displayTiesMode = toNumber(displayTiesMode?.value ?? 0, this.locale);\n        assert(() => _displayTiesMode >= 0 && _displayTiesMode <= 3, _t(\"Wrong value of 'display_ties_mode'. Expected a positive number between 0 and 3. Got %s.\", _displayTiesMode));\n        const sortedData = sortMatrix(transposeMatrix(range), this.locale, ...sortingCriteria);\n        const sameRows = (i, j) => JSON.stringify(sortedData[i].map((c) => c.value)) ===\n            JSON.stringify(sortedData[j].map((c) => c.value));\n        /*\n         * displayTiesMode determine how ties (equal values) are dealt with:\n         * 0 - ignore ties and show first n rows only\n         * 1 - show first n rows plus any additional ties with nth row\n         * 2 - show n rows but remove duplicates\n         * 3 - show first n unique rows and all duplicates of these rows\n         */\n        switch (_displayTiesMode) {\n            case 0:\n                return transposeMatrix(sortedData.slice(0, _n));\n            case 1:\n                for (let i = _n; i < sortedData.length; i++) {\n                    if (!sameRows(i, _n - 1)) {\n                        return transposeMatrix(sortedData.slice(0, i));\n                    }\n                }\n                return transposeMatrix(sortedData);\n            case 2: {\n                const uniques = [sortedData[0]];\n                for (let i = 1; i < sortedData.length; i++) {\n                    for (let j = 0; j < i; j++) {\n                        if (sameRows(i, j)) {\n                            break;\n                        }\n                        if (j === i - 1) {\n                            uniques.push(sortedData[i]);\n                        }\n                    }\n                }\n                return transposeMatrix(uniques.slice(0, _n));\n            }\n            case 3: {\n                const uniques = [sortedData[0]];\n                let counter = 1;\n                for (let i = 1; i < sortedData.length; i++) {\n                    if (!sameRows(i, i - 1)) {\n                        counter++;\n                    }\n                    if (counter > _n) {\n                        break;\n                    }\n                    uniques.push(sortedData[i]);\n                }\n                return transposeMatrix(uniques);\n            }\n        }\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// UNIQUE\n// -----------------------------------------------------------------------------\nconst UNIQUE = {\n    description: _t(\"Unique rows in the provided source range.\"),\n    args: [\n        arg(\"range (any, range<any>)\", _t(\"The data to filter by unique entries.\")),\n        arg(\"by_column (boolean, default=FALSE)\", _t(\"Whether to filter the data by columns or by rows.\")),\n        arg(\"exactly_once (boolean, default=FALSE)\", _t(\"Whether to return only entries with no duplicates.\")),\n    ],\n    compute: function (range = { value: \"\" }, byColumn, exactlyOnce) {\n        if (!isMatrix(range)) {\n            return [[range]];\n        }\n        const _byColumn = toBoolean(byColumn?.value) || false;\n        const _exactlyOnce = toBoolean(exactlyOnce?.value) || false;\n        if (!_byColumn) {\n            range = transposeMatrix(range);\n        }\n        const map = new Map();\n        for (const data of range) {\n            const key = JSON.stringify(data.map((item) => item.value));\n            const occurrence = map.get(key);\n            if (!occurrence) {\n                map.set(key, { data, count: 1 });\n            }\n            else {\n                occurrence.count++;\n            }\n        }\n        const result = [];\n        for (const row of map.values()) {\n            if (_exactlyOnce && row.count > 1) {\n                continue;\n            }\n            result.push(row.data);\n        }\n        if (!result.length)\n            throw new EvaluationError(_t(\"No unique values found\"));\n        return _byColumn ? result : transposeMatrix(result);\n    },\n    isExported: true,\n};\n\nvar filter = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    FILTER: FILTER,\n    SORT: SORT,\n    SORTN: SORTN,\n    UNIQUE: UNIQUE\n});\n\n/** Assert maturity date > settlement date */\nfunction assertMaturityAndSettlementDatesAreValid(settlement, maturity) {\n    assert(() => settlement < maturity, _t(\"The maturity (%s) must be strictly greater than the settlement (%s).\", maturity.toString(), settlement.toString()));\n}\n/** Assert settlement date > issue date */\nfunction assertSettlementAndIssueDatesAreValid(settlement, issue) {\n    assert(() => issue < settlement, _t(\"The settlement date (%s) must be strictly greater than the issue date (%s).\", settlement.toString(), issue.toString()));\n}\n/** Assert coupon frequency is in [1, 2, 4] */\nfunction assertCouponFrequencyIsValid(frequency) {\n    assert(() => [1, 2, 4].includes(frequency), _t(\"The frequency (%s) must be one of %s\", frequency.toString(), [1, 2, 4].toString()));\n}\n/** Assert dayCountConvention is between 0 and 4 */\nfunction assertDayCountConventionIsValid(dayCountConvention) {\n    assert(() => 0 <= dayCountConvention && dayCountConvention <= 4, _t(\"The day_count_convention (%s) must be between 0 and 4 inclusive.\", dayCountConvention.toString()));\n}\nfunction assertRedemptionStrictlyPositive(redemption) {\n    assert(() => redemption > 0, _t(\"The redemption (%s) must be strictly positive.\", redemption.toString()));\n}\nfunction assertPriceStrictlyPositive(price) {\n    assert(() => price > 0, _t(\"The price (%s) must be strictly positive.\", price.toString()));\n}\nfunction assertNumberOfPeriodsStrictlyPositive(nPeriods) {\n    assert(() => nPeriods > 0, _t(\"The number_of_periods (%s) must be greater than 0.\", nPeriods.toString()));\n}\nfunction assertRateStrictlyPositive(rate) {\n    assert(() => rate > 0, _t(\"The rate (%s) must be strictly positive.\", rate.toString()));\n}\nfunction assertLifeStrictlyPositive(life) {\n    assert(() => life > 0, _t(\"The life (%s) must be strictly positive.\", life.toString()));\n}\nfunction assertCostStrictlyPositive(cost) {\n    assert(() => cost > 0, _t(\"The cost (%s) must be strictly positive.\", cost.toString()));\n}\nfunction assertPurchaseDatePositiveOrZero(purchaseDate) {\n    assert(() => purchaseDate >= 0, _t(\"The purchase_date (%s) must be positive or null.\", purchaseDate.toString()));\n}\nfunction assertIssuePositiveOrZero(issue) {\n    assert(() => issue >= 0, _t(\"The issue (%s) must be positive or null.\", issue.toString()));\n}\nfunction assertCostPositiveOrZero(cost) {\n    assert(() => cost >= 0, _t(\"The cost (%s) must be positive or null.\", cost.toString()));\n}\nfunction assertPeriodStrictlyPositive(period) {\n    assert(() => period > 0, _t(\"The period (%s) must be strictly positive.\", period.toString()));\n}\nfunction assertPeriodPositiveOrZero(period) {\n    assert(() => period >= 0, _t(\"The period (%s) must be positive or null.\", period.toString()));\n}\nfunction assertSalvagePositiveOrZero(salvage) {\n    assert(() => salvage >= 0, _t(\"The salvage (%s) must be positive or null.\", salvage.toString()));\n}\nfunction assertSalvageSmallerOrEqualThanCost(salvage, cost) {\n    assert(() => salvage <= cost, _t(\"The salvage (%s) must be smaller or equal than the cost (%s).\", salvage.toString(), cost.toString()));\n}\nfunction assertPresentValueStrictlyPositive(pv) {\n    assert(() => pv > 0, _t(\"The present value (%s) must be strictly positive.\", pv.toString()));\n}\nfunction assertPeriodSmallerOrEqualToLife(period, life) {\n    assert(() => period <= life, _t(\"The period (%s) must be less than or equal life (%s).\", period.toString(), life.toString()));\n}\nfunction assertInvestmentStrictlyPositive(investment) {\n    assert(() => investment > 0, _t(\"The investment (%s) must be strictly positive.\", investment.toString()));\n}\nfunction assertDiscountStrictlyPositive(discount) {\n    assert(() => discount > 0, _t(\"The discount (%s) must be strictly positive.\", discount.toString()));\n}\nfunction assertDiscountStrictlySmallerThanOne(discount) {\n    assert(() => discount < 1, _t(\"The discount (%s) must be smaller than 1.\", discount.toString()));\n}\nfunction assertDeprecationFactorStrictlyPositive(factor) {\n    assert(() => factor > 0, _t(\"The depreciation factor (%s) must be strictly positive.\", factor.toString()));\n}\nfunction assertSettlementLessThanOneYearBeforeMaturity(settlement, maturity, locale) {\n    const startDate = toJsDate(settlement, locale);\n    const endDate = toJsDate(maturity, locale);\n    const startDatePlusOneYear = toJsDate(settlement, locale);\n    startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1);\n    assert(() => endDate.getTime() <= startDatePlusOneYear.getTime(), _t(\"The settlement date (%s) must at most one year after the maturity date (%s).\", settlement.toString(), maturity.toString()));\n}\n/**\n * Check if the given periods are valid. This will assert :\n *\n * - 0 < numberOfPeriods\n * - 0 < firstPeriod <= lastPeriod\n * - 0 < lastPeriod <= numberOfPeriods\n *\n */\nfunction assertFirstAndLastPeriodsAreValid(firstPeriod, lastPeriod, numberOfPeriods) {\n    assertNumberOfPeriodsStrictlyPositive(numberOfPeriods);\n    assert(() => firstPeriod > 0, _t(\"The first_period (%s) must be strictly positive.\", firstPeriod.toString()));\n    assert(() => lastPeriod > 0, _t(\"The last_period (%s) must be strictly positive.\", lastPeriod.toString()));\n    assert(() => firstPeriod <= lastPeriod, _t(\"The first_period (%s) must be smaller or equal to the last_period (%s).\", firstPeriod.toString(), lastPeriod.toString()));\n    assert(() => lastPeriod <= numberOfPeriods, _t(\"The last_period (%s) must be smaller or equal to the number_of_periods (%s).\", firstPeriod.toString(), numberOfPeriods.toString()));\n}\n/**\n * Check if the given periods are valid. This will assert :\n *\n * - 0 < life\n * - 0 <= startPeriod <= endPeriod\n * - 0 <= endPeriod <= life\n *\n */\nfunction assertStartAndEndPeriodAreValid(startPeriod, endPeriod, life) {\n    assertLifeStrictlyPositive(life);\n    assert(() => startPeriod >= 0, _t(\"The start_period (%s) must be greater or equal than 0.\", startPeriod.toString()));\n    assert(() => endPeriod >= 0, _t(\"The end_period (%s) must be greater or equal than 0.\", endPeriod.toString()));\n    assert(() => startPeriod <= endPeriod, _t(\"The start_period (%s) must be smaller or equal to the end_period (%s).\", startPeriod.toString(), endPeriod.toString()));\n    assert(() => endPeriod <= life, _t(\"The end_period (%s) must be smaller or equal to the life (%s).\", startPeriod.toString(), life.toString()));\n}\nfunction assertRateGuessStrictlyGreaterThanMinusOne(guess) {\n    assert(() => guess > -1, _t(\"The rate_guess (%s) must be strictly greater than -1.\", guess.toString()));\n}\nfunction assertCashFlowsAndDatesHaveSameDimension(cashFlows, dates) {\n    assert(() => cashFlows.length === dates.length && cashFlows[0].length === dates[0].length, _t(\"The cashflow_amounts and cashflow_dates ranges must have the same dimensions.\"));\n}\nfunction assertCashFlowsHavePositiveAndNegativesValues(cashFlow) {\n    assert(() => cashFlow.some((val) => val > 0) && cashFlow.some((val) => val < 0), _t(\"There must be both positive and negative values in cashflow_amounts.\"));\n}\nfunction assertEveryDateGreaterThanFirstDateOfCashFlowDates(dates) {\n    assert(() => dates.every((date) => date >= dates[0]), _t(\"All the dates should be greater or equal to the first date in cashflow_dates (%s).\", dates[0].toString()));\n}\n\nconst DEFAULT_DAY_COUNT_CONVENTION = 0;\nconst DEFAULT_END_OR_BEGINNING = 0;\nconst DEFAULT_FUTURE_VALUE = 0;\nconst COUPON_FUNCTION_ARGS = [\n    arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n    arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n    arg(\"frequency (number)\", _t(\"The number of interest or coupon payments per year (1, 2, or 4).\")),\n    arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n];\n/**\n * Use the Newton\u2013Raphson method to find a root of the given function in an iterative manner.\n *\n * @param func the function to find a root of\n * @param derivFunc the derivative of the function\n * @param startValue the initial value for the first iteration of the algorithm\n * @param maxIterations the maximum number of iterations\n * @param epsMax the epsilon for the root\n * @param nanFallback a function giving a fallback value to use if func(x) returns NaN. Useful if the\n *                       function is not defined for some range, but we know approximately where the root is when the Newton\n *                       algorithm ends up in this range.\n */\nfunction newtonMethod(func, derivFunc, startValue, maxIterations, epsMax = 1e-10, nanFallback) {\n    let x = startValue;\n    let newX;\n    let xDelta;\n    let y;\n    let yEqual0 = false;\n    let count = 0;\n    let previousFallback = undefined;\n    do {\n        y = func(x);\n        if (isNaN(y)) {\n            assert(() => count < maxIterations && nanFallback !== undefined, _t(\"Function [[FUNCTION_NAME]] didn't find any result.\"));\n            count++;\n            x = nanFallback(previousFallback);\n            previousFallback = x;\n            continue;\n        }\n        newX = x - y / derivFunc(x);\n        xDelta = Math.abs(newX - x);\n        x = newX;\n        yEqual0 = xDelta < epsMax || Math.abs(y) < epsMax;\n        assert(() => count < maxIterations, _t(\"Function [[FUNCTION_NAME]] didn't find any result.\"));\n        count++;\n    } while (!yEqual0);\n    return x;\n}\n// -----------------------------------------------------------------------------\n// ACCRINTM\n// -----------------------------------------------------------------------------\nconst ACCRINTM = {\n    description: _t(\"Accrued interest of security paying at maturity.\"),\n    args: [\n        arg(\"issue (date)\", _t(\"The date the security was initially issued.\")),\n        arg(\"maturity (date)\", _t(\"The maturity date of the security.\")),\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"redemption (number)\", _t(\"The redemption amount per 100 face value, or par.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (issue, maturity, rate, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        const start = Math.trunc(toNumber(issue, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const _redemption = toNumber(redemption, this.locale);\n        const _rate = toNumber(rate, this.locale);\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertIssuePositiveOrZero(start);\n        assertSettlementAndIssueDatesAreValid(end, start);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assertRedemptionStrictlyPositive(_redemption);\n        assertRateStrictlyPositive(_rate);\n        const yearFrac = getYearFrac(start, end, _dayCountConvention);\n        return _redemption * _rate * yearFrac;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// AMORLINC\n// -----------------------------------------------------------------------------\nconst AMORLINC = {\n    description: _t(\"Depreciation for an accounting period.\"),\n    args: [\n        arg(\"cost (number)\", _t(\"The initial cost of the asset.\")),\n        arg(\"purchase_date (date)\", _t(\"The date the asset was purchased.\")),\n        arg(\"first_period_end (date)\", _t(\"The date the first period ended.\")),\n        arg(\"salvage (number)\", _t(\"The value of the asset at the end of depreciation.\")),\n        arg(\"period (number)\", _t(\"The single period within life for which to calculate depreciation.\")),\n        arg(\"rate (number)\", _t(\"The deprecation rate.\")),\n        arg(\"day_count_convention (number, optional)\", _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (cost, purchaseDate, firstPeriodEnd, salvage, period, rate, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _cost = toNumber(cost, this.locale);\n        const _purchaseDate = Math.trunc(toNumber(purchaseDate, this.locale));\n        const _firstPeriodEnd = Math.trunc(toNumber(firstPeriodEnd, this.locale));\n        const _salvage = toNumber(salvage, this.locale);\n        const _period = toNumber(period, this.locale);\n        const _rate = toNumber(rate, this.locale);\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertCostStrictlyPositive(_cost);\n        assertPurchaseDatePositiveOrZero(_purchaseDate);\n        assertSalvagePositiveOrZero(_salvage);\n        assertSalvageSmallerOrEqualThanCost(_salvage, _cost);\n        assertPeriodPositiveOrZero(_period);\n        assertRateStrictlyPositive(_rate);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assert(() => _purchaseDate <= _firstPeriodEnd, _t(\"The purchase_date (%s) must be before the first_period_end (%s).\", _purchaseDate.toString(), _firstPeriodEnd.toString()));\n        /**\n         * https://wiki.documentfoundation.org/Documentation/Calc_Functions/AMORLINC\n         *\n         * AMORLINC period 0 = cost * rate * YEARFRAC(purchase date, first period end)\n         * AMORLINC period n = cost * rate\n         * AMORLINC at the last period is such that the remaining deprecated cost is equal to the salvage value.\n         *\n         * The period is and rounded to 1 if < 1 truncated if > 1,\n         *\n         * Compatibility note :\n         * If (purchase date) === (first period end), on GSheet the deprecation at the first period is 0, and on Excel\n         * it is a full period deprecation. We choose to use the Excel behaviour.\n         */\n        const roundedPeriod = _period < 1 && _period > 0 ? 1 : Math.trunc(_period);\n        const deprec = _cost * _rate;\n        const yearFrac = getYearFrac(_purchaseDate, _firstPeriodEnd, _dayCountConvention);\n        const firstDeprec = _purchaseDate === _firstPeriodEnd ? deprec : deprec * yearFrac;\n        const valueAtPeriod = _cost - firstDeprec - deprec * roundedPeriod;\n        if (valueAtPeriod >= _salvage) {\n            return roundedPeriod === 0 ? firstDeprec : deprec;\n        }\n        return _salvage - valueAtPeriod < deprec ? deprec - (_salvage - valueAtPeriod) : 0;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUPDAYS\n// -----------------------------------------------------------------------------\nconst COUPDAYS = {\n    description: _t(\"Days in coupon period containing settlement date.\"),\n    args: COUPON_FUNCTION_ARGS,\n    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        // https://wiki.documentfoundation.org/Documentation/Calc_Functions/COUPDAYS\n        if (_dayCountConvention === 1) {\n            const before = COUPPCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention).value;\n            const after = COUPNCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention).value;\n            return after - before;\n        }\n        const daysInYear = _dayCountConvention === 3 ? 365 : 360;\n        return daysInYear / _frequency;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUPDAYBS\n// -----------------------------------------------------------------------------\nconst COUPDAYBS = {\n    description: _t(\"Days from settlement until next coupon.\"),\n    args: COUPON_FUNCTION_ARGS,\n    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        const couponBeforeStart = COUPPCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention).value;\n        if ([1, 2, 3].includes(_dayCountConvention)) {\n            return start - couponBeforeStart;\n        }\n        if (_dayCountConvention === 4) {\n            const yearFrac = getYearFrac(couponBeforeStart, start, _dayCountConvention);\n            return Math.round(yearFrac * 360);\n        }\n        const startDate = toJsDate(start, this.locale);\n        const dateCouponBeforeStart = toJsDate(couponBeforeStart, this.locale);\n        const y1 = dateCouponBeforeStart.getFullYear();\n        const y2 = startDate.getFullYear();\n        const m1 = dateCouponBeforeStart.getMonth() + 1; // +1 because months in js start at 0 and it's confusing\n        const m2 = startDate.getMonth() + 1;\n        let d1 = dateCouponBeforeStart.getDate();\n        let d2 = startDate.getDate();\n        /**\n         * Rules based on https://en.wikipedia.org/wiki/Day_count_convention#30/360_US\n         *\n         * These are slightly modified (no mention of if investment is EOM and rules order is modified),\n         * but from my testing this seems the rules used by Excel/GSheet.\n         */\n        if (m1 === 2 &&\n            m2 === 2 &&\n            isLastDayOfMonth(dateCouponBeforeStart) &&\n            isLastDayOfMonth(startDate)) {\n            d2 = 30;\n        }\n        if (d2 === 31 && (d1 === 30 || d1 === 31)) {\n            d2 = 30;\n        }\n        if (m1 === 2 && isLastDayOfMonth(dateCouponBeforeStart)) {\n            d1 = 30;\n        }\n        if (d1 === 31) {\n            d1 = 30;\n        }\n        return (y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUPDAYSNC\n// -----------------------------------------------------------------------------\nconst COUPDAYSNC = {\n    description: _t(\"Days from settlement until next coupon.\"),\n    args: COUPON_FUNCTION_ARGS,\n    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        const couponAfterStart = COUPNCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention).value;\n        if ([1, 2, 3].includes(_dayCountConvention)) {\n            return couponAfterStart - start;\n        }\n        if (_dayCountConvention === 4) {\n            const yearFrac = getYearFrac(start, couponAfterStart, _dayCountConvention);\n            return Math.round(yearFrac * 360);\n        }\n        const coupDayBs = COUPDAYBS.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);\n        const coupDays = COUPDAYS.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);\n        return coupDays - coupDayBs;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUPNCD\n// -----------------------------------------------------------------------------\nconst COUPNCD = {\n    description: _t(\"Next coupon date after the settlement date.\"),\n    args: COUPON_FUNCTION_ARGS,\n    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        const monthsPerPeriod = 12 / _frequency;\n        const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);\n        const date = addMonthsToDate(toJsDate(end, this.locale), -(coupNum - 1) * monthsPerPeriod, true);\n        return {\n            value: jsDateToRoundNumber(date),\n            format: this.locale.dateFormat,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUPNUM\n// -----------------------------------------------------------------------------\nconst COUPNUM = {\n    description: _t(\"Number of coupons between settlement and maturity.\"),\n    args: COUPON_FUNCTION_ARGS,\n    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        let num = 1;\n        let currentDate = end;\n        const monthsPerPeriod = 12 / _frequency;\n        while (currentDate > start) {\n            currentDate = jsDateToRoundNumber(addMonthsToDate(toJsDate(currentDate, this.locale), -monthsPerPeriod, false));\n            num++;\n        }\n        return num - 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COUPPCD\n// -----------------------------------------------------------------------------\nconst COUPPCD = {\n    description: _t(\"Last coupon date prior to or on the settlement date.\"),\n    args: COUPON_FUNCTION_ARGS,\n    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        const monthsPerPeriod = 12 / _frequency;\n        const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);\n        const date = addMonthsToDate(toJsDate(end, this.locale), -coupNum * monthsPerPeriod, true);\n        return {\n            value: jsDateToRoundNumber(date),\n            format: this.locale.dateFormat,\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CUMIPMT\n// -----------------------------------------------------------------------------\nconst CUMIPMT = {\n    description: _t(\"Cumulative interest paid over a set of periods.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The interest rate.\")),\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"present_value (number)\", _t(\"The current value of the annuity.\")),\n        arg(\"first_period (number)\", _t(\"The number of the payment period to begin the cumulative calculation.\")),\n        arg(\"last_period (number)\", _t(\"The number of the payment period to end the cumulative calculation.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n    ],\n    compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {\n        const first = toNumber(firstPeriod, this.locale);\n        const last = toNumber(lastPeriod, this.locale);\n        const r = toNumber(rate, this.locale);\n        const pv = toNumber(presentValue, this.locale);\n        const n = toNumber(numberOfPeriods, this.locale);\n        const type = toBoolean(endOrBeginning) ? 1 : 0;\n        assertFirstAndLastPeriodsAreValid(first, last, n);\n        assertRateStrictlyPositive(r);\n        assertPresentValueStrictlyPositive(pv);\n        let cumSum = 0;\n        for (let i = first; i <= last; i++) {\n            cumSum += impt(r, i, n, pv, 0, type);\n        }\n        return cumSum;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CUMPRINC\n// -----------------------------------------------------------------------------\nconst CUMPRINC = {\n    description: _t(\"Cumulative principal paid over a set of periods.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The interest rate.\")),\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"present_value (number)\", _t(\"The current value of the annuity.\")),\n        arg(\"first_period (number)\", _t(\"The number of the payment period to begin the cumulative calculation.\")),\n        arg(\"last_period (number)\", _t(\"The number of the payment period to end the cumulative calculation.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n    ],\n    compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {\n        const first = toNumber(firstPeriod, this.locale);\n        const last = toNumber(lastPeriod, this.locale);\n        const r = toNumber(rate, this.locale);\n        const pv = toNumber(presentValue, this.locale);\n        const n = toNumber(numberOfPeriods, this.locale);\n        const type = toBoolean(endOrBeginning) ? 1 : 0;\n        assertFirstAndLastPeriodsAreValid(first, last, n);\n        assertRateStrictlyPositive(r);\n        assertPresentValueStrictlyPositive(pv);\n        let cumSum = 0;\n        for (let i = first; i <= last; i++) {\n            cumSum += ppmt(r, i, n, pv, 0, type);\n        }\n        return cumSum;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DB\n// -----------------------------------------------------------------------------\nconst DB = {\n    description: _t(\"Depreciation via declining balance method.\"),\n    args: [\n        arg(\"cost (number)\", _t(\"The initial cost of the asset.\")),\n        arg(\"salvage (number)\", _t(\"The value of the asset at the end of depreciation.\")),\n        arg(\"life (number)\", _t(\"The number of periods over which the asset is depreciated.\")),\n        arg(\"period (number)\", _t(\"The single period within life for which to calculate depreciation.\")),\n        arg(\"month (number, optional)\", _t(\"The number of months in the first year of depreciation.\")),\n    ],\n    // to do: replace by dollar format\n    compute: function (cost, salvage, life, period, ...args) {\n        const _cost = toNumber(cost, this.locale);\n        const _salvage = toNumber(salvage, this.locale);\n        const _life = toNumber(life, this.locale);\n        const _period = Math.trunc(toNumber(period, this.locale));\n        const _month = args.length ? Math.trunc(toNumber(args[0], this.locale)) : 12;\n        const lifeLimit = _life + (_month === 12 ? 0 : 1);\n        assertCostPositiveOrZero(_cost);\n        assertSalvagePositiveOrZero(_salvage);\n        assertPeriodStrictlyPositive(_period);\n        assertLifeStrictlyPositive(_life);\n        assert(() => 1 <= _month && _month <= 12, _t(\"The month (%s) must be between 1 and 12 inclusive.\", _month.toString()));\n        assert(() => _period <= lifeLimit, _t(\"The period (%s) must be less than or equal to %s.\", _period.toString(), lifeLimit.toString()));\n        const monthPart = _month / 12;\n        let rate = 1 - Math.pow(_salvage / _cost, 1 / _life);\n        // round to 3 decimal places\n        rate = Math.round(rate * 1000) / 1000;\n        let before = _cost;\n        let after = _cost * (1 - rate * monthPart);\n        for (let i = 1; i < _period; i++) {\n            before = after;\n            after = before * (1 - rate);\n            if (i === _life) {\n                after = before * (1 - rate * (1 - monthPart));\n            }\n        }\n        return {\n            value: before - after,\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DDB\n// -----------------------------------------------------------------------------\nconst DEFAULT_DDB_DEPRECIATION_FACTOR = 2;\nfunction ddb(cost, salvage, life, period, factor) {\n    assertCostPositiveOrZero(cost);\n    assertSalvagePositiveOrZero(salvage);\n    assertPeriodStrictlyPositive(period);\n    assertLifeStrictlyPositive(life);\n    assertPeriodSmallerOrEqualToLife(period, life);\n    assertDeprecationFactorStrictlyPositive(factor);\n    if (cost === 0 || salvage >= cost)\n        return 0;\n    const deprecFactor = factor / life;\n    if (deprecFactor > 1) {\n        return period === 1 ? cost - salvage : 0;\n    }\n    if (period <= 1) {\n        return cost * deprecFactor;\n    }\n    const previousCost = cost * Math.pow(1 - deprecFactor, period - 1);\n    const nextCost = cost * Math.pow(1 - deprecFactor, period);\n    const deprec = nextCost < salvage ? previousCost - salvage : previousCost - nextCost;\n    return Math.max(deprec, 0);\n}\nconst DDB = {\n    description: _t(\"Depreciation via double-declining balance method.\"),\n    args: [\n        arg(\"cost (number)\", _t(\"The initial cost of the asset.\")),\n        arg(\"salvage (number)\", _t(\"The value of the asset at the end of depreciation.\")),\n        arg(\"life (number)\", _t(\"The number of periods over which the asset is depreciated.\")),\n        arg(\"period (number)\", _t(\"The single period within life for which to calculate depreciation.\")),\n        arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t(\"The factor by which depreciation decreases.\")),\n    ],\n    compute: function (cost, salvage, life, period, factor = { value: DEFAULT_DDB_DEPRECIATION_FACTOR }) {\n        const _cost = toNumber(cost, this.locale);\n        const _salvage = toNumber(salvage, this.locale);\n        const _life = toNumber(life, this.locale);\n        const _period = toNumber(period, this.locale);\n        const _factor = toNumber(factor, this.locale);\n        return {\n            value: ddb(_cost, _salvage, _life, _period, _factor),\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DISC\n// -----------------------------------------------------------------------------\nconst DISC = {\n    description: _t(\"Discount rate of a security based on price.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"price (number)\", _t(\"The price at which the security is bought per 100 face value.\")),\n        arg(\"redemption (number)\", _t(\"The redemption amount per 100 face value, or par.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, price, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _price = toNumber(price, this.locale);\n        const _redemption = toNumber(redemption, this.locale);\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assertPriceStrictlyPositive(_price);\n        assertRedemptionStrictlyPositive(_redemption);\n        /**\n         * https://support.microsoft.com/en-us/office/disc-function-71fce9f3-3f05-4acf-a5a3-eac6ef4daa53\n         *\n         * B = number of days in year, depending on year basis\n         * DSM = number of days from settlement to maturity\n         *\n         *        redemption - price          B\n         * DISC = ____________________  *    ____\n         *            redemption             DSM\n         */\n        const yearsFrac = getYearFrac(_settlement, _maturity, _dayCountConvention);\n        return (_redemption - _price) / _redemption / yearsFrac;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DOLLARDE\n// -----------------------------------------------------------------------------\nconst DOLLARDE = {\n    description: _t(\"Convert a decimal fraction to decimal value.\"),\n    args: [\n        arg(\"fractional_price (number)\", _t(\"The price quotation given using fractional decimal conventions.\")),\n        arg(\"unit (number)\", _t(\"The units of the fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.\")),\n    ],\n    compute: function (fractionalPrice, unit) {\n        const price = toNumber(fractionalPrice, this.locale);\n        const _unit = Math.trunc(toNumber(unit, this.locale));\n        assert(() => _unit > 0, _t(\"The unit (%s) must be strictly positive.\", _unit.toString()));\n        const truncatedPrice = Math.trunc(price);\n        const priceFractionalPart = price - truncatedPrice;\n        const frac = 10 ** Math.ceil(Math.log10(_unit)) / _unit;\n        return truncatedPrice + priceFractionalPart * frac;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DOLLARFR\n// -----------------------------------------------------------------------------\nconst DOLLARFR = {\n    description: _t(\"Convert a decimal value to decimal fraction.\"),\n    args: [\n        arg(\"decimal_price (number)\", _t(\"The price quotation given as a decimal value.\")),\n        arg(\"unit (number)\", _t(\"The units of the desired fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.\")),\n    ],\n    compute: function (decimalPrice, unit) {\n        const price = toNumber(decimalPrice, this.locale);\n        const _unit = Math.trunc(toNumber(unit, this.locale));\n        assert(() => _unit > 0, _t(\"The unit (%s) must be strictly positive.\", _unit.toString()));\n        const truncatedPrice = Math.trunc(price);\n        const priceFractionalPart = price - truncatedPrice;\n        const frac = _unit / 10 ** Math.ceil(Math.log10(_unit));\n        return truncatedPrice + priceFractionalPart * frac;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DURATION\n// -----------------------------------------------------------------------------\nconst DURATION = {\n    description: _t(\"Number of periods for an investment to reach a value.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"yield (number)\", _t(\"The expected annual yield of the security.\")),\n        arg(\"frequency (number)\", _t(\"The number of interest or coupon payments per year (1, 2, or 4).\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const _rate = toNumber(rate, this.locale);\n        const _yield = toNumber(securityYield, this.locale);\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assert(() => _rate >= 0, _t(\"The rate (%s) must be positive or null.\", _rate.toString()));\n        assert(() => _yield >= 0, _t(\"The yield (%s) must be positive or null.\", _yield.toString()));\n        const years = getYearFrac(start, end, _dayCountConvention);\n        const timeFirstYear = years - Math.trunc(years) || 1 / _frequency;\n        const nbrCoupons = Math.ceil(years * _frequency);\n        // The DURATION function return the Macaulay duration\n        // See example: https://en.wikipedia.org/wiki/Bond_duration#Formulas\n        const cashFlowFromCoupon = _rate / _frequency;\n        const yieldPerPeriod = _yield / _frequency;\n        let count = 0;\n        let sum = 0;\n        for (let i = 1; i <= nbrCoupons; i++) {\n            const cashFlowPerPeriod = cashFlowFromCoupon + (i === nbrCoupons ? 1 : 0);\n            const presentValuePerPeriod = cashFlowPerPeriod / (1 + yieldPerPeriod) ** i;\n            sum += (timeFirstYear + (i - 1) / _frequency) * presentValuePerPeriod;\n            count += presentValuePerPeriod;\n        }\n        return count === 0 ? 0 : sum / count;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// EFFECT\n// -----------------------------------------------------------------------------\nconst EFFECT = {\n    description: _t(\"Annual effective interest rate.\"),\n    args: [\n        arg(\"nominal_rate (number)\", _t(\"The nominal interest rate per year.\")),\n        arg(\"periods_per_year (number)\", _t(\"The number of compounding periods per year.\")),\n    ],\n    compute: function (nominal_rate, periods_per_year) {\n        const nominal = toNumber(nominal_rate, this.locale);\n        const periods = Math.trunc(toNumber(periods_per_year, this.locale));\n        assert(() => nominal > 0, _t(\"The nominal rate (%s) must be strictly greater than 0.\", nominal.toString()));\n        assert(() => periods > 0, _t(\"The number of periods by year (%s) must strictly greater than 0.\", periods.toString()));\n        // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate\n        return Math.pow(1 + nominal / periods, periods) - 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FV\n// -----------------------------------------------------------------------------\nconst DEFAULT_PRESENT_VALUE = 0;\nfunction fv(r, n, p, pv, t) {\n    if (r === 0) {\n        return -(pv + p * n);\n    }\n    return -pv * (1 + r) ** n - (p * (1 + r * t) * ((1 + r) ** n - 1)) / r;\n}\nconst FV = {\n    description: _t(\"Future value of an annuity investment.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The interest rate.\")),\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"payment_amount (number)\", _t(\"The amount per period to be paid.\")),\n        arg(`present_value (number, default=${DEFAULT_PRESENT_VALUE})`, _t(\"The current value of the annuity.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n    ],\n    // to do: replace by dollar format\n    compute: function (rate, numberOfPeriods, paymentAmount, presentValue = { value: DEFAULT_PRESENT_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {\n        presentValue = presentValue || 0;\n        endOrBeginning = endOrBeginning || 0;\n        const r = toNumber(rate, this.locale);\n        const n = toNumber(numberOfPeriods, this.locale);\n        const p = toNumber(paymentAmount, this.locale);\n        const pv = toNumber(presentValue, this.locale);\n        const type = toBoolean(endOrBeginning) ? 1 : 0;\n        return {\n            value: fv(r, n, p, pv, type),\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FVSCHEDULE\n// -----------------------------------------------------------------------------\nconst FVSCHEDULE = {\n    description: _t(\"Future value of principal from series of rates.\"),\n    args: [\n        arg(\"principal (number)\", _t(\"The amount of initial capital or value to compound against.\")),\n        arg(\"rate_schedule (number, range<number>)\", _t(\"A series of interest rates to compound against the principal.\")),\n    ],\n    compute: function (principalAmount, rateSchedule) {\n        const principal = toNumber(principalAmount, this.locale);\n        return reduceAny([rateSchedule], (acc, rate) => acc * (1 + toNumber(rate, this.locale)), principal);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// INTRATE\n// -----------------------------------------------------------------------------\nconst INTRATE = {\n    description: _t(\"Calculates effective interest rate.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"investment (number)\", _t(\"The amount invested in the security.\")),\n        arg(\"redemption (number)\", _t(\"The amount to be received at maturity.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, investment, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _redemption = toNumber(redemption, this.locale);\n        const _investment = toNumber(investment, this.locale);\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertInvestmentStrictlyPositive(_investment);\n        assertRedemptionStrictlyPositive(_redemption);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        /**\n         * https://wiki.documentfoundation.org/Documentation/Calc_Functions/INTRATE\n         *\n         *             (Redemption  - Investment) / Investment\n         * INTRATE =  _________________________________________\n         *              YEARFRAC(settlement, maturity, basis)\n         */\n        const yearFrac = getYearFrac(_settlement, _maturity, _dayCountConvention);\n        return (_redemption - _investment) / _investment / yearFrac;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// IPMT\n// -----------------------------------------------------------------------------\nfunction impt(r, per, n, pv, fv, type) {\n    return pmt(r, n, pv, fv, type) - ppmt(r, per, n, pv, fv, type);\n}\nconst IPMT = {\n    description: _t(\"Payment on the principal of an investment.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"period (number)\", _t(\"The amortization period, in terms of number of periods.\")),\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"present_value (number)\", _t(\"The current value of the annuity.\")),\n        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t(\"The future value remaining after the final payment has been made.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n    ],\n    compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {\n        const r = toNumber(rate, this.locale);\n        const period = toNumber(currentPeriod, this.locale);\n        const n = toNumber(numberOfPeriods, this.locale);\n        const pv = toNumber(presentValue, this.locale);\n        const fv = toNumber(futureValue, this.locale);\n        const type = toBoolean(endOrBeginning) ? 1 : 0;\n        return {\n            value: impt(r, period, n, pv, fv, type),\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// IRR\n// -----------------------------------------------------------------------------\nconst DEFAULT_RATE_GUESS = 0.1;\nconst IRR = {\n    description: _t(\"Internal rate of return given periodic cashflows.\"),\n    args: [\n        arg(\"cashflow_amounts (number, range<number>)\", _t(\"An array or range containing the income or payments associated with the investment.\")),\n        arg(`rate_guess (number, default=${DEFAULT_RATE_GUESS})`, _t(\"An estimate for what the internal rate of return will be.\")),\n    ],\n    compute: function (cashFlowAmounts, rateGuess = { value: DEFAULT_RATE_GUESS }) {\n        const _rateGuess = toNumber(rateGuess, this.locale);\n        assertRateGuessStrictlyGreaterThanMinusOne(_rateGuess);\n        // check that values contains at least one positive value and one negative value\n        // and extract number present in the cashFlowAmount argument\n        let positive = false;\n        let negative = false;\n        let amounts = [];\n        visitNumbers([cashFlowAmounts], ({ value: amount }) => {\n            if (amount > 0)\n                positive = true;\n            if (amount < 0)\n                negative = true;\n            amounts.push(amount);\n        }, this.locale);\n        assert(() => positive && negative, _t(\"The cashflow_amounts must include negative and positive values.\"));\n        const firstAmount = amounts.shift();\n        // The result of IRR is the rate at which the NPV() function will return zero with the given values.\n        // This algorithm uses the Newton's method on the NPV function to determine the result\n        // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method\n        // As the NPV function isn't continuous, we apply the Newton's method on the numerator of the NPV formula.\n        function npvNumerator(rate, startValue, values) {\n            const nbrValue = values.length;\n            let i = 0;\n            return values.reduce((acc, v) => {\n                i++;\n                return acc + v * rate ** (nbrValue - i);\n            }, startValue * rate ** nbrValue);\n        }\n        function npvNumeratorDeriv(rate, startValue, values) {\n            const nbrValue = values.length;\n            let i = 0;\n            return values.reduce((acc, v) => {\n                i++;\n                return acc + v * (nbrValue - i) * rate ** (nbrValue - i - 1);\n            }, startValue * nbrValue * rate ** (nbrValue - 1));\n        }\n        function func(x) {\n            return npvNumerator(x, firstAmount, amounts);\n        }\n        function derivFunc(x) {\n            return npvNumeratorDeriv(x, firstAmount, amounts);\n        }\n        return {\n            value: newtonMethod(func, derivFunc, _rateGuess + 1, 20, 1e-5) - 1,\n            format: \"0%\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISPMT\n// -----------------------------------------------------------------------------\nconst ISPMT = {\n    description: _t(\"Returns the interest paid at a particular period of an investment.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The interest rate.\")),\n        arg(\"period (number)\", _t(\"The period for which you want to view the interest payment.\")),\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"present_value (number)\", _t(\"The current value of the annuity.\")),\n    ],\n    compute: function (rate, currentPeriod, numberOfPeriods, presentValue) {\n        const interestRate = toNumber(rate, this.locale);\n        const period = toNumber(currentPeriod, this.locale);\n        const nOfPeriods = toNumber(numberOfPeriods, this.locale);\n        const investment = toNumber(presentValue, this.locale);\n        assert(() => nOfPeriods !== 0, _t(\"The number of periods must be different than 0.\", nOfPeriods.toString()));\n        const currentInvestment = investment - investment * (period / nOfPeriods);\n        return -1 * currentInvestment * interestRate;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MDURATION\n// -----------------------------------------------------------------------------\nconst MDURATION = {\n    description: _t(\"Modified Macaulay duration.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"yield (number)\", _t(\"The expected annual yield of the security.\")),\n        arg(\"frequency (number)\", _t(\"The number of interest or coupon payments per year (1, 2, or 4).\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        const duration = DURATION.compute.bind(this)(settlement, maturity, rate, securityYield, frequency, dayCountConvention);\n        const y = toNumber(securityYield, this.locale);\n        const k = Math.trunc(toNumber(frequency, this.locale));\n        return duration / (1 + y / k);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MIRR\n// -----------------------------------------------------------------------------\nconst MIRR = {\n    description: _t(\"Modified internal rate of return.\"),\n    args: [\n        arg(\"cashflow_amounts (range<number>)\", _t(\"A range containing the income or payments associated with the investment. The array should contain bot payments and incomes.\")),\n        arg(\"financing_rate (number)\", _t(\"The interest rate paid on funds invested.\")),\n        arg(\"reinvestment_return_rate (number)\", _t(\"The return (as a percentage) earned on reinvestment of income received from the investment.\")),\n    ],\n    compute: function (cashflowAmount, financingRate, reinvestmentRate) {\n        const fRate = toNumber(financingRate, this.locale);\n        const rRate = toNumber(reinvestmentRate, this.locale);\n        const cashFlow = transposeMatrix(cashflowAmount)\n            .flat()\n            .filter((t) => t.value !== null)\n            .map((val) => toNumber(val, this.locale));\n        const n = cashFlow.length;\n        /**\n         * https://en.wikipedia.org/wiki/Modified_internal_rate_of_return\n         *\n         *         /  FV(positive cash flows, reinvestment rate) \\  ^ (1 / (n - 1))\n         * MIRR = |  ___________________________________________  |                 - 1\n         *         \\   - PV(negative cash flows, finance rate)   /\n         *\n         * with n the number of cash flows.\n         *\n         * You can compute FV and PV as :\n         *\n         * FV =    SUM      [ (cashFlow[i]>0 ? cashFlow[i] : 0) * (1 + rRate)**(n - i-1) ]\n         *       i= 0 => n\n         *\n         * PV =    SUM      [ (cashFlow[i]<0 ? cashFlow[i] : 0) / (1 + fRate)**i ]\n         *       i= 0 => n\n         */\n        let fv = 0;\n        let pv = 0;\n        for (const i of range(0, n)) {\n            const amount = cashFlow[i];\n            if (amount >= 0) {\n                fv += amount * (rRate + 1) ** (n - i - 1);\n            }\n            else {\n                pv += amount / (fRate + 1) ** i;\n            }\n        }\n        assert(() => pv !== 0 && fv !== 0, _t(\"There must be both positive and negative values in cashflow_amounts.\"));\n        const exponent = 1 / (n - 1);\n        return (-fv / pv) ** exponent - 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// NOMINAL\n// -----------------------------------------------------------------------------\nconst NOMINAL = {\n    description: _t(\"Annual nominal interest rate.\"),\n    args: [\n        arg(\"effective_rate (number)\", _t(\"The effective interest rate per year.\")),\n        arg(\"periods_per_year (number)\", _t(\"The number of compounding periods per year.\")),\n    ],\n    compute: function (effective_rate, periods_per_year) {\n        const effective = toNumber(effective_rate, this.locale);\n        const periods = Math.trunc(toNumber(periods_per_year, this.locale));\n        assert(() => effective > 0, _t(\"The effective rate (%s) must must strictly greater than 0.\", effective.toString()));\n        assert(() => periods > 0, _t(\"The number of periods by year (%s) must strictly greater than 0.\", periods.toString()));\n        // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate\n        return (Math.pow(effective + 1, 1 / periods) - 1) * periods;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// NPER\n// -----------------------------------------------------------------------------\nconst NPER = {\n    description: _t(\"Number of payment periods for an investment.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The interest rate.\")),\n        arg(\"payment_amount (number)\", _t(\"The amount of each payment made.\")),\n        arg(\"present_value (number)\", _t(\"The current value of the annuity.\")),\n        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t(\"The future value remaining after the final payment has been made.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n    ],\n    compute: function (rate, paymentAmount, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {\n        futureValue = futureValue || 0;\n        endOrBeginning = endOrBeginning || 0;\n        const r = toNumber(rate, this.locale);\n        const p = toNumber(paymentAmount, this.locale);\n        const pv = toNumber(presentValue, this.locale);\n        const fv = toNumber(futureValue, this.locale);\n        const t = toBoolean(endOrBeginning) ? 1 : 0;\n        /**\n         * https://wiki.documentfoundation.org/Documentation/Calc_Functions/NPER\n         *\n         * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r\n         *\n         * We solve the equation for N:\n         *\n         * with C = [ p * (1 + r * t)] / r and\n         *      R = 1 + r\n         *\n         * => 0 = pv * R^N + C * R^N - C + fv\n         * <=> (C - fv) = R^N * (pv + C)\n         * <=> log[(C - fv) / (pv + C)] = N * log(R)\n         */\n        if (r === 0) {\n            return -(fv + pv) / p;\n        }\n        const c = (p * (1 + r * t)) / r;\n        return Math.log((c - fv) / (pv + c)) / Math.log(1 + r);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// NPV\n// -----------------------------------------------------------------------------\nfunction npvResult(r, startValue, values, locale) {\n    let i = 0;\n    return reduceNumbers(values, (acc, v) => {\n        i++;\n        return acc + v / (1 + r) ** i;\n    }, startValue, locale);\n}\nconst NPV = {\n    description: _t(\"The net present value of an investment based on a series of periodic cash flows and a discount rate.\"),\n    args: [\n        arg(\"discount (number)\", _t(\"The discount rate of the investment over one period.\")),\n        arg(\"cashflow1 (number, range<number>)\", _t(\"The first future cash flow.\")),\n        arg(\"cashflow2 (number, range<number>, repeating)\", _t(\"Additional future cash flows.\")),\n    ],\n    // to do: replace by dollar format\n    compute: function (discount, ...values) {\n        const _discount = toNumber(discount, this.locale);\n        assert(() => _discount !== -1, _t(\"The discount (%s) must be different from -1.\", _discount.toString()));\n        return {\n            value: npvResult(_discount, 0, values, this.locale),\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PDURATION\n// -----------------------------------------------------------------------------\nconst PDURATION = {\n    description: _t(\"Computes the number of periods needed for an investment to reach a value.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The rate at which the investment grows each period.\")),\n        arg(\"present_value (number)\", _t(\"The investment's current value.\")),\n        arg(\"future_value (number)\", _t(\"The investment's desired future value.\")),\n    ],\n    compute: function (rate, presentValue, futureValue) {\n        const _rate = toNumber(rate, this.locale);\n        const _presentValue = toNumber(presentValue, this.locale);\n        const _futureValue = toNumber(futureValue, this.locale);\n        assertRateStrictlyPositive(_rate);\n        assert(() => _presentValue > 0, _t(\"The present_value (%s) must be strictly positive.\", _presentValue.toString()));\n        assert(() => _futureValue > 0, _t(\"The future_value (%s) must be strictly positive.\", _futureValue.toString()));\n        return (Math.log(_futureValue) - Math.log(_presentValue)) / Math.log(1 + _rate);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PMT\n// -----------------------------------------------------------------------------\nfunction pmt(r, n, pv, fv, t) {\n    assertNumberOfPeriodsStrictlyPositive(n);\n    /**\n     * https://wiki.documentfoundation.org/Documentation/Calc_Functions/PMT\n     *\n     * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r\n     *\n     * We simply the equation for p\n     */\n    if (r === 0) {\n        return -(fv + pv) / n;\n    }\n    let payment = -(pv * (1 + r) ** n + fv);\n    payment = (payment * r) / ((1 + r * t) * ((1 + r) ** n - 1));\n    return payment;\n}\nconst PMT = {\n    description: _t(\"Periodic payment for an annuity investment.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"present_value (number)\", _t(\"The current value of the annuity.\")),\n        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t(\"The future value remaining after the final payment has been made.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n    ],\n    compute: function (rate, numberOfPeriods, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {\n        const n = toNumber(numberOfPeriods, this.locale);\n        const r = toNumber(rate, this.locale);\n        const t = toBoolean(endOrBeginning) ? 1 : 0;\n        const fv = toNumber(futureValue, this.locale);\n        const pv = toNumber(presentValue, this.locale);\n        return { value: pmt(r, n, pv, fv, t), format: \"#,##0.00\" };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PPMT\n// -----------------------------------------------------------------------------\nfunction ppmt(r, per, n, pValue, fValue, t) {\n    assertNumberOfPeriodsStrictlyPositive(n);\n    assert(() => per > 0 && per <= n, _t(\"The period must be between 1 and number_of_periods (%s)\", n));\n    const payment = pmt(r, n, pValue, fValue, t);\n    if (t === 1 && per === 1)\n        return payment;\n    const eqPeriod = t === 0 ? per - 1 : per - 2;\n    const eqPv = pValue + payment * t;\n    const capitalAtPeriod = -fv(r, eqPeriod, payment, eqPv, 0);\n    const currentInterest = capitalAtPeriod * r;\n    return payment + currentInterest;\n}\nconst PPMT = {\n    description: _t(\"Payment on the principal of an investment.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"period (number)\", _t(\"The amortization period, in terms of number of periods.\")),\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"present_value (number)\", _t(\"The current value of the annuity.\")),\n        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t(\"The future value remaining after the final payment has been made.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n    ],\n    compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {\n        const n = toNumber(numberOfPeriods, this.locale);\n        const r = toNumber(rate, this.locale);\n        const period = toNumber(currentPeriod, this.locale);\n        const type = toBoolean(endOrBeginning) ? 1 : 0;\n        const fv = toNumber(futureValue, this.locale);\n        const pv = toNumber(presentValue, this.locale);\n        return {\n            value: ppmt(r, period, n, pv, fv, type),\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PV\n// -----------------------------------------------------------------------------\nconst PV = {\n    description: _t(\"Present value of an annuity investment.\"),\n    args: [\n        arg(\"rate (number)\", _t(\"The interest rate.\")),\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"payment_amount (number)\", _t(\"The amount per period to be paid.\")),\n        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t(\"The future value remaining after the final payment has been made.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n    ],\n    // to do: replace by dollar format\n    compute: function (rate, numberOfPeriods, paymentAmount, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {\n        futureValue = futureValue || 0;\n        endOrBeginning = endOrBeginning || 0;\n        const r = toNumber(rate, this.locale);\n        const n = toNumber(numberOfPeriods, this.locale);\n        const p = toNumber(paymentAmount, this.locale);\n        const fv = toNumber(futureValue, this.locale);\n        const type = toBoolean(endOrBeginning) ? 1 : 0;\n        // https://wiki.documentfoundation.org/Documentation/Calc_Functions/PV\n        return {\n            value: r\n                ? -((p * (1 + r * type) * ((1 + r) ** n - 1)) / r + fv) / (1 + r) ** n\n                : -(fv + p * n),\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PRICE\n// -----------------------------------------------------------------------------\nconst PRICE = {\n    description: _t(\"Price of a security paying periodic interest.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"yield (number)\", _t(\"The expected annual yield of the security.\")),\n        arg(\"redemption (number)\", _t(\"The redemption amount per 100 face value, or par.\")),\n        arg(\"frequency (number)\", _t(\"The number of interest or coupon payments per year (1, 2, or 4).\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, rate, securityYield, redemption, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _rate = toNumber(rate, this.locale);\n        const _yield = toNumber(securityYield, this.locale);\n        const _redemption = toNumber(redemption, this.locale);\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assert(() => _rate >= 0, _t(\"The rate (%s) must be positive or null.\", _rate.toString()));\n        assert(() => _yield >= 0, _t(\"The yield (%s) must be positive or null.\", _yield.toString()));\n        assertRedemptionStrictlyPositive(_redemption);\n        const years = getYearFrac(_settlement, _maturity, _dayCountConvention);\n        const nbrRealCoupons = years * _frequency;\n        const nbrFullCoupons = Math.ceil(nbrRealCoupons);\n        const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1;\n        const yieldFactorPerPeriod = 1 + _yield / _frequency;\n        const cashFlowFromCoupon = (100 * _rate) / _frequency;\n        if (nbrFullCoupons === 1) {\n            return ((cashFlowFromCoupon + _redemption) / ((timeFirstCoupon * _yield) / _frequency + 1) -\n                cashFlowFromCoupon * (1 - timeFirstCoupon));\n        }\n        let cashFlowsPresentValue = 0;\n        for (let i = 1; i <= nbrFullCoupons; i++) {\n            cashFlowsPresentValue +=\n                cashFlowFromCoupon / yieldFactorPerPeriod ** (i - 1 + timeFirstCoupon);\n        }\n        const redemptionPresentValue = _redemption / yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon);\n        return (redemptionPresentValue + cashFlowsPresentValue - cashFlowFromCoupon * (1 - timeFirstCoupon));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PRICEDISC\n// -----------------------------------------------------------------------------\nconst PRICEDISC = {\n    description: _t(\"Price of a discount security.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"discount (number)\", _t(\"The discount rate of the security at time of purchase.\")),\n        arg(\"redemption (number)\", _t(\"The redemption amount per 100 face value, or par.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, discount, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _discount = toNumber(discount, this.locale);\n        const _redemption = toNumber(redemption, this.locale);\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assertDiscountStrictlyPositive(_discount);\n        assertRedemptionStrictlyPositive(_redemption);\n        /**\n         * https://support.microsoft.com/en-us/office/pricedisc-function-d06ad7c1-380e-4be7-9fd9-75e3079acfd3\n         *\n         * B = number of days in year, depending on year basis\n         * DSM = number of days from settlement to maturity\n         *\n         * PRICEDISC = redemption - discount * redemption * (DSM/B)\n         */\n        const yearsFrac = getYearFrac(_settlement, _maturity, _dayCountConvention);\n        return _redemption - _discount * _redemption * yearsFrac;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PRICEMAT\n// -----------------------------------------------------------------------------\nconst PRICEMAT = {\n    description: _t(\"Calculates the price of a security paying interest at maturity, based on expected yield.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"issue (date)\", _t(\"The date the security was initially issued.\")),\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"yield (number)\", _t(\"The expected annual yield of the security.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, issue, rate, securityYield, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _issue = Math.trunc(toNumber(issue, this.locale));\n        const _rate = toNumber(rate, this.locale);\n        const _yield = toNumber(securityYield, this.locale);\n        const _dayCount = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertSettlementAndIssueDatesAreValid(_settlement, _issue);\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertDayCountConventionIsValid(_dayCount);\n        assert(() => _rate >= 0, _t(\"The rate (%s) must be positive or null.\", _rate.toString()));\n        assert(() => _yield >= 0, _t(\"The yield (%s) must be positive or null.\", _yield.toString()));\n        /**\n         * https://support.microsoft.com/en-us/office/pricemat-function-52c3b4da-bc7e-476a-989f-a95f675cae77\n         *\n         * B = number of days in year, depending on year basis\n         * DSM = number of days from settlement to maturity\n         * DIM = number of days from issue to maturity\n         * DIS = number of days from issue to settlement\n         *\n         *             100 + (DIM/B * rate * 100)\n         *  PRICEMAT =  __________________________   - (DIS/B * rate * 100)\n         *              1 + (DSM/B * yield)\n         *\n         * The ratios number_of_days / days_in_year are computed using the YEARFRAC function, that handle\n         * differences due to day count conventions.\n         *\n         * Compatibility note :\n         *\n         * Contrary to GSheet and OpenOffice, Excel doesn't seems to always use its own YEARFRAC function\n         * to compute PRICEMAT, and give different values for some combinations of dates and day count\n         * conventions ( notably for leap years and dayCountConvention = 1 (Actual/Actual)).\n         *\n         * Our function PRICEMAT give us the same results as LibreOffice Calc.\n         * Google Sheet use the formula with YEARFRAC, but its YEARFRAC function results are different\n         * from the results of Excel/LibreOffice, thus we get different values with PRICEMAT.\n         *\n         */\n        const settlementToMaturity = getYearFrac(_settlement, _maturity, _dayCount);\n        const issueToSettlement = getYearFrac(_settlement, _issue, _dayCount);\n        const issueToMaturity = getYearFrac(_issue, _maturity, _dayCount);\n        const numerator = 100 + issueToMaturity * _rate * 100;\n        const denominator = 1 + settlementToMaturity * _yield;\n        const term2 = issueToSettlement * _rate * 100;\n        return numerator / denominator - term2;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// RATE\n// -----------------------------------------------------------------------------\nconst RATE_GUESS_DEFAULT = 0.1;\nconst RATE = {\n    description: _t(\"Interest rate of an annuity investment.\"),\n    args: [\n        arg(\"number_of_periods (number)\", _t(\"The number of payments to be made.\")),\n        arg(\"payment_per_period (number)\", _t(\"The amount per period to be paid.\")),\n        arg(\"present_value (number)\", _t(\"The current value of the annuity.\")),\n        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t(\"The future value remaining after the final payment has been made.\")),\n        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t(\"Whether payments are due at the end (0) or beginning (1) of each period.\")),\n        arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t(\"An estimate for what the interest rate will be.\")),\n    ],\n    compute: function (numberOfPeriods, paymentPerPeriod, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }, rateGuess = { value: RATE_GUESS_DEFAULT }) {\n        const n = toNumber(numberOfPeriods, this.locale);\n        const payment = toNumber(paymentPerPeriod, this.locale);\n        const type = toBoolean(endOrBeginning) ? 1 : 0;\n        const guess = toNumber(rateGuess, this.locale) || RATE_GUESS_DEFAULT;\n        let fv = toNumber(futureValue, this.locale);\n        let pv = toNumber(presentValue, this.locale);\n        assertNumberOfPeriodsStrictlyPositive(n);\n        assert(() => [payment, pv, fv].some((val) => val > 0) && [payment, pv, fv].some((val) => val < 0), _t(\"There must be both positive and negative values in [payment_amount, present_value, future_value].\", n.toString()));\n        assertRateGuessStrictlyGreaterThanMinusOne(guess);\n        fv -= payment * type;\n        pv += payment * type;\n        // https://github.com/apache/openoffice/blob/trunk/main/sc/source/core/tool/interpr2.cxx\n        const func = (rate) => {\n            const powN = Math.pow(1 + rate, n);\n            const intResult = (powN - 1) / rate;\n            return fv + pv * powN + payment * intResult;\n        };\n        const derivFunc = (rate) => {\n            const powNMinus1 = Math.pow(1 + rate, n - 1);\n            const powN = Math.pow(1 + rate, n);\n            const intResult = (powN - 1) / rate;\n            const intResultDeriv = (n * powNMinus1) / rate - intResult / rate;\n            const fTermDerivation = pv * n * powNMinus1 + payment * intResultDeriv;\n            return fTermDerivation;\n        };\n        return {\n            value: newtonMethod(func, derivFunc, guess, 40, 1e-5),\n            format: \"0%\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// RECEIVED\n// -----------------------------------------------------------------------------\nconst RECEIVED = {\n    description: _t(\"Amount received at maturity for a security.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"investment (number)\", _t(\"The amount invested (irrespective of face value of each security).\")),\n        arg(\"discount (number)\", _t(\"The discount rate of the security invested in.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, investment, discount, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _investment = toNumber(investment, this.locale);\n        const _discount = toNumber(discount, this.locale);\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assertInvestmentStrictlyPositive(_investment);\n        assertDiscountStrictlyPositive(_discount);\n        /**\n         * https://support.microsoft.com/en-us/office/received-function-7a3f8b93-6611-4f81-8576-828312c9b5e5\n         *\n         *                    investment\n         * RECEIVED = _________________________\n         *              1 - discount * DSM / B\n         *\n         * with DSM = number of days from settlement to maturity and B = number of days in a year\n         *\n         * The ratio DSM/B can be computed with the YEARFRAC function to take the dayCountConvention into account.\n         */\n        const yearsFrac = getYearFrac(_settlement, _maturity, _dayCountConvention);\n        return _investment / (1 - _discount * yearsFrac);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// RRI\n// -----------------------------------------------------------------------------\nconst RRI = {\n    description: _t(\"Computes the rate needed for an investment to reach a specific value within a specific number of periods.\"),\n    args: [\n        arg(\"number_of_periods (number)\", _t(\"The number of periods.\")),\n        arg(\"present_value (number)\", _t(\"The present value of the investment.\")),\n        arg(\"future_value (number)\", _t(\"The future value of the investment.\")),\n    ],\n    compute: function (numberOfPeriods, presentValue, futureValue) {\n        const n = toNumber(numberOfPeriods, this.locale);\n        const pv = toNumber(presentValue, this.locale);\n        const fv = toNumber(futureValue, this.locale);\n        assertNumberOfPeriodsStrictlyPositive(n);\n        /**\n         * https://support.microsoft.com/en-us/office/rri-function-6f5822d8-7ef1-4233-944c-79e8172930f4\n         *\n         * RRI = (future value / present value) ^ (1 / number of periods) - 1\n         */\n        return (fv / pv) ** (1 / n) - 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SLN\n// -----------------------------------------------------------------------------\nconst SLN = {\n    description: _t(\"Depreciation of an asset using the straight-line method.\"),\n    args: [\n        arg(\"cost (number)\", _t(\"The initial cost of the asset.\")),\n        arg(\"salvage (number)\", _t(\"The value of the asset at the end of depreciation.\")),\n        arg(\"life (number)\", _t(\"The number of periods over which the asset is depreciated.\")),\n    ],\n    compute: function (cost, salvage, life) {\n        const _cost = toNumber(cost, this.locale);\n        const _salvage = toNumber(salvage, this.locale);\n        const _life = toNumber(life, this.locale);\n        // No assertion is done on the values of the arguments to be compatible with Excel/Gsheet that don't check the values.\n        // It's up to the user to make sure the arguments make sense, which is good design because the user is smart.\n        return {\n            value: (_cost - _salvage) / _life,\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SYD\n// -----------------------------------------------------------------------------\nconst SYD = {\n    description: _t(\"Depreciation via sum of years digit method.\"),\n    args: [\n        arg(\"cost (number)\", _t(\"The initial cost of the asset.\")),\n        arg(\"salvage (number)\", _t(\"The value of the asset at the end of depreciation.\")),\n        arg(\"life (number)\", _t(\"The number of periods over which the asset is depreciated.\")),\n        arg(\"period (number)\", _t(\"The single period within life for which to calculate depreciation.\")),\n    ],\n    compute: function (cost, salvage, life, period) {\n        const _cost = toNumber(cost, this.locale);\n        const _salvage = toNumber(salvage, this.locale);\n        const _life = toNumber(life, this.locale);\n        const _period = toNumber(period, this.locale);\n        assertPeriodStrictlyPositive(_period);\n        assertLifeStrictlyPositive(_life);\n        assertPeriodSmallerOrEqualToLife(_period, _life);\n        /**\n         * This deprecation method use the sum of digits of the periods of the life as the deprecation factor.\n         * For example for a life = 5, we have a deprecation factor or 1 + 2 + 3 + 4 + 5 = 15 = life * (life + 1) / 2 = F.\n         *\n         * The deprecation for a period p is then computed based on F and the remaining lifetime at the period P.\n         *\n         * deprecation = (cost - salvage) * (number of remaining periods / F)\n         */\n        const deprecFactor = (_life * (_life + 1)) / 2;\n        const remainingPeriods = _life - _period + 1;\n        return {\n            value: (_cost - _salvage) * (remainingPeriods / deprecFactor),\n            format: \"#,##0.00\",\n        };\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TBILLPRICE\n// -----------------------------------------------------------------------------\nfunction tBillPrice(start, end, disc) {\n    /**\n     * https://support.microsoft.com/en-us/office/tbillprice-function-eacca992-c29d-425a-9eb8-0513fe6035a2\n     *\n     * TBILLPRICE = 100 * (1 - discount * DSM / 360)\n     *\n     * with DSM = number of days from settlement to maturity\n     *\n     * The ratio DSM/360 can be computed with the YEARFRAC function with dayCountConvention = 2 (actual/360).\n     */\n    const yearFrac = getYearFrac(start, end, 2);\n    return 100 * (1 - disc * yearFrac);\n}\nconst TBILLPRICE = {\n    description: _t(\"Price of a US Treasury bill.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"discount (number)\", _t(\"The discount rate of the bill at time of purchase.\")),\n    ],\n    compute: function (settlement, maturity, discount) {\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const disc = toNumber(discount, this.locale);\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale);\n        assertDiscountStrictlyPositive(disc);\n        assertDiscountStrictlySmallerThanOne(disc);\n        return tBillPrice(start, end, disc);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TBILLEQ\n// -----------------------------------------------------------------------------\nconst TBILLEQ = {\n    description: _t(\"Equivalent rate of return for a US Treasury bill.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"discount (number)\", _t(\"The discount rate of the bill at time of purchase.\")),\n    ],\n    compute: function (settlement, maturity, discount) {\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const disc = toNumber(discount, this.locale);\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale);\n        assertDiscountStrictlyPositive(disc);\n        assertDiscountStrictlySmallerThanOne(disc);\n        /**\n         * https://support.microsoft.com/en-us/office/tbilleq-function-2ab72d90-9b4d-4efe-9fc2-0f81f2c19c8c\n         *\n         *               365 * discount\n         * TBILLEQ = ________________________\n         *            360 - discount * DSM\n         *\n         * with DSM = number of days from settlement to maturity\n         *\n         * What is not indicated in the Excel documentation is that this formula only works for duration between settlement\n         * and maturity that are less than 6 months (182 days). This is because US Treasury bills use semi-annual interest,\n         * and thus we have to take into account the compound interest for the calculation.\n         *\n         * For this case, the formula becomes (Treasury Securities and Derivatives, by Frank J. Fabozzi, page 49)\n         *\n         *            -2X + 2* SQRT[ X\u00b2 - (2X - 1) * (1 - 100/p) ]\n         * TBILLEQ = ________________________________________________\n         *                            2X - 1\n         *\n         * with X = DSM / (number of days in a year),\n         *  and p is the price, computed with TBILLPRICE\n         *\n         * Note that from my tests in Excel, we take (number of days in a year) = 366 ONLY if DSM is 366, not if\n         * the settlement year is a leap year.\n         *\n         */\n        const nDays = DAYS.compute.bind(this)({ value: end }, { value: start });\n        if (nDays <= 182) {\n            return (365 * disc) / (360 - disc * nDays);\n        }\n        const p = tBillPrice(start, end, disc) / 100;\n        const daysInYear = nDays === 366 ? 366 : 365;\n        const x = nDays / daysInYear;\n        const num = -2 * x + 2 * Math.sqrt(x ** 2 - (2 * x - 1) * (1 - 1 / p));\n        const denom = 2 * x - 1;\n        return num / denom;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TBILLYIELD\n// -----------------------------------------------------------------------------\nconst TBILLYIELD = {\n    description: _t(\"The yield of a US Treasury bill based on price.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"price (number)\", _t(\"The price at which the security is bought per 100 face value.\")),\n    ],\n    compute: function (settlement, maturity, price) {\n        const start = Math.trunc(toNumber(settlement, this.locale));\n        const end = Math.trunc(toNumber(maturity, this.locale));\n        const p = toNumber(price, this.locale);\n        assertMaturityAndSettlementDatesAreValid(start, end);\n        assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale);\n        assertPriceStrictlyPositive(p);\n        /**\n         * https://support.microsoft.com/en-us/office/tbillyield-function-6d381232-f4b0-4cd5-8e97-45b9c03468ba\n         *\n         *              100 - price     360\n         * TBILLYIELD = ____________ * _____\n         *                 price        DSM\n         *\n         * with DSM = number of days from settlement to maturity\n         *\n         * The ratio DSM/360 can be computed with the YEARFRAC function with dayCountConvention = 2 (actual/360).\n         *\n         */\n        const yearFrac = getYearFrac(start, end, 2);\n        return ((100 - p) / p) * (1 / yearFrac);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// VDB\n// -----------------------------------------------------------------------------\nconst DEFAULT_VDB_NO_SWITCH = false;\nconst VDB = {\n    description: _t(\"Variable declining balance. WARNING : does not handle decimal periods.\"),\n    args: [\n        arg(\"cost (number)\", _t(\"The initial cost of the asset.\")),\n        arg(\"salvage (number)\", _t(\"The value of the asset at the end of depreciation.\")),\n        arg(\"life (number)\", _t(\"The number of periods over which the asset is depreciated.\")),\n        arg(\"start (number)\", _t(\"Starting period to calculate depreciation.\")),\n        arg(\"end (number)\", _t(\"Ending period to calculate depreciation.\")),\n        arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t(\"The number of months in the first year of depreciation.\")),\n        arg(`no_switch (number, default=${DEFAULT_VDB_NO_SWITCH})`, _t(\"Whether to switch to straight-line depreciation when the depreciation is greater than the declining balance calculation.\")),\n    ],\n    compute: function (cost, salvage, life, startPeriod, endPeriod, factor = { value: DEFAULT_DDB_DEPRECIATION_FACTOR }, noSwitch = { value: DEFAULT_VDB_NO_SWITCH }) {\n        factor = factor || 0;\n        const _cost = toNumber(cost, this.locale);\n        const _salvage = toNumber(salvage, this.locale);\n        const _life = toNumber(life, this.locale);\n        /* TODO : handle decimal periods\n         * on end_period it looks like it is a simple linear function, but I cannot understand exactly how\n         * decimals periods are handled with start_period.\n         */\n        const _startPeriod = Math.trunc(toNumber(startPeriod, this.locale));\n        const _endPeriod = Math.trunc(toNumber(endPeriod, this.locale));\n        const _factor = toNumber(factor, this.locale);\n        const _noSwitch = toBoolean(noSwitch);\n        assertCostPositiveOrZero(_cost);\n        assertSalvagePositiveOrZero(_salvage);\n        assertStartAndEndPeriodAreValid(_startPeriod, _endPeriod, _life);\n        assertDeprecationFactorStrictlyPositive(_factor);\n        if (_cost === 0)\n            return 0;\n        if (_salvage >= _cost) {\n            return _startPeriod < 1 ? _cost - _salvage : 0;\n        }\n        const doubleDeprecFactor = _factor / _life;\n        if (doubleDeprecFactor >= 1) {\n            return _startPeriod < 1 ? _cost - _salvage : 0;\n        }\n        let previousCost = _cost;\n        let currentDeprec = 0;\n        let resultDeprec = 0;\n        let isLinearDeprec = false;\n        for (let i = 0; i < _endPeriod; i++) {\n            // compute the current deprecation, or keep the last one if we reached a stage of linear deprecation\n            if (!isLinearDeprec || _noSwitch) {\n                const doubleDeprec = previousCost * doubleDeprecFactor;\n                const remainingPeriods = _life - i;\n                const linearDeprec = (previousCost - _salvage) / remainingPeriods;\n                if (!_noSwitch && linearDeprec > doubleDeprec) {\n                    isLinearDeprec = true;\n                    currentDeprec = linearDeprec;\n                }\n                else {\n                    currentDeprec = doubleDeprec;\n                }\n            }\n            const nextCost = Math.max(previousCost - currentDeprec, _salvage);\n            if (i >= _startPeriod) {\n                resultDeprec += previousCost - nextCost;\n            }\n            previousCost = nextCost;\n        }\n        return resultDeprec;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// XIRR\n// -----------------------------------------------------------------------------\nconst XIRR = {\n    description: _t(\"Internal rate of return given non-periodic cash flows.\"),\n    args: [\n        arg(\"cashflow_amounts (range<number>)\", _t(\"An range containing the income or payments associated with the investment.\")),\n        arg(\"cashflow_dates (range<number>)\", _t(\"An range with dates corresponding to the cash flows in cashflow_amounts.\")),\n        arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t(\"An estimate for what the internal rate of return will be.\")),\n    ],\n    compute: function (cashflowAmounts, cashflowDates, rateGuess = { value: RATE_GUESS_DEFAULT }) {\n        const guess = toNumber(rateGuess, this.locale);\n        const _cashFlows = cashflowAmounts.flat().map((val) => toNumber(val, this.locale));\n        const _dates = cashflowDates.flat().map((val) => toNumber(val, this.locale));\n        assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates);\n        assertCashFlowsHavePositiveAndNegativesValues(_cashFlows);\n        assertEveryDateGreaterThanFirstDateOfCashFlowDates(_dates);\n        assertRateGuessStrictlyGreaterThanMinusOne(guess);\n        const map = new Map();\n        for (const i of range(0, _dates.length)) {\n            const date = _dates[i];\n            if (map.has(date))\n                map.set(date, map.get(date) + _cashFlows[i]);\n            else\n                map.set(date, _cashFlows[i]);\n        }\n        const dates = Array.from(map.keys());\n        const values = dates.map((date) => map.get(date));\n        /**\n         * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d\n         *\n         * The rate is computed iteratively by trying to solve the equation\n         *\n         *\n         * 0 =    SUM     [ P_i * (1 + rate) ^((d_0 - d_i) / 365) ]  + P_0\n         *     i = 1 => n\n         *\n         * with P_i = price number i\n         *      d_i = date number i\n         *\n         * This function is not defined for rate < -1. For the case where we get rates < -1 in the Newton method, add\n         * a fallback for a number very close to -1 to continue the Newton method.\n         *\n         */\n        const func = (rate) => {\n            let value = values[0];\n            for (const i of range(1, values.length)) {\n                const dateDiff = (dates[0] - dates[i]) / 365;\n                value += values[i] * (1 + rate) ** dateDiff;\n            }\n            return value;\n        };\n        const derivFunc = (rate) => {\n            let deriv = 0;\n            for (const i of range(1, values.length)) {\n                const dateDiff = (dates[0] - dates[i]) / 365;\n                deriv += dateDiff * values[i] * (1 + rate) ** (dateDiff - 1);\n            }\n            return deriv;\n        };\n        const nanFallback = (previousFallback) => {\n            // -0.9 => -0.99 => -0.999 => ...\n            if (!previousFallback)\n                return -0.9;\n            return previousFallback / 10 - 0.9;\n        };\n        return newtonMethod(func, derivFunc, guess, 40, 1e-5, nanFallback);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// XNPV\n// -----------------------------------------------------------------------------\nconst XNPV = {\n    description: _t(\"Net present value given to non-periodic cash flows..\"),\n    args: [\n        arg(\"discount (number)\", _t(\"The discount rate of the investment over one period.\")),\n        arg(\"cashflow_amounts (number, range<number>)\", _t(\"An range containing the income or payments associated with the investment.\")),\n        arg(\"cashflow_dates (number, range<number>)\", _t(\"An range with dates corresponding to the cash flows in cashflow_amounts.\")),\n    ],\n    compute: function (discount, cashflowAmounts, cashflowDates) {\n        const rate = toNumber(discount, this.locale);\n        const _cashFlows = isMatrix(cashflowAmounts)\n            ? cashflowAmounts.flat().map((data) => strictToNumber(data, this.locale))\n            : [strictToNumber(cashflowAmounts, this.locale)];\n        const _dates = isMatrix(cashflowDates)\n            ? cashflowDates.flat().map((data) => strictToNumber(data, this.locale))\n            : [strictToNumber(cashflowDates, this.locale)];\n        if (isMatrix(cashflowDates) && isMatrix(cashflowAmounts)) {\n            assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates);\n        }\n        else {\n            assert(() => _cashFlows.length === _dates.length, _t(\"There must be the same number of values in cashflow_amounts and cashflow_dates.\"));\n        }\n        assertEveryDateGreaterThanFirstDateOfCashFlowDates(_dates);\n        assertRateStrictlyPositive(rate);\n        if (_cashFlows.length === 1)\n            return _cashFlows[0];\n        // aggregate values of the same date\n        const map = new Map();\n        for (const i of range(0, _dates.length)) {\n            const date = _dates[i];\n            if (map.has(date))\n                map.set(date, map.get(date) + _cashFlows[i]);\n            else\n                map.set(date, _cashFlows[i]);\n        }\n        const dates = Array.from(map.keys());\n        const values = dates.map((date) => map.get(date));\n        /**\n         * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d\n         *\n         * The present value is computed using\n         *\n         *\n         * NPV =    SUM     [ P_i *(1 + rate) ^((d_0 - d_i) / 365) ]  + P_0\n         *       i = 1 => n\n         *\n         * with P_i = price number i\n         *      d_i = date number i\n         *\n         *\n         */\n        let pv = values[0];\n        for (const i of range(1, values.length)) {\n            const dateDiff = (dates[0] - dates[i]) / 365;\n            pv += values[i] * (1 + rate) ** dateDiff;\n        }\n        return pv;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// YIELD\n// -----------------------------------------------------------------------------\nconst YIELD = {\n    description: _t(\"Annual yield of a security paying periodic interest.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"price (number)\", _t(\"The price at which the security is bought per 100 face value.\")),\n        arg(\"redemption (number)\", _t(\"The redemption amount per 100 face value, or par.\")),\n        arg(\"frequency (number)\", _t(\"The number of interest or coupon payments per year (1, 2, or 4).\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, rate, price, redemption, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _rate = toNumber(rate, this.locale);\n        const _price = toNumber(price, this.locale);\n        const _redemption = toNumber(redemption, this.locale);\n        const _frequency = Math.trunc(toNumber(frequency, this.locale));\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertCouponFrequencyIsValid(_frequency);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assert(() => _rate >= 0, _t(\"The rate (%s) must be positive or null.\", _rate.toString()));\n        assertPriceStrictlyPositive(_price);\n        assertRedemptionStrictlyPositive(_redemption);\n        const years = getYearFrac(_settlement, _maturity, _dayCountConvention);\n        const nbrRealCoupons = years * _frequency;\n        const nbrFullCoupons = Math.ceil(nbrRealCoupons);\n        const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1;\n        const cashFlowFromCoupon = (100 * _rate) / _frequency;\n        if (nbrFullCoupons === 1) {\n            const subPart = _price + cashFlowFromCoupon * (1 - timeFirstCoupon);\n            return (((_redemption + cashFlowFromCoupon - subPart) * _frequency * (1 / timeFirstCoupon)) /\n                subPart);\n        }\n        // The result of YIELD function is the yield at which the PRICE function will return the given price.\n        // This algorithm uses the Newton's method on the PRICE function to determine the result.\n        // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method\n        // As the PRICE function isn't continuous, we apply the Newton's method on the numerator of the PRICE formula.\n        // For simplicity, it is not yield but yieldFactorPerPeriod (= 1 + yield / frequency) which will be calibrated in Newton's method.\n        // yield can be deduced from yieldFactorPerPeriod in sequence.\n        function priceNumerator(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon, redemption) {\n            let result = redemption -\n                (price + cashFlowFromCoupon * (1 - timeFirstCoupon)) *\n                    yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon);\n            for (let i = 1; i <= nbrFullCoupons; i++) {\n                result += cashFlowFromCoupon * yieldFactorPerPeriod ** (i - 1);\n            }\n            return result;\n        }\n        function priceNumeratorDeriv(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon) {\n            let result = -(price + cashFlowFromCoupon * (1 - timeFirstCoupon)) *\n                (nbrFullCoupons - 1 + timeFirstCoupon) *\n                yieldFactorPerPeriod ** (nbrFullCoupons - 2 + timeFirstCoupon);\n            for (let i = 1; i <= nbrFullCoupons; i++) {\n                result += cashFlowFromCoupon * (i - 1) * yieldFactorPerPeriod ** (i - 2);\n            }\n            return result;\n        }\n        function func(x) {\n            return priceNumerator(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon, _redemption);\n        }\n        function derivFunc(x) {\n            return priceNumeratorDeriv(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon);\n        }\n        const initYield = _rate + 1;\n        const initYieldFactorPerPeriod = 1 + initYield / _frequency;\n        const methodResult = newtonMethod(func, derivFunc, initYieldFactorPerPeriod, 100, 1e-5);\n        return (methodResult - 1) * _frequency;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// YIELDDISC\n// -----------------------------------------------------------------------------\nconst YIELDDISC = {\n    description: _t(\"Annual yield of a discount security.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"price (number)\", _t(\"The price at which the security is bought per 100 face value.\")),\n        arg(\"redemption (number)\", _t(\"The redemption amount per 100 face value, or par.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, price, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _price = toNumber(price, this.locale);\n        const _redemption = toNumber(redemption, this.locale);\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assertPriceStrictlyPositive(_price);\n        assertRedemptionStrictlyPositive(_redemption);\n        /**\n         * https://wiki.documentfoundation.org/Documentation/Calc_Functions/YIELDDISC\n         *\n         *                    (redemption / price) - 1\n         * YIELDDISC = _____________________________________\n         *             YEARFRAC(settlement, maturity, basis)\n         */\n        const yearFrac = getYearFrac(_settlement, _maturity, _dayCountConvention);\n        return (_redemption / _price - 1) / yearFrac;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// YIELDMAT\n// -----------------------------------------------------------------------------\nconst YIELDMAT = {\n    description: _t(\"Annual yield of a security paying interest at maturity.\"),\n    args: [\n        arg(\"settlement (date)\", _t(\"The settlement date of the security, the date after issuance when the security is delivered to the buyer.\")),\n        arg(\"maturity (date)\", _t(\"The maturity or end date of the security, when it can be redeemed at face, or par value.\")),\n        arg(\"issue (date)\", _t(\"The date the security was initially issued.\")),\n        arg(\"rate (number)\", _t(\"The annualized rate of interest.\")),\n        arg(\"price (number)\", _t(\"The price at which the security is bought.\")),\n        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t(\"An indicator of what day count method to use.\")),\n    ],\n    compute: function (settlement, maturity, issue, rate, price, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {\n        dayCountConvention = dayCountConvention || 0;\n        const _settlement = Math.trunc(toNumber(settlement, this.locale));\n        const _maturity = Math.trunc(toNumber(maturity, this.locale));\n        const _issue = Math.trunc(toNumber(issue, this.locale));\n        const _rate = toNumber(rate, this.locale);\n        const _price = toNumber(price, this.locale);\n        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));\n        assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);\n        assertDayCountConventionIsValid(_dayCountConvention);\n        assert(() => _settlement >= _issue, _t(\"The settlement (%s) must be greater than or equal to the issue (%s).\", _settlement.toString(), _issue.toString()));\n        assert(() => _rate >= 0, _t(\"The rate (%s) must be positive or null.\", _rate.toString()));\n        assertPriceStrictlyPositive(_price);\n        const issueToMaturity = getYearFrac(_issue, _maturity, _dayCountConvention);\n        const issueToSettlement = getYearFrac(_issue, _settlement, _dayCountConvention);\n        const settlementToMaturity = getYearFrac(_settlement, _maturity, _dayCountConvention);\n        const numerator = (100 * (1 + _rate * issueToMaturity)) / (_price + 100 * _rate * issueToSettlement) - 1;\n        return numerator / settlementToMaturity;\n    },\n    isExported: true,\n};\n\nvar financial = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    ACCRINTM: ACCRINTM,\n    AMORLINC: AMORLINC,\n    COUPDAYBS: COUPDAYBS,\n    COUPDAYS: COUPDAYS,\n    COUPDAYSNC: COUPDAYSNC,\n    COUPNCD: COUPNCD,\n    COUPNUM: COUPNUM,\n    COUPPCD: COUPPCD,\n    CUMIPMT: CUMIPMT,\n    CUMPRINC: CUMPRINC,\n    DB: DB,\n    DDB: DDB,\n    DISC: DISC,\n    DOLLARDE: DOLLARDE,\n    DOLLARFR: DOLLARFR,\n    DURATION: DURATION,\n    EFFECT: EFFECT,\n    FV: FV,\n    FVSCHEDULE: FVSCHEDULE,\n    INTRATE: INTRATE,\n    IPMT: IPMT,\n    IRR: IRR,\n    ISPMT: ISPMT,\n    MDURATION: MDURATION,\n    MIRR: MIRR,\n    NOMINAL: NOMINAL,\n    NPER: NPER,\n    NPV: NPV,\n    PDURATION: PDURATION,\n    PMT: PMT,\n    PPMT: PPMT,\n    PRICE: PRICE,\n    PRICEDISC: PRICEDISC,\n    PRICEMAT: PRICEMAT,\n    PV: PV,\n    RATE: RATE,\n    RECEIVED: RECEIVED,\n    RRI: RRI,\n    SLN: SLN,\n    SYD: SYD,\n    TBILLEQ: TBILLEQ,\n    TBILLPRICE: TBILLPRICE,\n    TBILLYIELD: TBILLYIELD,\n    VDB: VDB,\n    XIRR: XIRR,\n    XNPV: XNPV,\n    YIELD: YIELD,\n    YIELDDISC: YIELDDISC,\n    YIELDMAT: YIELDMAT\n});\n\nvar State;\n(function (State) {\n    /**\n     * Initial state.\n     * Expecting any reference for the left part of a range\n     * e.g. \"A1\", \"1\", \"A\", \"Sheet1!A1\", \"Sheet1!A\"\n     */\n    State[State[\"LeftRef\"] = 0] = \"LeftRef\";\n    /**\n     * Expecting any reference for the right part of a range\n     * e.g. \"A1\", \"1\", \"A\", \"Sheet1!A1\", \"Sheet1!A\"\n     */\n    State[State[\"RightRef\"] = 1] = \"RightRef\";\n    /**\n     * Expecting the separator without any constraint on the right part\n     */\n    State[State[\"Separator\"] = 2] = \"Separator\";\n    /**\n     * Expecting the separator for a full column range\n     */\n    State[State[\"FullColumnSeparator\"] = 3] = \"FullColumnSeparator\";\n    /**\n     * Expecting the separator for a full row range\n     */\n    State[State[\"FullRowSeparator\"] = 4] = \"FullRowSeparator\";\n    /**\n     * Expecting the right part of a full column range\n     * e.g. \"1\", \"A1\"\n     */\n    State[State[\"RightColumnRef\"] = 5] = \"RightColumnRef\";\n    /**\n     * Expecting the right part of a full row range\n     * e.g. \"A\", \"A1\"\n     */\n    State[State[\"RightRowRef\"] = 6] = \"RightRowRef\";\n    /**\n     * Final state. A range has been matched\n     */\n    State[State[\"Found\"] = 7] = \"Found\";\n})(State || (State = {}));\nconst goTo = (state, guard = () => true) => [\n    {\n        goTo: state,\n        guard,\n    },\n];\nconst goToMulti = (state, guard = () => true) => ({\n    goTo: state,\n    guard,\n});\nconst machine = {\n    [State.LeftRef]: {\n        REFERENCE: goTo(State.Separator),\n        NUMBER: goTo(State.FullRowSeparator),\n        SYMBOL: [\n            goToMulti(State.FullColumnSeparator, (token) => isColReference(token.value)),\n            goToMulti(State.FullRowSeparator, (token) => isRowReference(token.value)),\n        ],\n    },\n    [State.FullColumnSeparator]: {\n        SPACE: goTo(State.FullColumnSeparator),\n        OPERATOR: goTo(State.RightColumnRef, (token) => token.value === \":\"),\n    },\n    [State.FullRowSeparator]: {\n        SPACE: goTo(State.FullRowSeparator),\n        OPERATOR: goTo(State.RightRowRef, (token) => token.value === \":\"),\n    },\n    [State.Separator]: {\n        SPACE: goTo(State.Separator),\n        OPERATOR: goTo(State.RightRef, (token) => token.value === \":\"),\n    },\n    [State.RightRef]: {\n        SPACE: goTo(State.RightRef),\n        NUMBER: goTo(State.Found),\n        REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),\n        SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),\n    },\n    [State.RightColumnRef]: {\n        SPACE: goTo(State.RightColumnRef),\n        SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),\n        REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),\n    },\n    [State.RightRowRef]: {\n        SPACE: goTo(State.RightRowRef),\n        NUMBER: goTo(State.Found),\n        REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),\n        SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),\n    },\n    [State.Found]: {},\n};\n/**\n * Check if the list of tokens starts with a sequence of tokens representing\n * a range.\n * If a range is found, the sequence is removed from the list and is returned\n * as a single token.\n */\nfunction matchReference(tokens) {\n    let head = 0;\n    let transitions = machine[State.LeftRef];\n    let matchedTokens = \"\";\n    while (transitions !== undefined) {\n        const token = tokens[head++];\n        if (!token) {\n            return null;\n        }\n        const transition = transitions[token.type]?.find((transition) => transition.guard(token));\n        const nextState = transition ? transition.goTo : undefined;\n        switch (nextState) {\n            case undefined:\n                return null;\n            case State.Found:\n                matchedTokens += token.value;\n                tokens.splice(0, head);\n                return {\n                    type: \"REFERENCE\",\n                    value: matchedTokens,\n                };\n            default:\n                transitions = machine[nextState];\n                matchedTokens += token.value;\n                break;\n        }\n    }\n    return null;\n}\n/**\n * Take the result of the tokenizer and transform it to be usable in the\n * manipulations of range\n *\n * @param formula\n */\nfunction rangeTokenize(formula, locale = DEFAULT_LOCALE) {\n    const tokens = tokenize(formula, locale);\n    const result = [];\n    while (tokens.length) {\n        result.push(matchReference(tokens) || tokens.shift());\n    }\n    return result;\n}\n\nconst functionRegex = /[a-zA-Z0-9\\_]+(\\.[a-zA-Z0-9\\_]+)*/;\nconst UNARY_OPERATORS_PREFIX = [\"-\", \"+\"];\nconst UNARY_OPERATORS_POSTFIX = [\"%\"];\nconst ASSOCIATIVE_OPERATORS = [\"*\", \"+\", \"&\"];\nconst OP_PRIORITY = {\n    \"^\": 30,\n    \"%\": 30,\n    \"*\": 20,\n    \"/\": 20,\n    \"+\": 15,\n    \"-\": 15,\n    \"&\": 13,\n    \">\": 10,\n    \"<>\": 10,\n    \">=\": 10,\n    \"<\": 10,\n    \"<=\": 10,\n    \"=\": 10,\n};\n/**\n * Parse the next operand in an arithmetic expression.\n * e.g.\n *  for 1+2*3, the next operand is 1\n *  for (1+2)*3, the next operand is (1+2)\n *  for SUM(1,2)+3, the next operand is SUM(1,2)\n */\nfunction parseOperand(tokens) {\n    const current = tokens.shift();\n    if (!current) {\n        throw new BadExpressionError();\n    }\n    switch (current.type) {\n        case \"DEBUGGER\":\n            const next = parseExpression(tokens, 1000);\n            next.debug = true;\n            return next;\n        case \"NUMBER\":\n            return { type: \"NUMBER\", value: parseNumber(current.value, DEFAULT_LOCALE) };\n        case \"STRING\":\n            return { type: \"STRING\", value: removeStringQuotes(current.value) };\n        case \"INVALID_REFERENCE\":\n            return {\n                type: \"REFERENCE\",\n                value: CellErrorType.InvalidReference,\n            };\n        case \"REFERENCE\":\n            if (tokens[0]?.value === \":\" && tokens[1]?.type === \"REFERENCE\") {\n                tokens.shift();\n                const rightReference = tokens.shift();\n                return {\n                    type: \"REFERENCE\",\n                    value: `${current.value}:${rightReference?.value}`,\n                };\n            }\n            return {\n                type: \"REFERENCE\",\n                value: current.value,\n            };\n        case \"SYMBOL\":\n            const value = current.value;\n            const nextToken = tokens[0];\n            if (nextToken?.type === \"LEFT_PAREN\" &&\n                functionRegex.test(current.value) &&\n                value === unquote(value, \"'\")) {\n                const args = parseFunctionArgs(tokens);\n                return { type: \"FUNCALL\", value: value, args };\n            }\n            const upperCaseValue = value.toUpperCase();\n            if (upperCaseValue === \"TRUE\" || upperCaseValue === \"FALSE\") {\n                return { type: \"BOOLEAN\", value: upperCaseValue === \"TRUE\" };\n            }\n            return { type: \"SYMBOL\", value: unquote(current.value, \"'\") };\n        case \"LEFT_PAREN\":\n            const result = parseExpression(tokens);\n            consumeOrThrow(tokens, \"RIGHT_PAREN\", _t(\"Missing closing parenthesis\"));\n            return result;\n        case \"OPERATOR\":\n            const operator = current.value;\n            if (UNARY_OPERATORS_PREFIX.includes(operator)) {\n                return {\n                    type: \"UNARY_OPERATION\",\n                    value: operator,\n                    operand: parseExpression(tokens, OP_PRIORITY[operator]),\n                };\n            }\n            throw new BadExpressionError(_t(\"Unexpected token: %s\", current.value));\n        default:\n            throw new BadExpressionError(_t(\"Unexpected token: %s\", current.value));\n    }\n}\nfunction parseFunctionArgs(tokens) {\n    consumeOrThrow(tokens, \"LEFT_PAREN\", _t(\"Missing opening parenthesis\"));\n    const nextToken = tokens[0];\n    if (nextToken?.type === \"RIGHT_PAREN\") {\n        consumeOrThrow(tokens, \"RIGHT_PAREN\");\n        return [];\n    }\n    const args = [];\n    args.push(parseOneFunctionArg(tokens));\n    while (tokens[0]?.type !== \"RIGHT_PAREN\") {\n        consumeOrThrow(tokens, \"ARG_SEPARATOR\", _t(\"Wrong function call\"));\n        args.push(parseOneFunctionArg(tokens));\n    }\n    consumeOrThrow(tokens, \"RIGHT_PAREN\");\n    return args;\n}\nfunction parseOneFunctionArg(tokens) {\n    const nextToken = tokens[0];\n    if (nextToken?.type === \"ARG_SEPARATOR\" || nextToken?.type === \"RIGHT_PAREN\") {\n        // arg is empty: \"sum(1,,2)\" \"sum(,1)\" \"sum(1,)\"\n        return { type: \"EMPTY\", value: \"\" };\n    }\n    return parseExpression(tokens);\n}\nfunction consumeOrThrow(tokens, type, message) {\n    const token = tokens.shift();\n    if (!token || token.type !== type) {\n        throw new BadExpressionError(message);\n    }\n}\nfunction parseExpression(tokens, parent_priority = 0) {\n    if (tokens.length === 0) {\n        throw new BadExpressionError();\n    }\n    let left = parseOperand(tokens);\n    // as long as we have operators with higher priority than the parent one,\n    // continue parsing the expression because it is a child sub-expression\n    while (tokens[0]?.type === \"OPERATOR\" && OP_PRIORITY[tokens[0].value] > parent_priority) {\n        const operator = tokens.shift().value;\n        if (UNARY_OPERATORS_POSTFIX.includes(operator)) {\n            left = {\n                type: \"UNARY_OPERATION\",\n                value: operator,\n                operand: left,\n                postfix: true,\n            };\n        }\n        else {\n            const right = parseExpression(tokens, OP_PRIORITY[operator]);\n            left = {\n                type: \"BIN_OPERATION\",\n                value: operator,\n                left,\n                right,\n            };\n        }\n    }\n    return left;\n}\n/**\n * Parse an expression (as a string) into an AST.\n */\nfunction parse(str) {\n    return parseTokens(rangeTokenize(str));\n}\nfunction parseTokens(tokens) {\n    tokens = tokens.filter((x) => x.type !== \"SPACE\");\n    if (tokens[0]?.value === \"=\") {\n        tokens.splice(0, 1);\n    }\n    const result = parseExpression(tokens);\n    if (tokens.length) {\n        throw new BadExpressionError();\n    }\n    return result;\n}\n/**\n * Allows to visit all nodes of an AST and apply a mapping function\n * to nodes of a specific type.\n * Useful if you want to convert some part of a formula.\n *\n * @example\n * convertAstNodes(ast, \"FUNCALL\", convertFormulaToExcel)\n *\n * function convertFormulaToExcel(ast: ASTFuncall) {\n *   // ...\n *   return modifiedAst\n * }\n */\nfunction convertAstNodes(ast, type, fn) {\n    return mapAst(ast, (ast) => {\n        if (ast.type === type) {\n            return fn(ast);\n        }\n        return ast;\n    });\n}\nfunction iterateAstNodes(ast) {\n    return Array.from(astIterator(ast));\n}\nfunction* astIterator(ast) {\n    yield ast;\n    switch (ast.type) {\n        case \"FUNCALL\":\n            for (const arg of ast.args) {\n                yield* astIterator(arg);\n            }\n            break;\n        case \"UNARY_OPERATION\":\n            yield* astIterator(ast.operand);\n            break;\n        case \"BIN_OPERATION\":\n            yield* astIterator(ast.left);\n            yield* astIterator(ast.right);\n            break;\n    }\n}\nfunction mapAst(ast, fn) {\n    ast = fn(ast);\n    switch (ast.type) {\n        case \"FUNCALL\":\n            return {\n                ...ast,\n                args: ast.args.map((child) => mapAst(child, fn)),\n            };\n        case \"UNARY_OPERATION\":\n            return {\n                ...ast,\n                operand: mapAst(ast.operand, fn),\n            };\n        case \"BIN_OPERATION\":\n            return {\n                ...ast,\n                right: mapAst(ast.right, fn),\n                left: mapAst(ast.left, fn),\n            };\n        default:\n            return ast;\n    }\n}\n/**\n * Converts an ast formula to the corresponding string\n */\nfunction astToFormula(ast) {\n    switch (ast.type) {\n        case \"FUNCALL\":\n            const args = ast.args.map((arg) => astToFormula(arg));\n            return `${ast.value}(${args.join(\",\")})`;\n        case \"NUMBER\":\n            return ast.value.toString();\n        case \"REFERENCE\":\n            return ast.value;\n        case \"STRING\":\n            return `\"${ast.value}\"`;\n        case \"BOOLEAN\":\n            return ast.value ? \"TRUE\" : \"FALSE\";\n        case \"UNARY_OPERATION\":\n            return ast.postfix\n                ? leftOperandToFormula(ast) + ast.value\n                : ast.value + rightOperandToFormula(ast);\n        case \"BIN_OPERATION\":\n            return leftOperandToFormula(ast) + ast.value + rightOperandToFormula(ast);\n        default:\n            return ast.value;\n    }\n}\n/**\n * Convert the left operand of a binary operation to the corresponding string\n * and enclose the result inside parenthesis if necessary.\n */\nfunction leftOperandToFormula(operationAST) {\n    const mainOperator = operationAST.value;\n    const leftOperation = \"left\" in operationAST ? operationAST.left : operationAST.operand;\n    const leftOperator = leftOperation.value;\n    const needParenthesis = leftOperation.type === \"BIN_OPERATION\" && OP_PRIORITY[leftOperator] < OP_PRIORITY[mainOperator];\n    return needParenthesis ? `(${astToFormula(leftOperation)})` : astToFormula(leftOperation);\n}\n/**\n * Convert the right operand of a binary or unary operation to the corresponding string\n * and enclose the result inside parenthesis if necessary.\n */\nfunction rightOperandToFormula(operationAST) {\n    const mainOperator = operationAST.value;\n    const rightOperation = \"right\" in operationAST ? operationAST.right : operationAST.operand;\n    const rightPriority = OP_PRIORITY[rightOperation.value];\n    const mainPriority = OP_PRIORITY[mainOperator];\n    let needParenthesis = false;\n    if (rightOperation.type !== \"BIN_OPERATION\") {\n        needParenthesis = false;\n    }\n    else if (rightPriority < mainPriority) {\n        needParenthesis = true;\n    }\n    else if (rightPriority === mainPriority && !ASSOCIATIVE_OPERATORS.includes(mainOperator)) {\n        needParenthesis = true;\n    }\n    return needParenthesis ? `(${astToFormula(rightOperation)})` : astToFormula(rightOperation);\n}\n\n/**\n * Add the following information on tokens:\n * - length\n * - start\n * - end\n */\nfunction enrichTokens(tokens) {\n    let current = 0;\n    return tokens.map((x) => {\n        const len = x.value.toString().length;\n        const token = Object.assign({}, x, {\n            start: current,\n            end: current + len,\n            length: len,\n        });\n        current = token.end;\n        return token;\n    });\n}\n/**\n * add on each token the length, start and end\n * also matches the opening to its closing parenthesis (using the same number)\n */\nfunction mapParenthesis(tokens) {\n    let maxParen = 1;\n    const stack = [];\n    return tokens.map((token) => {\n        if (token.type === \"LEFT_PAREN\") {\n            stack.push(maxParen);\n            token.parenIndex = maxParen;\n            maxParen++;\n        }\n        else if (token.type === \"RIGHT_PAREN\") {\n            token.parenIndex = stack.pop();\n        }\n        return token;\n    });\n}\n/**\n * add on each token its parent function and the index corresponding to\n * its position as an argument of the function.\n * In this example \"=MIN(42,SUM(MAX(1,2),3))\":\n * - the parent function of the token correspond to number 42 is the MIN function\n * - the argument position of the token correspond to number 42 is 0\n * - the parent function of the token correspond to number 3 is the SUM function\n * - the argument position of the token correspond to number 3 is 1\n */\nfunction mapParentFunction(tokens) {\n    let stack = [];\n    let functionStarted = \"\";\n    function pushTokenToFunctionContext(token) {\n        if (stack.length === 0) {\n            return;\n        }\n        const functionContext = stack.at(-1);\n        if (functionContext && functionContext.argsTokens) {\n            const { argsTokens, argPosition } = functionContext;\n            if (!argsTokens[argPosition]) {\n                argsTokens[argPosition] = [];\n            }\n            argsTokens[argPosition].push({ value: token.value, type: token.type });\n        }\n    }\n    const res = tokens.map((token, i) => {\n        if (![\"SPACE\", \"LEFT_PAREN\"].includes(token.type)) {\n            functionStarted = \"\";\n        }\n        switch (token.type) {\n            case \"SYMBOL\":\n                pushTokenToFunctionContext(token);\n                functionStarted = token.value;\n                break;\n            case \"LEFT_PAREN\":\n                stack.push({ parent: functionStarted, argPosition: 0, argsTokens: [], args: [] });\n                pushTokenToFunctionContext(token);\n                functionStarted = \"\";\n                break;\n            case \"RIGHT_PAREN\":\n                const child = stack.pop();\n                child?.argsTokens?.flat().forEach(pushTokenToFunctionContext);\n                pushTokenToFunctionContext(token);\n                break;\n            case \"ARG_SEPARATOR\":\n                pushTokenToFunctionContext(token);\n                if (stack.length) {\n                    // increment position on current function\n                    stack[stack.length - 1].argPosition++;\n                }\n                break;\n            default:\n                pushTokenToFunctionContext(token);\n                break;\n        }\n        if (stack.length) {\n            const functionContext = stack[stack.length - 1];\n            if (functionContext.parent) {\n                token.functionContext = Object.assign({}, functionContext);\n            }\n        }\n        return token;\n    });\n    return res;\n}\n/**\n * Parse the list of tokens that compose the arguments of a function to\n * their AST representation.\n */\nfunction addArgsAST(tokens) {\n    for (const token of tokens) {\n        if (token.functionContext) {\n            const { argsTokens, args } = token.functionContext;\n            // remove argsTokens from the context to remove noise\n            // The business logic should not need it, it is only used temporarily\n            // to build the arguments ASTs.\n            delete token.functionContext.argsTokens;\n            if (args.length || !argsTokens) {\n                // function context already process at a previous token\n                continue;\n            }\n            if (argsTokens[0]?.[0]?.type === \"LEFT_PAREN\") {\n                // remove the parenthesis leading the first argument\n                argsTokens[0] = argsTokens[0].slice(1);\n            }\n            for (const argTokens of argsTokens) {\n                let tokens = argTokens;\n                if (tokens.at(-1)?.type === \"ARG_SEPARATOR\") {\n                    tokens = tokens.slice(0, -1);\n                }\n                try {\n                    args.push(parseTokens(tokens));\n                }\n                catch (error) {\n                    args.push(undefined);\n                }\n            }\n        }\n    }\n    return tokens;\n}\n/**\n * Take the result of the tokenizer and transform it to be usable in the composer.\n *\n * @param formula\n */\nfunction composerTokenize(formula, locale) {\n    const tokens = rangeTokenize(formula, locale);\n    return addArgsAST(mapParentFunction(mapParenthesis(enrichTokens(tokens))));\n}\n\n/**\n * Change the reference types inside the given token, if the token represent a range or a cell\n *\n * Eg. :\n *   A1 => $A$1 => A$1 => $A1 => A1\n *   A1:$B$1 => $A$1:B$1 => A$1:$B1 => $A1:B1 => A1:$B$1\n */\nfunction loopThroughReferenceType(token) {\n    if (token.type !== \"REFERENCE\")\n        return token;\n    const { xc, sheetName } = splitReference(token.value);\n    const [left, right] = xc.split(\":\");\n    const updatedLeft = getTokenNextReferenceType(left);\n    const updatedRight = right ? `:${getTokenNextReferenceType(right)}` : \"\";\n    return { ...token, value: getFullReference(sheetName, updatedLeft + updatedRight) };\n}\n/**\n * Get a new token with a changed type of reference from the given cell token symbol.\n * Undefined behavior if given a token other than a cell or if the Xc contains a sheet reference\n *\n * A1 => $A$1 => A$1 => $A1 => A1\n */\nfunction getTokenNextReferenceType(xc) {\n    switch (getReferenceType(xc)) {\n        case \"none\":\n            xc = setXcToFixedReferenceType(xc, \"colrow\");\n            break;\n        case \"colrow\":\n            xc = setXcToFixedReferenceType(xc, \"row\");\n            break;\n        case \"row\":\n            xc = setXcToFixedReferenceType(xc, \"col\");\n            break;\n        case \"col\":\n            xc = setXcToFixedReferenceType(xc, \"none\");\n            break;\n    }\n    return xc;\n}\n/**\n * Returns the given XC with the given reference type. The XC string should not contain a sheet name.\n */\nfunction setXcToFixedReferenceType(xc, referenceType) {\n    if (xc.includes(\"!\")) {\n        throw new Error(\"The given XC should not contain a sheet name\");\n    }\n    xc = xc.replace(/\\$/g, \"\");\n    let indexOfNumber;\n    switch (referenceType) {\n        case \"col\":\n            return \"$\" + xc;\n        case \"row\":\n            indexOfNumber = xc.search(/[0-9]/);\n            return xc.slice(0, indexOfNumber) + \"$\" + xc.slice(indexOfNumber);\n        case \"colrow\":\n            indexOfNumber = xc.search(/[0-9]/);\n            if (indexOfNumber === -1 || indexOfNumber === 0) {\n                // no row number (eg. A) or no column (eg. 1)\n                return \"$\" + xc;\n            }\n            xc = xc.slice(0, indexOfNumber) + \"$\" + xc.slice(indexOfNumber);\n            return \"$\" + xc;\n        case \"none\":\n            return xc;\n    }\n}\n/**\n * Return the type of reference used in the given XC of a cell.\n * Undefined behavior if the XC have a sheet reference\n */\nfunction getReferenceType(xcCell) {\n    if (isColAndRowFixed(xcCell)) {\n        return \"colrow\";\n    }\n    else if (isColFixed(xcCell)) {\n        return \"col\";\n    }\n    else if (isRowFixed(xcCell)) {\n        return \"row\";\n    }\n    return \"none\";\n}\nfunction isColFixed(xc) {\n    return xc.startsWith(\"$\");\n}\nfunction isRowFixed(xc) {\n    return xc.includes(\"$\", 1);\n}\nfunction isColAndRowFixed(xc) {\n    return xc.startsWith(\"$\") && xc.length > 1 && xc.slice(1).includes(\"$\");\n}\n/**\n * Return the cycled reference if any (A1 -> $A$1 -> A$1 -> $A1 -> A1)\n */\nfunction cycleFixedReference(selection, content, locale) {\n    const currentTokens = content.startsWith(\"=\")\n        ? composerTokenize(content, locale)\n        : [];\n    const tokens = currentTokens.filter((t) => (t.start <= selection.start && t.end >= selection.start) ||\n        (t.start >= selection.start && t.start < selection.end));\n    const refTokens = tokens.filter((token) => token.type === \"REFERENCE\");\n    if (refTokens.length === 0) {\n        return;\n    }\n    const updatedReferences = tokens\n        .map(loopThroughReferenceType)\n        .map((token) => token.value)\n        .join(\"\");\n    const start = tokens[0].start;\n    const end = tokens[tokens.length - 1].end;\n    const newContent = content.slice(0, start) + updatedReferences + content.slice(end);\n    const lengthDiff = newContent.length - content.length;\n    const startOfTokens = refTokens[0].start;\n    const endOfTokens = refTokens[refTokens.length - 1].end + lengthDiff;\n    const newSelection = { start: startOfTokens, end: endOfTokens };\n    if (refTokens.length === 1 && selection.start === selection.end) {\n        newSelection.start = newSelection.end;\n    }\n    return { content: newContent, selection: newSelection };\n}\n\n// -----------------------------------------------------------------------------\n// CELL\n// -----------------------------------------------------------------------------\n// NOTE: missing from Excel: \"color\", \"filename\", \"parentheses\", \"prefix\", \"protect\" and \"width\"\nconst CELL_INFO_TYPES = [\"address\", \"col\", \"contents\", \"format\", \"row\", \"type\"];\nconst CELL = {\n    description: _t(\"Gets information about a cell.\"),\n    args: [\n        arg(\"info_type (string)\", _t(\"The type of information requested. Can be one of %s\", CELL_INFO_TYPES.join(\", \"))),\n        arg(\"reference (meta)\", _t(\"The reference to the cell.\")),\n    ],\n    compute: function (info, reference) {\n        const _info = toString(info).toLowerCase();\n        assert(() => CELL_INFO_TYPES.includes(_info), _t(\"The info_type should be one of %s.\", CELL_INFO_TYPES.join(\", \")));\n        const sheetId = this.__originSheetId;\n        const _reference = toString(reference);\n        const topLeftReference = _reference.includes(\":\") ? _reference.split(\":\")[0] : _reference;\n        let { sheetName, xc } = splitReference(topLeftReference);\n        // only put the sheet name if the referenced range is in another sheet than the cell the formula is on\n        sheetName = sheetName === this.getters.getSheetName(sheetId) ? undefined : sheetName;\n        const fixedRef = getFullReference(sheetName, setXcToFixedReferenceType(xc, \"colrow\"));\n        const range = this.getters.getRangeFromSheetXC(sheetId, fixedRef);\n        switch (_info) {\n            case \"address\":\n                return this.getters.getRangeString(range, sheetId);\n            case \"col\":\n                return range.zone.left + 1;\n            case \"contents\": {\n                const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };\n                return this.getters.getEvaluatedCell(position).value;\n            }\n            case \"format\": {\n                const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };\n                return this.getters.getEvaluatedCell(position).format || \"\";\n            }\n            case \"row\":\n                return range.zone.top + 1;\n            case \"type\": {\n                const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };\n                const type = this.getters.getEvaluatedCell(position).type;\n                if (type === CellValueType.empty) {\n                    return \"b\"; // blank\n                }\n                else if (type === CellValueType.text) {\n                    return \"l\"; // label\n                }\n                else {\n                    return \"v\"; // value\n                }\n            }\n        }\n        return \"\";\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISERR\n// -----------------------------------------------------------------------------\nconst ISERR = {\n    description: _t(\"Whether a value is an error other than #N/A.\"),\n    args: [arg(\"value (any)\", _t(\"The value to be verified as an error type.\"))],\n    compute: function (data) {\n        const value = data?.value;\n        return isEvaluationError(value) && value !== CellErrorType.NotAvailable;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISERROR\n// -----------------------------------------------------------------------------\nconst ISERROR = {\n    description: _t(\"Whether a value is an error.\"),\n    args: [arg(\"value (any)\", _t(\"The value to be verified as an error type.\"))],\n    compute: function (data) {\n        const value = data?.value;\n        return isEvaluationError(value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISLOGICAL\n// -----------------------------------------------------------------------------\nconst ISLOGICAL = {\n    description: _t(\"Whether a value is `true` or `false`.\"),\n    args: [arg(\"value (any)\", _t(\"The value to be verified as a logical TRUE or FALSE.\"))],\n    compute: function (value) {\n        return typeof value?.value === \"boolean\";\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISNA\n// -----------------------------------------------------------------------------\nconst ISNA = {\n    description: _t(\"Whether a value is the error #N/A.\"),\n    args: [arg(\"value (any)\", _t(\"The value to be verified as an error type.\"))],\n    compute: function (data) {\n        return data?.value === CellErrorType.NotAvailable;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISNONTEXT\n// -----------------------------------------------------------------------------\nconst ISNONTEXT = {\n    description: _t(\"Whether a value is non-textual.\"),\n    args: [arg(\"value (any)\", _t(\"The value to be checked.\"))],\n    compute: function (value) {\n        return !ISTEXT.compute.bind(this)(value);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISNUMBER\n// -----------------------------------------------------------------------------\nconst ISNUMBER = {\n    description: _t(\"Whether a value is a number.\"),\n    args: [arg(\"value (any)\", _t(\"The value to be verified as a number.\"))],\n    compute: function (value) {\n        return typeof value?.value === \"number\";\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISTEXT\n// -----------------------------------------------------------------------------\nconst ISTEXT = {\n    description: _t(\"Whether a value is text.\"),\n    args: [arg(\"value (any)\", _t(\"The value to be verified as text.\"))],\n    compute: function (value) {\n        return typeof value?.value === \"string\" && isEvaluationError(value?.value) === false;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ISBLANK\n// -----------------------------------------------------------------------------\nconst ISBLANK = {\n    description: _t(\"Whether the referenced cell is empty\"),\n    args: [arg(\"value (any)\", _t(\"Reference to the cell that will be checked for emptiness.\"))],\n    compute: function (value) {\n        return value?.value === null;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// NA\n// -----------------------------------------------------------------------------\nconst NA = {\n    description: _t(\"Returns the error value #N/A.\"),\n    args: [],\n    compute: function () {\n        return { value: CellErrorType.NotAvailable };\n    },\n    isExported: true,\n};\n\nvar info = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    CELL: CELL,\n    ISBLANK: ISBLANK,\n    ISERR: ISERR,\n    ISERROR: ISERROR,\n    ISLOGICAL: ISLOGICAL,\n    ISNA: ISNA,\n    ISNONTEXT: ISNONTEXT,\n    ISNUMBER: ISNUMBER,\n    ISTEXT: ISTEXT,\n    NA: NA\n});\n\n// -----------------------------------------------------------------------------\n// AND\n// -----------------------------------------------------------------------------\nconst AND = {\n    description: _t(\"Logical `and` operator.\"),\n    args: [\n        arg(\"logical_expression1 (boolean, range<boolean>)\", _t(\"An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.\")),\n        arg(\"logical_expression2 (boolean, range<boolean>, repeating)\", _t(\"More expressions that represent logical values.\")),\n    ],\n    compute: function (...logicalExpressions) {\n        const { result, foundBoolean } = boolAnd(logicalExpressions);\n        assert(() => foundBoolean, _t(\"[[FUNCTION_NAME]] has no valid input data.\"));\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FALSE\n// -----------------------------------------------------------------------------\nconst FALSE = {\n    description: _t(\"Logical value `false`.\"),\n    args: [],\n    compute: function () {\n        return false;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// IF\n// -----------------------------------------------------------------------------\nconst IF = {\n    description: _t(\"Returns value depending on logical expression.\"),\n    args: [\n        arg(\"logical_expression (boolean)\", _t(\"An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE.\")),\n        arg(\"value_if_true (any)\", _t(\"The value the function returns if logical_expression is TRUE.\")),\n        arg(\"value_if_false (any, default=FALSE)\", _t(\"The value the function returns if logical_expression is FALSE.\")),\n    ],\n    compute: function (logicalExpression, valueIfTrue, valueIfFalse) {\n        const result = toBoolean(logicalExpression?.value) ? valueIfTrue : valueIfFalse;\n        if (result === undefined) {\n            return { value: \"\" };\n        }\n        if (result.value === null) {\n            result.value = \"\";\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// IFERROR\n// -----------------------------------------------------------------------------\nconst IFERROR = {\n    description: _t(\"Value if it is not an error, otherwise 2nd argument.\"),\n    args: [\n        arg(\"value (any)\", _t(\"The value to return if value itself is not an error.\")),\n        arg(`value_if_error (any, default=\"empty\")`, _t(\"The value the function returns if value is an error.\")),\n    ],\n    compute: function (value, valueIfError = { value: \"\" }) {\n        const result = isEvaluationError(value?.value) ? valueIfError : value;\n        if (result === undefined) {\n            return { value: \"\" };\n        }\n        if (result.value === null) {\n            result.value = \"\";\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// IFNA\n// -----------------------------------------------------------------------------\nconst IFNA = {\n    description: _t(\"Value if it is not an #N/A error, otherwise 2nd argument.\"),\n    args: [\n        arg(\"value (any)\", _t(\"The value to return if value itself is not #N/A an error.\")),\n        arg(`value_if_error (any, default=\"empty\")`, _t(\"The value the function returns if value is an #N/A error.\")),\n    ],\n    compute: function (value, valueIfError = { value: \"\" }) {\n        const result = value?.value === CellErrorType.NotAvailable ? valueIfError : value;\n        if (result === undefined) {\n            return { value: \"\" };\n        }\n        if (result.value === null) {\n            result.value = \"\";\n        }\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// IFS\n// -----------------------------------------------------------------------------\nconst IFS = {\n    description: _t(\"Returns a value depending on multiple logical expressions.\"),\n    args: [\n        arg(\"condition1 (boolean)\", _t(\"The first condition to be evaluated. This can be a boolean, a number, an array, or a reference to any of those.\")),\n        arg(\"value1 (any)\", _t(\"The returned value if condition1 is TRUE.\")),\n        arg(\"condition2 (boolean, any, repeating)\", _t(\"Additional conditions to be evaluated if the previous ones are FALSE.\")),\n        arg(\"value2 (any, repeating)\", _t(\"Additional values to be returned if their corresponding conditions are TRUE.\")),\n    ],\n    compute: function (...values) {\n        assert(() => values.length % 2 === 0, _t(\"Wrong number of arguments. Expected an even number of arguments.\"));\n        for (let n = 0; n < values.length - 1; n += 2) {\n            if (toBoolean(values[n]?.value)) {\n                const result = values[n + 1];\n                if (result === undefined) {\n                    return { value: \"\" };\n                }\n                if (result.value === null) {\n                    result.value = \"\";\n                }\n                return result;\n            }\n        }\n        throw new EvaluationError(_t(\"No match.\"));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// NOT\n// -----------------------------------------------------------------------------\nconst NOT = {\n    description: _t(\"Returns opposite of provided logical value.\"),\n    args: [\n        arg(\"logical_expression (boolean)\", _t(\"An expression or reference to a cell holding an expression that represents some logical value.\")),\n    ],\n    compute: function (logicalExpression) {\n        return !toBoolean(logicalExpression);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// OR\n// -----------------------------------------------------------------------------\nconst OR = {\n    description: _t(\"Logical `or` operator.\"),\n    args: [\n        arg(\"logical_expression1 (boolean, range<boolean>)\", _t(\"An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.\")),\n        arg(\"logical_expression2 (boolean, range<boolean>, repeating)\", _t(\"More expressions that evaluate to logical values.\")),\n    ],\n    compute: function (...logicalExpressions) {\n        const { result, foundBoolean } = boolOr(logicalExpressions);\n        assert(() => foundBoolean, _t(\"[[FUNCTION_NAME]] has no valid input data.\"));\n        return result;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TRUE\n// -----------------------------------------------------------------------------\nconst TRUE = {\n    description: _t(\"Logical value `true`.\"),\n    args: [],\n    compute: function () {\n        return true;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// XOR\n// -----------------------------------------------------------------------------\nconst XOR = {\n    description: _t(\"Logical `xor` operator.\"),\n    args: [\n        arg(\"logical_expression1 (boolean, range<boolean>)\", _t(\"An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.\")),\n        arg(\"logical_expression2 (boolean, range<boolean>, repeating)\", _t(\"More expressions that evaluate to logical values.\")),\n    ],\n    compute: function (...logicalExpressions) {\n        let foundBoolean = false;\n        let acc = false;\n        conditionalVisitBoolean(logicalExpressions, (arg) => {\n            foundBoolean = true;\n            acc = acc ? !arg : arg;\n            return true; // no stop condition\n        });\n        assert(() => foundBoolean, _t(\"[[FUNCTION_NAME]] has no valid input data.\"));\n        return acc;\n    },\n    isExported: true,\n};\n\nvar logical = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    AND: AND,\n    FALSE: FALSE,\n    IF: IF,\n    IFERROR: IFERROR,\n    IFNA: IFNA,\n    IFS: IFS,\n    NOT: NOT,\n    OR: OR,\n    TRUE: TRUE,\n    XOR: XOR\n});\n\n/**\n * Get the pivot ID from the formula pivot ID.\n */\nfunction getPivotId(pivotFormulaId, getters) {\n    const pivotId = getters.getPivotId(pivotFormulaId);\n    if (!pivotId) {\n        throw new EvaluationError(_t('There is no pivot with id \"%s\"', pivotFormulaId));\n    }\n    return pivotId;\n}\nfunction assertMeasureExist(pivotId, measure, getters) {\n    const { measures } = getters.getPivotCoreDefinition(pivotId);\n    if (!measures.find((m) => m.id === measure)) {\n        const validMeasures = `(${measures.map((m) => m.id).join(\", \")})`;\n        throw new EvaluationError(_t(\"The argument %s is not a valid measure. Here are the measures: %s\", measure, validMeasures));\n    }\n}\nfunction assertDomainLength(domain) {\n    if (domain.length % 2 !== 0) {\n        throw new EvaluationError(_t(\"Function PIVOT takes an even number of arguments.\"));\n    }\n}\nfunction addPivotDependencies(evalContext, coreDefinition, forMeasures) {\n    //TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER\n    const dependencies = [];\n    if (coreDefinition.type === \"SPREADSHEET\" && coreDefinition.dataSet) {\n        const { sheetId, zone } = coreDefinition.dataSet;\n        const xc = zoneToXc(zone);\n        const range = evalContext.getters.getRangeFromSheetXC(sheetId, xc);\n        if (range === undefined || range.invalidXc || range.invalidSheetName) {\n            throw new InvalidReferenceError();\n        }\n        dependencies.push(range);\n    }\n    for (const measure of forMeasures) {\n        if (measure.computedBy) {\n            const formula = evalContext.getters.getMeasureCompiledFormula(measure);\n            dependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));\n        }\n    }\n    const originPosition = evalContext.__originCellPosition;\n    if (originPosition && dependencies.length) {\n        // The following line is used to reset the dependencies of the cell, to avoid\n        // keeping dependencies from previous evaluation of the PIVOT formula (i.e.\n        // in case the reference has been changed).\n        evalContext.updateDependencies?.(originPosition);\n        evalContext.addDependencies?.(originPosition, dependencies);\n    }\n}\n\nconst DEFAULT_IS_SORTED = true;\nconst DEFAULT_MATCH_MODE = 0;\nconst DEFAULT_SEARCH_MODE = 1;\nconst DEFAULT_ABSOLUTE_RELATIVE_MODE = 1;\nfunction valueNotAvailable(searchKey) {\n    return {\n        value: CellErrorType.NotAvailable,\n        message: _t(\"Did not find value '%s' in [[FUNCTION_NAME]] evaluation.\", toString(searchKey)),\n    };\n}\n// -----------------------------------------------------------------------------\n// ADDRESS\n// -----------------------------------------------------------------------------\nconst ADDRESS = {\n    description: _t(\"Returns a cell reference as a string. \"),\n    args: [\n        arg(\"row (number)\", _t(\"The row number of the cell reference. \")),\n        arg(\"column (number)\", _t(\"The column number (not name) of the cell reference. A is column number 1. \")),\n        arg(`absolute_relative_mode (number, default=${DEFAULT_ABSOLUTE_RELATIVE_MODE})`, _t(\"An indicator of whether the reference is row/column absolute. 1 is row and column absolute (e.g. $A$1), 2 is row absolute and column relative (e.g. A$1), 3 is row relative and column absolute (e.g. $A1), and 4 is row and column relative (e.g. A1).\")),\n        arg(\"use_a1_notation (boolean, default=TRUE)\", _t(\"A boolean indicating whether to use A1 style notation (TRUE) or R1C1 style notation (FALSE).\")),\n        arg(\"sheet (string, optional)\", _t(\"A string indicating the name of the sheet into which the address points.\")),\n    ],\n    compute: function (row, column, absoluteRelativeMode = { value: DEFAULT_ABSOLUTE_RELATIVE_MODE }, useA1Notation = { value: true }, sheet) {\n        const rowNumber = strictToInteger(row, this.locale);\n        const colNumber = strictToInteger(column, this.locale);\n        assertNumberGreaterThanOrEqualToOne(rowNumber);\n        assertNumberGreaterThanOrEqualToOne(colNumber);\n        const _absoluteRelativeMode = strictToInteger(absoluteRelativeMode, this.locale);\n        assert(() => [1, 2, 3, 4].includes(_absoluteRelativeMode), expectNumberRangeError(1, 4, _absoluteRelativeMode));\n        const _useA1Notation = toBoolean(useA1Notation);\n        let cellReference;\n        if (_useA1Notation) {\n            const rangePart = {\n                rowFixed: [1, 2].includes(_absoluteRelativeMode) ? true : false,\n                colFixed: [1, 3].includes(_absoluteRelativeMode) ? true : false,\n            };\n            cellReference = toXC(colNumber - 1, rowNumber - 1, rangePart);\n        }\n        else {\n            const rowPart = [1, 2].includes(_absoluteRelativeMode) ? `R${rowNumber}` : `R[${rowNumber}]`;\n            const colPart = [1, 3].includes(_absoluteRelativeMode) ? `C${colNumber}` : `C[${colNumber}]`;\n            cellReference = rowPart + colPart;\n        }\n        if (sheet !== undefined) {\n            return getFullReference(toString(sheet), cellReference);\n        }\n        return cellReference;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COLUMN\n// -----------------------------------------------------------------------------\nconst COLUMN = {\n    description: _t(\"Column number of a specified cell.\"),\n    args: [\n        arg(\"cell_reference (meta, default='this cell')\", _t(\"The cell whose column number will be returned. Column A corresponds to 1. By default, the function use the cell in which the formula is entered.\")),\n    ],\n    compute: function (cellReference) {\n        if (isEvaluationError(cellReference?.value)) {\n            throw cellReference;\n        }\n        const column = cellReference === undefined\n            ? this.__originCellPosition?.col\n            : toZone(cellReference.value).left;\n        assert(() => column !== undefined, \"In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter.\");\n        return column + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// COLUMNS\n// -----------------------------------------------------------------------------\nconst COLUMNS = {\n    description: _t(\"Number of columns in a specified array or range.\"),\n    args: [arg(\"range (meta)\", _t(\"The range whose column count will be returned.\"))],\n    compute: function (range) {\n        if (isEvaluationError(range?.value)) {\n            throw range;\n        }\n        const zone = toZone(range.value);\n        return zone.right - zone.left + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// HLOOKUP\n// -----------------------------------------------------------------------------\nconst HLOOKUP = {\n    description: _t(\"Horizontal lookup\"),\n    args: [\n        arg(\"search_key (string, number, boolean)\", _t(\"The value to search for. For example, 42, 'Cats', or I24.\")),\n        arg(\"range (range)\", _t(\"The range to consider for the search. The first row in the range is searched for the key specified in search_key.\")),\n        arg(\"index (number)\", _t(\"The row index of the value to be returned, where the first row in range is numbered 1.\")),\n        arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t(\"Indicates whether the row to be searched (the first row of the specified range) is sorted, in which case the closest match for search_key will be returned.\")),\n    ],\n    compute: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {\n        const _index = Math.trunc(toNumber(index?.value, this.locale));\n        assert(() => 1 <= _index && _index <= range[0].length, _t(\"[[FUNCTION_NAME]] evaluates to an out of bounds range.\"));\n        const getValueFromRange = (range, index) => range[index][0].value;\n        const _isSorted = toBoolean(isSorted.value);\n        const colIndex = _isSorted\n            ? dichotomicSearch(range, searchKey, \"nextSmaller\", \"asc\", range.length, getValueFromRange)\n            : linearSearch(range, searchKey, \"wildcard\", range.length, getValueFromRange);\n        const col = range[colIndex];\n        if (col === undefined) {\n            return valueNotAvailable(searchKey);\n        }\n        return col[_index - 1];\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// INDEX\n// -----------------------------------------------------------------------------\nconst INDEX = {\n    description: _t(\"Returns the content of a cell, specified by row and column offset.\"),\n    args: [\n        arg(\"reference (any, range)\", _t(\"The range of cells from which the values are returned.\")),\n        arg(\"row (number, default=0)\", _t(\"The index of the row to be returned from within the reference range of cells.\")),\n        arg(\"column (number, default=0)\", _t(\"The index of the column to be returned from within the reference range of cells.\")),\n    ],\n    compute: function (reference, row = { value: 0 }, column = { value: 0 }) {\n        const _reference = toMatrix(reference);\n        const _row = toNumber(row.value, this.locale);\n        const _column = toNumber(column.value, this.locale);\n        assert(() => _column >= 0 &&\n            _column - 1 < _reference.length &&\n            _row >= 0 &&\n            _row - 1 < _reference[0].length, _t(\"Index out of range.\"));\n        if (_row === 0 && _column === 0) {\n            return _reference;\n        }\n        if (_row === 0) {\n            return [_reference[_column - 1]];\n        }\n        if (_column === 0) {\n            return _reference.map((col) => [col[_row - 1]]);\n        }\n        return _reference[_column - 1][_row - 1];\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// INDIRECT\n// -----------------------------------------------------------------------------\nconst INDIRECT = {\n    description: _t(\"Returns the content of a cell, specified by a string.\"),\n    args: [\n        arg(\"reference (string)\", _t(\"The range of cells from which the values are returned.\")),\n        arg(\"use_a1_notation (boolean, default=TRUE)\", _t(\"A boolean indicating whether to use A1 style notation (TRUE) or R1C1 style notation (FALSE).\")),\n    ],\n    compute: function (reference, useA1Notation = { value: true }) {\n        let _reference = reference?.value?.toString();\n        if (!_reference) {\n            throw new InvalidReferenceError(_t(\"Reference should be defined.\"));\n        }\n        const _useA1Notation = toBoolean(useA1Notation);\n        if (!_useA1Notation) {\n            throw new EvaluationError(_t(\"R1C1 notation is not supported.\"));\n        }\n        const sheetId = this.__originSheetId;\n        const originPosition = this.__originCellPosition;\n        if (originPosition) {\n            // The following line is used to reset the dependencies of the cell, to avoid\n            // keeping dependencies from previous evaluation of the INDIRECT formula (i.e.\n            // in case the reference has been changed).\n            this.updateDependencies?.(originPosition);\n        }\n        const range = this.getters.getRangeFromSheetXC(sheetId, _reference);\n        if (range === undefined || range.invalidXc || range.invalidSheetName) {\n            throw new InvalidReferenceError();\n        }\n        if (originPosition) {\n            this.addDependencies?.(originPosition, [range]);\n        }\n        const values = [];\n        for (let col = range.zone.left; col <= range.zone.right; col++) {\n            const colValues = [];\n            for (let row = range.zone.top; row <= range.zone.bottom; row++) {\n                const position = { sheetId: range.sheetId, col, row };\n                colValues.push(this.getters.getEvaluatedCell(position));\n            }\n            values.push(colValues);\n        }\n        return values.length === 1 && values[0].length === 1 ? values[0][0] : values;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// LOOKUP\n// -----------------------------------------------------------------------------\nconst LOOKUP = {\n    description: _t(\"Look up a value.\"),\n    args: [\n        arg(\"search_key (string, number, boolean)\", _t(\"The value to search for. For example, 42, 'Cats', or I24.\")),\n        arg(\"search_array (range)\", _t(\"One method of using this function is to provide a single sorted row or column search_array to look through for the search_key with a second argument result_range. The other way is to combine these two arguments into one search_array where the first row or column is searched and a value is returned from the last row or column in the array. If search_key is not found, a non-exact match may be returned.\")),\n        arg(\"result_range (range, optional)\", _t(\"The range from which to return a result. The value returned corresponds to the location where search_key is found in search_range. This range must be only a single row or column and should not be used if using the search_result_array method.\")),\n    ],\n    compute: function (searchKey, searchArray, resultRange) {\n        let nbCol = searchArray.length;\n        let nbRow = searchArray[0].length;\n        const verticalSearch = nbRow >= nbCol;\n        const getElement = verticalSearch\n            ? (range, index) => range[0][index].value\n            : (range, index) => range[index][0].value;\n        const rangeLength = verticalSearch ? nbRow : nbCol;\n        const index = dichotomicSearch(searchArray, searchKey, \"nextSmaller\", \"asc\", rangeLength, getElement);\n        if (index === -1 ||\n            (verticalSearch && searchArray[0][index] === undefined) ||\n            (!verticalSearch && searchArray[index][nbRow - 1] === undefined)) {\n            return valueNotAvailable(searchKey);\n        }\n        if (resultRange === undefined) {\n            return verticalSearch ? searchArray[nbCol - 1][index] : searchArray[index][nbRow - 1];\n        }\n        nbCol = resultRange.length;\n        nbRow = resultRange[0].length;\n        assert(() => nbCol === 1 || nbRow === 1, _t(\"The result_range must be a single row or a single column.\"));\n        if (nbCol > 1) {\n            assert(() => index <= nbCol - 1, _t(\"[[FUNCTION_NAME]] evaluates to an out of range row value %s.\", (index + 1).toString()));\n            return resultRange[index][0];\n        }\n        assert(() => index <= nbRow - 1, _t(\"[[FUNCTION_NAME]] evaluates to an out of range column value %s.\", (index + 1).toString()));\n        return resultRange[0][index];\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MATCH\n// -----------------------------------------------------------------------------\nconst DEFAULT_SEARCH_TYPE = 1;\nconst MATCH = {\n    description: _t(\"Position of item in range that matches value.\"),\n    args: [\n        arg(\"search_key (string, number, boolean)\", _t(\"The value to search for. For example, 42, 'Cats', or I24.\")),\n        arg(\"range (any, range)\", _t(\"The one-dimensional array to be searched.\")),\n        arg(`search_type (number, default=${DEFAULT_SEARCH_TYPE})`, _t(\"The search method. 1 (default) finds the largest value less than or equal to search_key when range is sorted in ascending order. 0 finds the exact value when range is unsorted. -1 finds the smallest value greater than or equal to search_key when range is sorted in descending order.\")),\n    ],\n    compute: function (searchKey, range, searchType = { value: DEFAULT_SEARCH_TYPE }) {\n        let _searchType = toNumber(searchType, this.locale);\n        const nbCol = range.length;\n        const nbRow = range[0].length;\n        assert(() => nbCol === 1 || nbRow === 1, _t(\"The range must be a single row or a single column.\"));\n        let index = -1;\n        const getElement = nbCol === 1\n            ? (range, index) => range[0][index].value\n            : (range, index) => range[index][0].value;\n        const rangeLen = nbCol === 1 ? range[0].length : range.length;\n        _searchType = Math.sign(_searchType);\n        switch (_searchType) {\n            case 1:\n                index = dichotomicSearch(range, searchKey, \"nextSmaller\", \"asc\", rangeLen, getElement);\n                break;\n            case 0:\n                index = linearSearch(range, searchKey, \"wildcard\", rangeLen, getElement);\n                break;\n            case -1:\n                index = dichotomicSearch(range, searchKey, \"nextGreater\", \"desc\", rangeLen, getElement);\n                break;\n        }\n        if ((nbCol === 1 && range[0][index] === undefined) ||\n            (nbCol !== 1 && range[index] === undefined)) {\n            return valueNotAvailable(searchKey);\n        }\n        return index + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ROW\n// -----------------------------------------------------------------------------\nconst ROW = {\n    description: _t(\"Row number of a specified cell.\"),\n    args: [\n        arg(\"cell_reference (meta, default='this cell')\", _t(\"The cell whose row number will be returned. By default, this function uses the cell in which the formula is entered.\")),\n    ],\n    compute: function (cellReference) {\n        if (isEvaluationError(cellReference?.value)) {\n            throw cellReference;\n        }\n        const row = cellReference === undefined\n            ? this.__originCellPosition?.row\n            : toZone(cellReference.value).top;\n        assert(() => row !== undefined, \"In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter.\");\n        return row + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// ROWS\n// -----------------------------------------------------------------------------\nconst ROWS = {\n    description: _t(\"Number of rows in a specified array or range.\"),\n    args: [arg(\"range (meta)\", _t(\"The range whose row count will be returned.\"))],\n    compute: function (range) {\n        if (isEvaluationError(range?.value)) {\n            throw range;\n        }\n        const zone = toZone(range.value);\n        return zone.bottom - zone.top + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// VLOOKUP\n// -----------------------------------------------------------------------------\nconst VLOOKUP = {\n    description: _t(\"Vertical lookup.\"),\n    args: [\n        arg(\"search_key (string, number, boolean)\", _t(\"The value to search for. For example, 42, 'Cats', or I24.\")),\n        arg(\"range (any, range)\", _t(\"The range to consider for the search. The first column in the range is searched for the key specified in search_key.\")),\n        arg(\"index (number)\", _t(\"The column index of the value to be returned, where the first column in range is numbered 1.\")),\n        arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t(\"Indicates whether the column to be searched (the first column of the specified range) is sorted, in which case the closest match for search_key will be returned.\")),\n    ],\n    compute: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {\n        const _index = Math.trunc(toNumber(index?.value, this.locale));\n        assert(() => 1 <= _index && _index <= range.length, _t(\"[[FUNCTION_NAME]] evaluates to an out of bounds range.\"));\n        const getValueFromRange = (range, index) => range[0][index].value;\n        const _isSorted = toBoolean(isSorted.value);\n        const rowIndex = _isSorted\n            ? dichotomicSearch(range, searchKey, \"nextSmaller\", \"asc\", range[0].length, getValueFromRange)\n            : linearSearch(range, searchKey, \"wildcard\", range[0].length, getValueFromRange);\n        const value = range[_index - 1][rowIndex];\n        if (value === undefined) {\n            return valueNotAvailable(searchKey);\n        }\n        return value;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// XLOOKUP\n// -----------------------------------------------------------------------------\nconst MATCH_MODE = {\n    \"0\": \"strict\",\n    \"1\": \"nextGreater\",\n    \"-1\": \"nextSmaller\",\n    \"2\": \"wildcard\",\n};\nconst XLOOKUP = {\n    description: _t(\"Search a range for a match and return the corresponding item from a second range.\"),\n    args: [\n        arg(\"search_key (string,number,boolean)\", _t(\"The value to search for.\")),\n        arg(\"lookup_range (any, range)\", _t(\"The range to consider for the search. Should be a single column or a single row.\")),\n        arg(\"return_range (any, range)\", _t(\"The range containing the return value. Should have the same dimensions as lookup_range.\")),\n        arg(\"if_not_found (any, optional)\", _t(\"If a valid match is not found, return this value.\")),\n        arg(`match_mode (any, default=${DEFAULT_MATCH_MODE})`, _t(\"(0) Exact match. \\\n        (-1) Return next smaller item if no match. \\\n        (1) Return next greater item if no match. \\\n        (2) Wildcard match.\")),\n        arg(`search_mode (any, default=${DEFAULT_SEARCH_MODE})`, _t(\"(1) Search starting at first item. \\\n      (-1) Search starting at last item. \\\n      (2) Perform a binary search that relies on lookup_array being sorted in ascending order. If not sorted, invalid results will be returned. \\\n      (-2) Perform a binary search that relies on lookup_array being sorted in descending order. If not sorted, invalid results will be returned.\\\n      \")),\n    ],\n    compute: function (searchKey, lookupRange, returnRange, defaultValue, matchMode = { value: DEFAULT_MATCH_MODE }, searchMode = { value: DEFAULT_SEARCH_MODE }) {\n        const _matchMode = Math.trunc(toNumber(matchMode.value, this.locale));\n        const _searchMode = Math.trunc(toNumber(searchMode.value, this.locale));\n        assert(() => lookupRange.length === 1 || lookupRange[0].length === 1, _t(\"lookup_range should be either a single row or single column.\"));\n        assert(() => [-1, 1, -2, 2].includes(_searchMode), _t(\"search_mode should be a value in [-1, 1, -2, 2].\"));\n        assert(() => [-1, 0, 1, 2].includes(_matchMode), _t(\"match_mode should be a value in [-1, 0, 1, 2].\"));\n        const lookupDirection = lookupRange.length === 1 ? \"col\" : \"row\";\n        assert(() => !(_matchMode === 2 && [-2, 2].includes(_searchMode)), _t(\"the search and match mode combination is not supported for XLOOKUP evaluation.\"));\n        assert(() => lookupDirection === \"col\"\n            ? returnRange[0].length === lookupRange[0].length\n            : returnRange.length === lookupRange.length, _t(\"return_range should have the same dimensions as lookup_range.\"));\n        const getElement = lookupDirection === \"col\"\n            ? (range, index) => range[0][index].value\n            : (range, index) => range[index][0].value;\n        const rangeLen = lookupDirection === \"col\" ? lookupRange[0].length : lookupRange.length;\n        const mode = MATCH_MODE[_matchMode];\n        const reverseSearch = _searchMode === -1;\n        const index = _searchMode === 2 || _searchMode === -2\n            ? dichotomicSearch(lookupRange, searchKey, mode, _searchMode === 2 ? \"asc\" : \"desc\", rangeLen, getElement)\n            : linearSearch(lookupRange, searchKey, mode, rangeLen, getElement, reverseSearch);\n        if (index !== -1) {\n            return lookupDirection === \"col\"\n                ? returnRange.map((col) => [col[index]])\n                : [returnRange[index]];\n        }\n        if (defaultValue === undefined) {\n            return valueNotAvailable(searchKey);\n        }\n        return [[defaultValue]];\n    },\n    isExported: true,\n};\n//--------------------------------------------------------------------------\n// Pivot functions\n//--------------------------------------------------------------------------\n// PIVOT.VALUE\nconst PIVOT_VALUE = {\n    description: _t(\"Get the value from a pivot.\"),\n    args: [\n        arg(\"pivot_id (number,string)\", _t(\"ID of the pivot.\")),\n        arg(\"measure_name (string)\", _t(\"Name of the measure.\")),\n        arg(\"domain_field_name (string,optional,repeating)\", _t(\"Field name.\")),\n        arg(\"domain_value (number,string,boolean,optional,repeating)\", _t(\"Value.\")),\n    ],\n    compute: function (formulaId, measureName, ...domainArgs) {\n        const _pivotFormulaId = toString(formulaId);\n        const _measure = toString(measureName);\n        const pivotId = getPivotId(_pivotFormulaId, this.getters);\n        assertMeasureExist(pivotId, _measure, this.getters);\n        assertDomainLength(domainArgs);\n        const pivot = this.getters.getPivot(pivotId);\n        const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);\n        addPivotDependencies(this, coreDefinition, coreDefinition.measures.filter((m) => m.id === _measure));\n        pivot.init({ reload: pivot.needsReevaluation });\n        const error = pivot.assertIsValid({ throwOnError: false });\n        if (error) {\n            return error;\n        }\n        if (!pivot.areDomainArgsFieldsValid(domainArgs)) {\n            const suggestion = _t(\"Consider using a dynamic pivot formula: %s. Or re-insert the static pivot from the Data menu.\", `=PIVOT(${_pivotFormulaId})`);\n            return {\n                value: CellErrorType.GenericError,\n                message: _t(\"Dimensions don't match the pivot definition\") + \". \" + suggestion,\n            };\n        }\n        const domain = pivot.parseArgsToPivotDomain(domainArgs);\n        return pivot.getPivotCellValueAndFormat(_measure, domain);\n    },\n};\n// PIVOT.HEADER\nconst PIVOT_HEADER = {\n    description: _t(\"Get the header of a pivot.\"),\n    args: [\n        arg(\"pivot_id (number,string)\", _t(\"ID of the pivot.\")),\n        arg(\"domain_field_name (string,optional,repeating)\", _t(\"Field name.\")),\n        arg(\"domain_value (number,string,value,optional,repeating)\", _t(\"Value.\")),\n    ],\n    compute: function (pivotId, ...domainArgs) {\n        const _pivotFormulaId = toString(pivotId);\n        const _pivotId = getPivotId(_pivotFormulaId, this.getters);\n        assertDomainLength(domainArgs);\n        const pivot = this.getters.getPivot(_pivotId);\n        const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);\n        addPivotDependencies(this, coreDefinition, []);\n        pivot.init({ reload: pivot.needsReevaluation });\n        const error = pivot.assertIsValid({ throwOnError: false });\n        if (error) {\n            return error;\n        }\n        if (!pivot.areDomainArgsFieldsValid(domainArgs)) {\n            const suggestion = _t(\"Consider using a dynamic pivot formula: %s. Or re-insert the static pivot from the Data menu.\", `=PIVOT(${_pivotFormulaId})`);\n            return {\n                value: CellErrorType.GenericError,\n                message: _t(\"Dimensions don't match the pivot definition\") + \". \" + suggestion,\n            };\n        }\n        const domain = pivot.parseArgsToPivotDomain(domainArgs);\n        const lastNode = domain.at(-1);\n        if (lastNode?.field === \"measure\") {\n            return pivot.getPivotMeasureValue(toString(lastNode.value), domain);\n        }\n        const { value, format } = pivot.getPivotHeaderValueAndFormat(domain);\n        return {\n            value,\n            format: !lastNode || lastNode.field === \"measure\" || lastNode.value === \"false\"\n                ? undefined\n                : format,\n        };\n    },\n};\nconst PIVOT = {\n    description: _t(\"Get a pivot table.\"),\n    args: [\n        arg(\"pivot_id (string)\", _t(\"ID of the pivot.\")),\n        arg(\"row_count (number, optional)\", _t(\"number of rows\")),\n        arg(\"include_total (boolean, default=TRUE)\", _t(\"Whether to include total/sub-totals or not.\")),\n        arg(\"include_column_titles (boolean, default=TRUE)\", _t(\"Whether to include the column titles or not.\")),\n        arg(\"column_count (number, optional)\", _t(\"number of columns\")),\n    ],\n    compute: function (pivotFormulaId, rowCount = { value: 10000 }, includeTotal = { value: true }, includeColumnHeaders = { value: true }, columnCount = { value: Number.MAX_VALUE }) {\n        const _pivotFormulaId = toString(pivotFormulaId);\n        const _rowCount = toNumber(rowCount, this.locale);\n        if (_rowCount < 0) {\n            throw new EvaluationError(_t(\"The number of rows must be positive.\"));\n        }\n        const _columnCount = toNumber(columnCount, this.locale);\n        if (_columnCount < 0) {\n            throw new EvaluationError(_t(\"The number of columns must be positive.\"));\n        }\n        const _includeColumnHeaders = toBoolean(includeColumnHeaders);\n        const _includedTotal = toBoolean(includeTotal);\n        const pivotId = getPivotId(_pivotFormulaId, this.getters);\n        const pivot = this.getters.getPivot(pivotId);\n        const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);\n        addPivotDependencies(this, coreDefinition, coreDefinition.measures);\n        pivot.init({ reload: pivot.needsReevaluation });\n        const error = pivot.assertIsValid({ throwOnError: false });\n        if (error) {\n            return error;\n        }\n        const table = pivot.getTableStructure();\n        const cells = table.getPivotCells(_includedTotal, _includeColumnHeaders);\n        const headerRows = _includeColumnHeaders ? table.columns.length : 0;\n        const pivotTitle = this.getters.getPivotDisplayName(pivotId);\n        const tableHeight = Math.min(headerRows + _rowCount, cells[0].length);\n        if (tableHeight === 0) {\n            return [[{ value: pivotTitle }]];\n        }\n        const tableWidth = Math.min(1 + _columnCount, cells.length);\n        const result = [];\n        for (const col of range(0, tableWidth)) {\n            result[col] = [];\n            for (const row of range(0, tableHeight)) {\n                const pivotCell = cells[col][row];\n                switch (pivotCell.type) {\n                    case \"EMPTY\":\n                        result[col].push({ value: \"\" });\n                        break;\n                    case \"HEADER\":\n                        const valueAndFormat = pivot.getPivotHeaderValueAndFormat(pivotCell.domain);\n                        result[col].push(addIndentAndAlignToPivotHeader(pivot, pivotCell.domain, valueAndFormat));\n                        break;\n                    case \"MEASURE_HEADER\":\n                        result[col].push(pivot.getPivotMeasureValue(pivotCell.measure, pivotCell.domain));\n                        break;\n                    case \"VALUE\":\n                        result[col].push(pivot.getPivotCellValueAndFormat(pivotCell.measure, pivotCell.domain));\n                        break;\n                }\n            }\n        }\n        if (_includeColumnHeaders) {\n            result[0][0] = { value: pivotTitle };\n        }\n        return result;\n    },\n};\n//--------------------------------------------------------------------------\n// OFFSET\n//--------------------------------------------------------------------------\nconst OFFSET = {\n    description: _t(\"Returns a range reference shifted by a specified number of rows and columns from a starting cell reference.\"),\n    args: [\n        arg(\"cell_reference (meta)\", _t(\"The starting point from which to count the offset rows and columns.\")),\n        arg(\"offset_rows (number)\", _t(\"The number of rows to offset by.\")),\n        arg(\"offset_columns (number)\", _t(\"The number of columns to offset by.\")),\n        arg(\"height (number, default='height of cell_reference')\", _t(\"The number of rows of the range to return starting at the offset target.\")),\n        arg(\"width (number, default='width of cell_reference')\", _t(\"The number of columns of the range to return starting at the offset target.\")),\n    ],\n    compute: function (cellReference, offsetRows, offsetColumns, height, width) {\n        if (isEvaluationError(cellReference?.value)) {\n            return cellReference;\n        }\n        const _cellReference = cellReference?.value;\n        if (!_cellReference) {\n            throw new Error(\"In this context, the function OFFSET needs to have a cell or range in parameter.\");\n        }\n        const zone = toZone(_cellReference);\n        let offsetHeight = zone.bottom - zone.top + 1;\n        let offsetWidth = zone.right - zone.left + 1;\n        if (height) {\n            const _height = toNumber(height, this.locale);\n            assertPositive(_t(\"Height value is %(_height)s. It should be greater than or equal to 1.\", { _height }), _height);\n            offsetHeight = _height;\n        }\n        if (width) {\n            const _width = toNumber(width, this.locale);\n            assertPositive(_t(\"Width value is %(_width)s. It should be greater than or equal to 1.\", { _width }), _width);\n            offsetWidth = _width;\n        }\n        const { sheetName } = splitReference(_cellReference);\n        const sheetId = (sheetName && this.getters.getSheetIdByName(sheetName)) || this.getters.getActiveSheetId();\n        const _offsetRows = toNumber(offsetRows, this.locale);\n        const _offsetColumns = toNumber(offsetColumns, this.locale);\n        const originPosition = this.__originCellPosition;\n        if (originPosition) {\n            this.updateDependencies?.(originPosition);\n        }\n        const startingCol = zone.left + _offsetColumns;\n        const startingRow = zone.top + _offsetRows;\n        if (startingCol < 0 || startingRow < 0) {\n            return new InvalidReferenceError(_t(\"OFFSET evaluates to an out of bounds range.\"));\n        }\n        const dependencyZone = {\n            left: startingCol,\n            top: startingRow,\n            right: startingCol + offsetWidth - 1,\n            bottom: startingRow + offsetHeight - 1,\n        };\n        const range = this.getters.getRangeFromZone(this.__originSheetId, dependencyZone);\n        if (range.invalidXc || range.invalidSheetName) {\n            return new InvalidReferenceError();\n        }\n        if (originPosition) {\n            this.addDependencies?.(originPosition, [range]);\n        }\n        return generateMatrix(offsetWidth, offsetHeight, (col, row) => this.getters.getEvaluatedCell({\n            sheetId,\n            col: startingCol + col,\n            row: startingRow + row,\n        }));\n    },\n};\n\nvar lookup = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    ADDRESS: ADDRESS,\n    COLUMN: COLUMN,\n    COLUMNS: COLUMNS,\n    HLOOKUP: HLOOKUP,\n    INDEX: INDEX,\n    INDIRECT: INDIRECT,\n    LOOKUP: LOOKUP,\n    MATCH: MATCH,\n    OFFSET: OFFSET,\n    PIVOT: PIVOT,\n    PIVOT_HEADER: PIVOT_HEADER,\n    PIVOT_VALUE: PIVOT_VALUE,\n    ROW: ROW,\n    ROWS: ROWS,\n    VLOOKUP: VLOOKUP,\n    XLOOKUP: XLOOKUP\n});\n\n// -----------------------------------------------------------------------------\n// ADD\n// -----------------------------------------------------------------------------\nconst ADD = {\n    description: _t(\"Sum of two numbers.\"),\n    args: [\n        arg(\"value1 (number)\", _t(\"The first addend.\")),\n        arg(\"value2 (number)\", _t(\"The second addend.\")),\n    ],\n    compute: function (value1, value2) {\n        return {\n            value: toNumber(value1, this.locale) + toNumber(value2, this.locale),\n            format: value1?.format || value2?.format,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// CONCAT\n// -----------------------------------------------------------------------------\nconst CONCAT = {\n    description: _t(\"Concatenation of two values.\"),\n    args: [\n        arg(\"value1 (string)\", _t(\"The value to which value2 will be appended.\")),\n        arg(\"value2 (string)\", _t(\"The value to append to value1.\")),\n    ],\n    compute: function (value1, value2) {\n        return toString(value1) + toString(value2);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// DIVIDE\n// -----------------------------------------------------------------------------\nconst DIVIDE = {\n    description: _t(\"One number divided by another.\"),\n    args: [\n        arg(\"dividend (number)\", _t(\"The number to be divided.\")),\n        arg(\"divisor (number)\", _t(\"The number to divide by.\")),\n    ],\n    compute: function (dividend, divisor) {\n        const _divisor = toNumber(divisor, this.locale);\n        assert(() => _divisor !== 0, _t(\"The divisor must be different from zero.\"), CellErrorType.DivisionByZero);\n        return {\n            value: toNumber(dividend, this.locale) / _divisor,\n            format: dividend?.format || divisor?.format,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// EQ\n// -----------------------------------------------------------------------------\nfunction isEmpty(data) {\n    return data === undefined || data.value === null;\n}\nconst getNeutral = { number: 0, string: \"\", boolean: false };\nconst EQ = {\n    description: _t(\"Equal.\"),\n    args: [\n        arg(\"value1 (string, number, boolean)\", _t(\"The first value.\")),\n        arg(\"value2 (string, number, boolean)\", _t(\"The value to test against value1 for equality.\")),\n    ],\n    compute: function (value1, value2) {\n        let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;\n        let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;\n        if (typeof _value1 === \"string\") {\n            _value1 = _value1.toUpperCase();\n        }\n        if (typeof _value2 === \"string\") {\n            _value2 = _value2.toUpperCase();\n        }\n        return _value1 === _value2;\n    },\n};\n// -----------------------------------------------------------------------------\n// GT\n// -----------------------------------------------------------------------------\nfunction applyRelationalOperator(value1, value2, cb) {\n    let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;\n    let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;\n    if (isEvaluationError(_value1)) {\n        throw value1;\n    }\n    if (isEvaluationError(_value2)) {\n        throw value2;\n    }\n    if (typeof _value1 !== \"number\") {\n        _value1 = toString(_value1).toUpperCase();\n    }\n    if (typeof _value2 !== \"number\") {\n        _value2 = toString(_value2).toUpperCase();\n    }\n    const tV1 = typeof _value1;\n    const tV2 = typeof _value2;\n    if (tV1 === \"string\" && tV2 === \"number\") {\n        return true;\n    }\n    if (tV2 === \"string\" && tV1 === \"number\") {\n        return false;\n    }\n    return cb(_value1, _value2);\n}\nconst GT = {\n    description: _t(\"Strictly greater than.\"),\n    args: [\n        arg(\"value1 (number, string, boolean)\", _t(\"The value to test as being greater than value2.\")),\n        arg(\"value2 (number, string, boolean)\", _t(\"The second value.\")),\n    ],\n    compute: function (value1, value2) {\n        return applyRelationalOperator(value1, value2, (v1, v2) => {\n            return v1 > v2;\n        });\n    },\n};\n// -----------------------------------------------------------------------------\n// GTE\n// -----------------------------------------------------------------------------\nconst GTE = {\n    description: _t(\"Greater than or equal to.\"),\n    args: [\n        arg(\"value1 (number, string, boolean)\", _t(\"The value to test as being greater than or equal to value2.\")),\n        arg(\"value2 (number, string, boolean)\", _t(\"The second value.\")),\n    ],\n    compute: function (value1, value2) {\n        return applyRelationalOperator(value1, value2, (v1, v2) => {\n            return v1 >= v2;\n        });\n    },\n};\n// -----------------------------------------------------------------------------\n// LT\n// -----------------------------------------------------------------------------\nconst LT = {\n    description: _t(\"Less than.\"),\n    args: [\n        arg(\"value1 (number, string, boolean)\", _t(\"The value to test as being less than value2.\")),\n        arg(\"value2 (number, string, boolean)\", _t(\"The second value.\")),\n    ],\n    compute: function (value1, value2) {\n        return !GTE.compute.bind(this)(value1, value2);\n    },\n};\n// -----------------------------------------------------------------------------\n// LTE\n// -----------------------------------------------------------------------------\nconst LTE = {\n    description: _t(\"Less than or equal to.\"),\n    args: [\n        arg(\"value1 (number, string, boolean)\", _t(\"The value to test as being less than or equal to value2.\")),\n        arg(\"value2 (number, string, boolean)\", _t(\"The second value.\")),\n    ],\n    compute: function (value1, value2) {\n        return !GT.compute.bind(this)(value1, value2);\n    },\n};\n// -----------------------------------------------------------------------------\n// MINUS\n// -----------------------------------------------------------------------------\nconst MINUS = {\n    description: _t(\"Difference of two numbers.\"),\n    args: [\n        arg(\"value1 (number)\", _t(\"The minuend, or number to be subtracted from.\")),\n        arg(\"value2 (number)\", _t(\"The subtrahend, or number to subtract from value1.\")),\n    ],\n    compute: function (value1, value2) {\n        return {\n            value: toNumber(value1, this.locale) - toNumber(value2, this.locale),\n            format: value1?.format || value2?.format,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// MULTIPLY\n// -----------------------------------------------------------------------------\nconst MULTIPLY = {\n    description: _t(\"Product of two numbers\"),\n    args: [\n        arg(\"factor1 (number)\", _t(\"The first multiplicand.\")),\n        arg(\"factor2 (number)\", _t(\"The second multiplicand.\")),\n    ],\n    compute: function (factor1, factor2) {\n        return {\n            value: toNumber(factor1, this.locale) * toNumber(factor2, this.locale),\n            format: factor1?.format || factor2?.format,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// NE\n// -----------------------------------------------------------------------------\nconst NE = {\n    description: _t(\"Not equal.\"),\n    args: [\n        arg(\"value1 (string, number, boolean)\", _t(\"The first value.\")),\n        arg(\"value2 (string, number, boolean)\", _t(\"The value to test against value1 for inequality.\")),\n    ],\n    compute: function (value1, value2) {\n        return !EQ.compute.bind(this)(value1, value2);\n    },\n};\n// -----------------------------------------------------------------------------\n// POW\n// -----------------------------------------------------------------------------\nconst POW = {\n    description: _t(\"A number raised to a power.\"),\n    args: [\n        arg(\"base (number)\", _t(\"The number to raise to the exponent power.\")),\n        arg(\"exponent (number)\", _t(\"The exponent to raise base to.\")),\n    ],\n    compute: function (base, exponent) {\n        return POWER.compute.bind(this)(base, exponent);\n    },\n};\n// -----------------------------------------------------------------------------\n// UMINUS\n// -----------------------------------------------------------------------------\nconst UMINUS = {\n    description: _t(\"A number with the sign reversed.\"),\n    args: [\n        arg(\"value (number)\", _t(\"The number to have its sign reversed. Equivalently, the number to multiply by -1.\")),\n    ],\n    compute: function (value) {\n        return {\n            value: -toNumber(value, this.locale),\n            format: value?.format,\n        };\n    },\n};\n// -----------------------------------------------------------------------------\n// UNARY_PERCENT\n// -----------------------------------------------------------------------------\nconst UNARY_PERCENT = {\n    description: _t(\"Value interpreted as a percentage.\"),\n    args: [arg(\"percentage (number)\", _t(\"The value to interpret as a percentage.\"))],\n    compute: function (percentage) {\n        return toNumber(percentage, this.locale) / 100;\n    },\n};\n// -----------------------------------------------------------------------------\n// UPLUS\n// -----------------------------------------------------------------------------\nconst UPLUS = {\n    description: _t(\"A specified number, unchanged.\"),\n    args: [arg(\"value (any)\", _t(\"The number to return.\"))],\n    compute: function (value = { value: null }) {\n        return value;\n    },\n};\n\nvar operators = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    ADD: ADD,\n    CONCAT: CONCAT,\n    DIVIDE: DIVIDE,\n    EQ: EQ,\n    GT: GT,\n    GTE: GTE,\n    LT: LT,\n    LTE: LTE,\n    MINUS: MINUS,\n    MULTIPLY: MULTIPLY,\n    NE: NE,\n    POW: POW,\n    UMINUS: UMINUS,\n    UNARY_PERCENT: UNARY_PERCENT,\n    UPLUS: UPLUS\n});\n\nconst transformFromFactor = (factor) => ({\n    transform: (x) => x * factor,\n    inverseTransform: (x) => x / factor,\n});\nconst standard = { transform: (x) => x, inverseTransform: (x) => x };\nconst ANG2M = 1e-10;\nconst IN2M = 0.0254;\nconst PICAPT2M = IN2M / 72;\nconst FT2M = 0.3048;\nconst YD2M = 0.9144;\nconst MI2M = 1609.34;\nconst NMI2M = 1852;\nconst LY2M = 9.46073047258e15;\nconst UNITS = {\n    // WEIGHT UNITs : Standard = gramme\n    g: { ...standard, category: \"weight\" },\n    u: { ...transformFromFactor(1.66053e-24), category: \"weight\" },\n    grain: { ...transformFromFactor(0.0647989), category: \"weight\" },\n    ozm: { ...transformFromFactor(28.3495), category: \"weight\" },\n    lbm: { ...transformFromFactor(453.592), category: \"weight\" },\n    stone: { ...transformFromFactor(6350.29), category: \"weight\" },\n    sg: { ...transformFromFactor(14593.90294), category: \"weight\" },\n    cwt: { ...transformFromFactor(45359.237), category: \"weight\" },\n    uk_cwt: { ...transformFromFactor(50802.3), category: \"weight\" },\n    ton: { ...transformFromFactor(907184.74), category: \"weight\" },\n    uk_ton: { ...transformFromFactor(1016046.9), category: \"weight\" },\n    // DISTANCE UNITS : Standard = meter\n    m: { ...standard, category: \"distance\" },\n    km: { ...transformFromFactor(1000), category: \"distance\" },\n    ang: { ...transformFromFactor(ANG2M), category: \"distance\" },\n    Picapt: { ...transformFromFactor(PICAPT2M), category: \"distance\" },\n    pica: { ...transformFromFactor(IN2M / 6), category: \"distance\" },\n    in: { ...transformFromFactor(IN2M), category: \"distance\" },\n    ft: { ...transformFromFactor(FT2M), category: \"distance\" },\n    yd: { ...transformFromFactor(YD2M), category: \"distance\" },\n    ell: { ...transformFromFactor(1.143), category: \"distance\" },\n    mi: { ...transformFromFactor(MI2M), category: \"distance\" },\n    survey_mi: { ...transformFromFactor(1609.34), category: \"distance\" },\n    Nmi: { ...transformFromFactor(NMI2M), category: \"distance\" },\n    ly: { ...transformFromFactor(LY2M), category: \"distance\" },\n    parsec: { ...transformFromFactor(3.0856775814914e16), category: \"distance\" },\n    // TIME UNITS : Standard = second\n    sec: { ...standard, category: \"time\" },\n    min: { ...transformFromFactor(60), category: \"time\" },\n    hr: { ...transformFromFactor(3600), category: \"time\" },\n    day: { ...transformFromFactor(86400), category: \"time\" },\n    yr: { ...transformFromFactor(31556952), category: \"time\" },\n    // PRESSURE UNITS : Standard = Pascal\n    Pa: { ...standard, category: \"pressure\" },\n    bar: { ...transformFromFactor(100000), category: \"pressure\" },\n    mmHg: { ...transformFromFactor(133.322), category: \"pressure\" },\n    Torr: { ...transformFromFactor(133.322), category: \"pressure\" },\n    psi: { ...transformFromFactor(6894.76), category: \"pressure\" },\n    atm: { ...transformFromFactor(101325), category: \"pressure\" },\n    // FORCE UNITS : Standard = Newton\n    N: { ...standard, category: \"force\" },\n    dyn: { ...transformFromFactor(1e-5), category: \"force\" },\n    pond: { ...transformFromFactor(0.00980665), category: \"force\" },\n    lbf: { ...transformFromFactor(4.44822), category: \"force\" },\n    // ENERGY UNITS : Standard = Joule\n    J: { ...standard, category: \"energy\" },\n    eV: { ...transformFromFactor(1.60218e-19), category: \"energy\" },\n    e: { ...transformFromFactor(1e-7), category: \"energy\" },\n    flb: { ...transformFromFactor(1.3558179483), category: \"energy\" },\n    c: { ...transformFromFactor(4.184), category: \"energy\" },\n    cal: { ...transformFromFactor(4.1868), category: \"energy\" },\n    BTU: { ...transformFromFactor(1055.06), category: \"energy\" },\n    Wh: { ...transformFromFactor(3600), category: \"energy\" },\n    HPh: { ...transformFromFactor(2684520), category: \"energy\" },\n    // POWER UNITS : Standard = Watt\n    W: { ...standard, category: \"power\" },\n    PS: { ...transformFromFactor(735.499), category: \"power\" },\n    HP: { ...transformFromFactor(745.7), category: \"power\" },\n    // MAGNETISM UNITS : Standard = Tesla\n    T: { ...standard, category: \"magnetism\" },\n    ga: { ...transformFromFactor(1e-4), category: \"magnetism\" },\n    // TEMPERATURE UNITS : Standard = Kelvin\n    K: { ...standard, category: \"temperature\" },\n    C: {\n        transform: (T) => T + 273.15,\n        inverseTransform: (T) => T - 273.15,\n        category: \"temperature\",\n    },\n    F: {\n        transform: (T) => ((T - 32) * 5) / 9 + 273.15,\n        inverseTransform: (T) => ((T - 273.15) * 9) / 5 + 32,\n        category: \"temperature\",\n    },\n    Rank: { ...transformFromFactor(5 / 9), category: \"temperature\" },\n    Reau: {\n        transform: (T) => T * 1.25 + 273.15,\n        inverseTransform: (T) => (T - 273.15) / 1.25,\n        category: \"temperature\",\n    },\n    // VOLUME UNITS : Standard = cubic meter\n    \"m^3\": { ...standard, category: \"volume\", order: 3 },\n    \"ang^3\": { ...transformFromFactor(Math.pow(ANG2M, 3)), category: \"volume\", order: 3 },\n    \"Picapt^3\": { ...transformFromFactor(Math.pow(PICAPT2M, 3)), category: \"volume\", order: 3 },\n    tsp: { ...transformFromFactor(4.92892e-6), category: \"volume\" },\n    tspm: { ...transformFromFactor(5e-6), category: \"volume\" },\n    tbs: { ...transformFromFactor(1.4786764825785619e-5), category: \"volume\" },\n    \"in^3\": { ...transformFromFactor(Math.pow(IN2M, 3)), category: \"volume\", order: 3 },\n    oz: { ...transformFromFactor(2.95735295625e-5), category: \"volume\" },\n    cup: { ...transformFromFactor(0.000237), category: \"volume\" },\n    pt: { ...transformFromFactor(0.0004731765), category: \"volume\" },\n    uk_pt: { ...transformFromFactor(0.000568261), category: \"volume\" },\n    qt: { ...transformFromFactor(0.0009463529), category: \"volume\" },\n    l: { ...transformFromFactor(1e-3), category: \"volume\" },\n    uk_qt: { ...transformFromFactor(0.0011365225), category: \"volume\" },\n    gal: { ...transformFromFactor(0.0037854118), category: \"volume\" },\n    uk_gal: { ...transformFromFactor(0.00454609), category: \"volume\" },\n    \"ft^3\": { ...transformFromFactor(Math.pow(FT2M, 3)), category: \"volume\", order: 3 },\n    bushel: { ...transformFromFactor(0.0352390704), category: \"volume\" },\n    barrel: { ...transformFromFactor(0.158987295), category: \"volume\" },\n    \"yd^3\": { ...transformFromFactor(Math.pow(YD2M, 3)), category: \"volume\", order: 3 },\n    MTON: { ...transformFromFactor(1.13267386368), category: \"volume\" },\n    GRT: { ...transformFromFactor(2.83168), category: \"volume\" },\n    \"mi^3\": { ...transformFromFactor(Math.pow(MI2M, 3)), category: \"volume\", order: 3 },\n    \"Nmi^3\": { ...transformFromFactor(Math.pow(NMI2M, 3)), category: \"volume\", order: 3 },\n    \"ly^3\": { ...transformFromFactor(Math.pow(LY2M, 3)), category: \"volume\", order: 3 },\n    // AREA UNITS : Standard = square meter\n    \"m^2\": { ...standard, category: \"area\", order: 2 },\n    \"ang^2\": { ...transformFromFactor(Math.pow(ANG2M, 2)), category: \"area\", order: 2 },\n    \"Picapt^2\": { ...transformFromFactor(Math.pow(PICAPT2M, 2)), category: \"area\", order: 2 },\n    \"in^2\": { ...transformFromFactor(Math.pow(IN2M, 2)), category: \"area\", order: 2 },\n    \"ft^2\": { ...transformFromFactor(Math.pow(FT2M, 2)), category: \"area\", order: 2 },\n    \"yd^2\": { ...transformFromFactor(Math.pow(YD2M, 2)), category: \"area\", order: 2 },\n    ar: { ...transformFromFactor(100), category: \"area\" },\n    Morgen: { ...transformFromFactor(2500), category: \"area\" },\n    uk_acre: { ...transformFromFactor(4046.8564224), category: \"area\" },\n    us_acre: { ...transformFromFactor(4046.8726098743), category: \"area\" },\n    ha: { ...transformFromFactor(1e4), category: \"area\" },\n    \"mi^2\": { ...transformFromFactor(Math.pow(MI2M, 2)), category: \"area\", order: 2 },\n    \"Nmi^2\": { ...transformFromFactor(Math.pow(NMI2M, 2)), category: \"area\", order: 2 },\n    \"ly^2\": { ...transformFromFactor(Math.pow(LY2M, 2)), category: \"area\", order: 2 },\n    // INFORMATION UNITS : Standard = bit\n    bit: { ...standard, category: \"information\" },\n    byte: { ...transformFromFactor(8), category: \"information\" },\n    // SPEED UNITS : Standard = m/s\n    \"m/s\": { ...standard, category: \"speed\" },\n    \"m/hr\": { ...transformFromFactor(1 / 3600), category: \"speed\" },\n    \"km/hr\": { ...transformFromFactor(1 / 3.6), category: \"speed\" },\n    mph: { ...transformFromFactor(0.44704), category: \"speed\" },\n    kn: { ...transformFromFactor(0.5144444444), category: \"speed\" },\n    admkn: { ...transformFromFactor(0.5147733333), category: \"speed\" },\n};\nconst UNITS_ALIASES = {\n    shweight: \"cwt\",\n    lcwt: \"uk_cwt\",\n    hweight: \"uk_cwt\",\n    LTON: \"uk_ton\",\n    brton: \"uk_ton\",\n    pc: \"parsec\",\n    Pica: \"Picapt\",\n    d: \"day\",\n    mn: \"min\",\n    s: \"sec\",\n    p: \"Pa\",\n    at: \"atm\",\n    dy: \"dyn\",\n    ev: \"eV\",\n    hh: \"HPh\",\n    wh: \"Wh\",\n    btu: \"BTU\",\n    h: \"HP\",\n    cel: \"C\",\n    fah: \"F\",\n    kel: \"K\",\n    us_pt: \"pt\",\n    L: \"l\",\n    lt: \"l\",\n    ang3: \"ang^3\",\n    ft3: \"ft^3\",\n    in3: \"in^3\",\n    ly3: \"ly^3\",\n    m3: \"m^3\",\n    mi3: \"mi^3\",\n    yd3: \"yd^3\",\n    Nmi3: \"Nmi^3\",\n    Picapt3: \"Picapt^3\",\n    \"Pica^3\": \"Picapt^3\",\n    Pica3: \"Picapt^3\",\n    regton: \"GRT\",\n    ang2: \"ang^2\",\n    ft2: \"ft^2\",\n    in2: \"in^2\",\n    ly2: \"ly^2\",\n    m2: \"m^2\",\n    mi2: \"mi^2\",\n    Nmi2: \"Nmi^2\",\n    Picapt2: \"Picapt^2\",\n    \"Pica^2\": \"Picapt^2\",\n    Pica2: \"Picapt^2\",\n    yd2: \"yd^2\",\n    \"m/h\": \"m/hr\",\n    \"m/sec\": \"m/s\",\n};\nconst UNIT_PREFIXES = {\n    \"\": 1,\n    Y: 1e24,\n    Z: 1e21,\n    E: 1e18,\n    P: 1e15,\n    T: 1e12,\n    G: 1e9,\n    M: 1e6,\n    k: 1e3,\n    h: 1e2,\n    da: 1e1,\n    e: 1e1,\n    d: 1e-1,\n    c: 1e-2,\n    m: 1e-3,\n    u: 1e-6,\n    n: 1e-9,\n    p: 1e-12,\n    f: 1e-15,\n    a: 1e-18,\n    z: 1e-21,\n    y: 1e-21,\n    Yi: Math.pow(2, 80),\n    Zi: Math.pow(2, 70),\n    Ei: Math.pow(2, 60),\n    Pi: Math.pow(2, 50),\n    Ti: Math.pow(2, 40),\n    Gi: Math.pow(2, 30),\n    Mi: Math.pow(2, 20),\n    ki: Math.pow(2, 10),\n};\nconst TRANSLATED_CATEGORIES = {\n    weight: _t(\"Weight\"),\n    distance: _t(\"Distance\"),\n    time: _t(\"Time\"),\n    pressure: _t(\"Pressure\"),\n    force: _t(\"Force\"),\n    energy: _t(\"Energy\"),\n    power: _t(\"Power\"),\n    magnetism: _t(\"Magnetism\"),\n    temperature: _t(\"Temperature\"),\n    volume: _t(\"Volume\"),\n    area: _t(\"Area\"),\n    information: _t(\"Information\"),\n    speed: _t(\"Speed\"),\n};\nfunction getTranslatedCategory(key) {\n    return TRANSLATED_CATEGORIES[key] ?? \"\";\n}\nfunction getTransformation(key) {\n    for (const [prefix, value] of Object.entries(UNIT_PREFIXES)) {\n        if (prefix && !key.startsWith(prefix))\n            continue;\n        const _key = key.slice(prefix.length);\n        let conversion = UNITS[_key];\n        if (!conversion && UNITS_ALIASES[_key]) {\n            conversion = UNITS[UNITS_ALIASES[_key]];\n        }\n        if (conversion) {\n            return {\n                ...conversion,\n                factor: conversion.order ? Math.pow(value, conversion.order) : value,\n            };\n        }\n    }\n    return;\n}\n\n// -----------------------------------------------------------------------------\n// CONVERT\n// -----------------------------------------------------------------------------\nconst CONVERT = {\n    description: _t(\"Converts a numeric value to a different unit of measure.\"),\n    args: [\n        arg(\"value (number)\", _t(\"the numeric value in start_unit to convert to end_unit\")),\n        arg(\"start_unit (string)\", _t(\"The starting unit, the unit currently assigned to value\")),\n        arg(\"end_unit (string)\", _t(\"The unit of measure into which to convert value\")),\n    ],\n    compute: function (value, startUnit, endUnit) {\n        const _value = toNumber(value, this.locale);\n        const _startUnit = toString(startUnit);\n        const _endUnit = toString(endUnit);\n        const startConversion = getTransformation(_startUnit);\n        const endConversion = getTransformation(_endUnit);\n        if (!startConversion) {\n            return {\n                value: CellErrorType.GenericError,\n                message: _t(\"Invalid units of measure ('%s')\", _startUnit),\n            };\n        }\n        if (!endConversion) {\n            return {\n                value: CellErrorType.GenericError,\n                message: _t(\"Invalid units of measure ('%s')\", _endUnit),\n            };\n        }\n        if (startConversion.category !== endConversion.category) {\n            return {\n                value: CellErrorType.GenericError,\n                message: _t(\"Incompatible units of measure ('%s' vs '%s')\", getTranslatedCategory(startConversion.category), getTranslatedCategory(endConversion.category)),\n            };\n        }\n        return {\n            value: endConversion.inverseTransform(startConversion.factor * startConversion.transform(_value)) /\n                endConversion.factor,\n            format: value?.format,\n        };\n    },\n    isExported: true,\n};\n\nvar parser = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    CONVERT: CONVERT\n});\n\nconst DEFAULT_STARTING_AT = 1;\n/** Regex matching all the words in a string */\nconst wordRegex = /[A-Za-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff]+/g;\n// -----------------------------------------------------------------------------\n// CHAR\n// -----------------------------------------------------------------------------\nconst CHAR = {\n    description: _t(\"Gets character associated with number.\"),\n    args: [\n        arg(\"table_number (number)\", _t(\"The number of the character to look up from the current Unicode table in decimal format.\")),\n    ],\n    compute: function (tableNumber) {\n        const _tableNumber = Math.trunc(toNumber(tableNumber, this.locale));\n        assert(() => _tableNumber >= 1, _t(\"The table_number (%s) is out of range.\", _tableNumber.toString()));\n        return String.fromCharCode(_tableNumber);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CLEAN\n// -----------------------------------------------------------------------------\nconst CLEAN = {\n    description: _t(\"Remove non-printable characters from a piece of text.\"),\n    args: [arg(\"text (string)\", _t(\"The text whose non-printable characters are to be removed.\"))],\n    compute: function (text) {\n        const _text = toString(text);\n        let cleanedStr = \"\";\n        for (const char of _text) {\n            if (char && char.charCodeAt(0) > 31) {\n                cleanedStr += char;\n            }\n        }\n        return cleanedStr;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// CONCATENATE\n// -----------------------------------------------------------------------------\nconst CONCATENATE = {\n    description: _t(\"Appends strings to one another.\"),\n    args: [\n        arg(\"string1 (string, range<string>)\", _t(\"The initial string.\")),\n        arg(\"string2 (string, range<string>, repeating)\", _t(\"More strings to append in sequence.\")),\n    ],\n    compute: function (...datas) {\n        return reduceAny(datas, (acc, a) => acc + toString(a), \"\");\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// EXACT\n// -----------------------------------------------------------------------------\nconst EXACT = {\n    description: _t(\"Tests whether two strings are identical.\"),\n    args: [\n        arg(\"string1 (string)\", _t(\"The first string to compare.\")),\n        arg(\"string2 (string)\", _t(\"The second string to compare.\")),\n    ],\n    compute: function (string1, string2) {\n        return toString(string1) === toString(string2);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// FIND\n// -----------------------------------------------------------------------------\nconst FIND = {\n    description: _t(\"First position of string found in text, case-sensitive.\"),\n    args: [\n        arg(\"search_for (string)\", _t(\"The string to look for within text_to_search.\")),\n        arg(\"text_to_search (string)\", _t(\"The text to search for the first occurrence of search_for.\")),\n        arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t(\"The character within text_to_search at which to start the search.\")),\n    ],\n    compute: function (searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {\n        const _searchFor = toString(searchFor);\n        const _textToSearch = toString(textToSearch);\n        const _startingAt = toNumber(startingAt, this.locale);\n        assert(() => _textToSearch !== \"\", _t(\"The text_to_search must be non-empty.\"));\n        assert(() => _startingAt >= 1, _t(\"The starting_at (%s) must be greater than or equal to 1.\", _startingAt.toString()));\n        const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);\n        assert(() => result >= 0, _t(\"In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.\", _searchFor.toString(), _textToSearch));\n        return result + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// JOIN\n// -----------------------------------------------------------------------------\nconst JOIN = {\n    description: _t(\"Concatenates elements of arrays with delimiter.\"),\n    args: [\n        arg(\"delimiter (string)\", _t(\"The character or string to place between each concatenated value.\")),\n        arg(\"value_or_array1 (string, range<string>)\", _t(\"The value or values to be appended using delimiter.\")),\n        arg(\"value_or_array2 (string, range<string>, repeating)\", _t(\"More values to be appended using delimiter.\")),\n    ],\n    compute: function (delimiter, ...valuesOrArrays) {\n        const _delimiter = toString(delimiter);\n        return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : \"\") + toString(a), \"\");\n    },\n};\n// -----------------------------------------------------------------------------\n// LEFT\n// -----------------------------------------------------------------------------\nconst LEFT = {\n    description: _t(\"Substring from beginning of specified string.\"),\n    args: [\n        arg(\"text (string)\", _t(\"The string from which the left portion will be returned.\")),\n        arg(\"number_of_characters (number, optional)\", _t(\"The number of characters to return from the left side of string.\")),\n    ],\n    compute: function (text, ...args) {\n        const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;\n        assert(() => _numberOfCharacters >= 0, _t(\"The number_of_characters (%s) must be positive or null.\", _numberOfCharacters.toString()));\n        return toString(text).substring(0, _numberOfCharacters);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// LEN\n// -----------------------------------------------------------------------------\nconst LEN = {\n    description: _t(\"Length of a string.\"),\n    args: [arg(\"text (string)\", _t(\"The string whose length will be returned.\"))],\n    compute: function (text) {\n        return toString(text).length;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// LOWER\n// -----------------------------------------------------------------------------\nconst LOWER = {\n    description: _t(\"Converts a specified string to lowercase.\"),\n    args: [arg(\"text (string)\", _t(\"The string to convert to lowercase.\"))],\n    compute: function (text) {\n        return toString(text).toLowerCase();\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// MID\n// -----------------------------------------------------------------------------\nconst MID = {\n    description: _t(\"A segment of a string.\"),\n    args: [\n        arg(\"text (string)\", _t(\"The string to extract a segment from.\")),\n        arg(\"starting_at (number)\", _t(\"The index from the left of string from which to begin extracting. The first character in string has the index 1.\")),\n        arg(\"extract_length (number)\", _t(\"The length of the segment to extract.\")),\n    ],\n    compute: function (text, starting_at, extract_length) {\n        const _text = toString(text);\n        const _starting_at = toNumber(starting_at, this.locale);\n        const _extract_length = toNumber(extract_length, this.locale);\n        assert(() => _starting_at >= 1, _t(\"The starting_at argument (%s) must be positive greater than one.\", _starting_at.toString()));\n        assert(() => _extract_length >= 0, _t(\"The extract_length argument (%s) must be positive or null.\", _extract_length.toString()));\n        return _text.slice(_starting_at - 1, _starting_at + _extract_length - 1);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// PROPER\n// -----------------------------------------------------------------------------\nconst PROPER = {\n    description: _t(\"Capitalizes each word in a specified string.\"),\n    args: [\n        arg(\"text_to_capitalize (string)\", _t(\"The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase.\")),\n    ],\n    compute: function (text) {\n        const _text = toString(text);\n        return _text.replace(wordRegex, (word) => {\n            return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();\n        });\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// REPLACE\n// -----------------------------------------------------------------------------\nconst REPLACE = {\n    description: _t(\"Replaces part of a text string with different text.\"),\n    args: [\n        arg(\"text (string)\", _t(\"The text, a part of which will be replaced.\")),\n        arg(\"position (number)\", _t(\"The position where the replacement will begin (starting from 1).\")),\n        arg(\"length (number)\", _t(\"The number of characters in the text to be replaced.\")),\n        arg(\"new_text (string)\", _t(\"The text which will be inserted into the original text.\")),\n    ],\n    compute: function (text, position, length, newText) {\n        const _position = toNumber(position, this.locale);\n        assert(() => _position >= 1, _t(\"The position (%s) must be greater than or equal to 1.\", _position.toString()));\n        const _text = toString(text);\n        const _length = toNumber(length, this.locale);\n        const _newText = toString(newText);\n        return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// RIGHT\n// -----------------------------------------------------------------------------\nconst RIGHT = {\n    description: _t(\"A substring from the end of a specified string.\"),\n    args: [\n        arg(\"text (string)\", _t(\"The string from which the right portion will be returned.\")),\n        arg(\"number_of_characters (number, optional)\", _t(\"The number of characters to return from the right side of string.\")),\n    ],\n    compute: function (text, ...args) {\n        const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;\n        assert(() => _numberOfCharacters >= 0, _t(\"The number_of_characters (%s) must be positive or null.\", _numberOfCharacters.toString()));\n        const _text = toString(text);\n        const stringLength = _text.length;\n        return _text.substring(stringLength - _numberOfCharacters, stringLength);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SEARCH\n// -----------------------------------------------------------------------------\nconst SEARCH = {\n    description: _t(\"First position of string found in text, ignoring case.\"),\n    args: [\n        arg(\"search_for (string)\", _t(\"The string to look for within text_to_search.\")),\n        arg(\"text_to_search (string)\", _t(\"The text to search for the first occurrence of search_for.\")),\n        arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t(\"The character within text_to_search at which to start the search.\")),\n    ],\n    compute: function (searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {\n        const _searchFor = toString(searchFor).toLowerCase();\n        const _textToSearch = toString(textToSearch).toLowerCase();\n        const _startingAt = toNumber(startingAt, this.locale);\n        assert(() => _textToSearch !== \"\", _t(\"The text_to_search must be non-empty.\"));\n        assert(() => _startingAt >= 1, _t(\"The starting_at (%s) must be greater than or equal to 1.\", _startingAt.toString()));\n        const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);\n        assert(() => result >= 0, _t(\"In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.\", _searchFor, _textToSearch));\n        return result + 1;\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SPLIT\n// -----------------------------------------------------------------------------\nconst SPLIT_DEFAULT_SPLIT_BY_EACH = true;\nconst SPLIT_DEFAULT_REMOVE_EMPTY_TEXT = true;\nconst SPLIT = {\n    description: _t(\"Split text by specific character delimiter(s).\"),\n    args: [\n        arg(\"text (string)\", _t(\"The text to divide.\")),\n        arg(\"delimiter (string)\", _t(\"The character or characters to use to split text.\")),\n        arg(`split_by_each (boolean, default=${SPLIT_DEFAULT_SPLIT_BY_EACH}})`, _t(\"Whether or not to divide text around each character contained in delimiter.\")),\n        arg(`remove_empty_text (boolean, default=${SPLIT_DEFAULT_REMOVE_EMPTY_TEXT})`, _t(\"Whether or not to remove empty text messages from the split results. The default behavior is to treat \\\n        consecutive delimiters as one (if TRUE). If FALSE, empty cells values are added between consecutive delimiters.\")),\n    ],\n    compute: function (text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {\n        const _text = toString(text);\n        const _delimiter = escapeRegExp(toString(delimiter));\n        const _splitByEach = toBoolean(splitByEach);\n        const _removeEmptyText = toBoolean(removeEmptyText);\n        assert(() => _delimiter.length > 0, _t(\"The _delimiter (%s) must be not be empty.\", _delimiter));\n        const regex = _splitByEach ? new RegExp(`[${_delimiter}]`, \"g\") : new RegExp(_delimiter, \"g\");\n        let result = _text.split(regex);\n        if (_removeEmptyText) {\n            result = result.filter((text) => text !== \"\");\n        }\n        return transposeMatrix([result]);\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// SUBSTITUTE\n// -----------------------------------------------------------------------------\nconst SUBSTITUTE = {\n    description: _t(\"Replaces existing text with new text in a string.\"),\n    args: [\n        arg(\"text_to_search (string)\", _t(\"The text within which to search and replace.\")),\n        arg(\"search_for (string)\", _t(\"The string to search for within text_to_search.\")),\n        arg(\"replace_with (string)\", _t(\"The string that will replace search_for.\")),\n        arg(\"occurrence_number (number, optional)\", _t(\"The instance of search_for within text_to_search to replace with replace_with. By default, all occurrences of search_for are replaced; however, if occurrence_number is specified, only the indicated instance of search_for is replaced.\")),\n    ],\n    compute: function (textToSearch, searchFor, replaceWith, occurrenceNumber) {\n        const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);\n        assert(() => _occurrenceNumber >= 0, _t(\"The occurrenceNumber (%s) must be positive or null.\", _occurrenceNumber.toString()));\n        const _textToSearch = toString(textToSearch);\n        const _searchFor = toString(searchFor);\n        if (_searchFor === \"\") {\n            return _textToSearch;\n        }\n        const _replaceWith = toString(replaceWith);\n        const reg = new RegExp(escapeRegExp(_searchFor), \"g\");\n        if (_occurrenceNumber === 0) {\n            return _textToSearch.replace(reg, _replaceWith);\n        }\n        let n = 0;\n        return _textToSearch.replace(reg, (text) => (++n === _occurrenceNumber ? _replaceWith : text));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TEXTJOIN\n// -----------------------------------------------------------------------------\nconst TEXTJOIN = {\n    description: _t(\"Combines text from multiple strings and/or arrays.\"),\n    args: [\n        arg(\"delimiter (string)\", _t(\" A string, possible empty, or a reference to a valid string. If empty, the text will be simply concatenated.\")),\n        arg(\"ignore_empty (boolean)\", _t(\"A boolean; if TRUE, empty cells selected in the text arguments won't be included in the result.\")),\n        arg(\"text1 (string, range<string>)\", _t(\"Any text item. This could be a string, or an array of strings in a range.\")),\n        arg(\"text2 (string, range<string>, repeating)\", _t(\"Additional text item(s).\")),\n    ],\n    compute: function (delimiter, ignoreEmpty, ...textsOrArrays) {\n        const _delimiter = toString(delimiter);\n        const _ignoreEmpty = toBoolean(ignoreEmpty);\n        let n = 0;\n        return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === \"\") ? (n++ ? acc + _delimiter : \"\") + toString(a) : acc, \"\");\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TRIM\n// -----------------------------------------------------------------------------\nconst TRIM = {\n    description: _t(\"Removes space characters.\"),\n    args: [\n        arg(\"text (string)\", _t(\"The text or reference to a cell containing text to be trimmed.\")),\n    ],\n    compute: function (text) {\n        return trimContent(toString(text));\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// UPPER\n// -----------------------------------------------------------------------------\nconst UPPER = {\n    description: _t(\"Converts a specified string to uppercase.\"),\n    args: [arg(\"text (string)\", _t(\"The string to convert to uppercase.\"))],\n    compute: function (text) {\n        return toString(text).toUpperCase();\n    },\n    isExported: true,\n};\n// -----------------------------------------------------------------------------\n// TEXT\n// -----------------------------------------------------------------------------\nconst TEXT = {\n    description: _t(\"Converts a number to text according to a specified format.\"),\n    args: [\n        arg(\"number (number)\", _t(\"The number, date or time to format.\")),\n        arg(\"format (string)\", _t(\"The pattern by which to format the number, enclosed in quotation marks.\")),\n    ],\n    compute: function (number, format) {\n        const _number = toNumber(number, this.locale);\n        return formatValue(_number, { format: toString(format), locale: this.locale });\n    },\n    isExported: true,\n};\n\nvar text = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    CHAR: CHAR,\n    CLEAN: CLEAN,\n    CONCATENATE: CONCATENATE,\n    EXACT: EXACT,\n    FIND: FIND,\n    JOIN: JOIN,\n    LEFT: LEFT,\n    LEN: LEN,\n    LOWER: LOWER,\n    MID: MID,\n    PROPER: PROPER,\n    REPLACE: REPLACE,\n    RIGHT: RIGHT,\n    SEARCH: SEARCH,\n    SPLIT: SPLIT,\n    SUBSTITUTE: SUBSTITUTE,\n    TEXT: TEXT,\n    TEXTJOIN: TEXTJOIN,\n    TRIM: TRIM,\n    UPPER: UPPER\n});\n\n// -----------------------------------------------------------------------------\n// HYPERLINK\n// -----------------------------------------------------------------------------\nconst HYPERLINK = {\n    description: _t(\"Creates a hyperlink in a cell.\"),\n    args: [\n        arg(\"url (string)\", _t(\"The full URL of the link enclosed in quotation marks.\")),\n        arg(\"link_label (string, optional)\", _t(\"The text to display in the cell, enclosed in quotation marks.\")),\n    ],\n    compute: function (url, linkLabel) {\n        const processedUrl = toString(url).trim();\n        const processedLabel = toString(linkLabel) || processedUrl;\n        if (processedUrl === \"\")\n            return processedLabel;\n        return markdownLink(processedLabel, processedUrl);\n    },\n    isExported: true,\n};\n\nvar web = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    HYPERLINK: HYPERLINK\n});\n\nconst categories = [\n    { name: _t(\"Array\"), functions: array },\n    { name: _t(\"Database\"), functions: database },\n    { name: _t(\"Date\"), functions: date },\n    { name: _t(\"Filter\"), functions: filter },\n    { name: _t(\"Financial\"), functions: financial },\n    { name: _t(\"Info\"), functions: info },\n    { name: _t(\"Lookup\"), functions: lookup },\n    { name: _t(\"Logical\"), functions: logical },\n    { name: _t(\"Math\"), functions: math },\n    { name: _t(\"Misc\"), functions: misc },\n    { name: _t(\"Operator\"), functions: operators },\n    { name: _t(\"Statistical\"), functions: statistical },\n    { name: _t(\"Text\"), functions: text },\n    { name: _t(\"Engineering\"), functions: engineering },\n    { name: _t(\"Web\"), functions: web },\n    { name: _t(\"Parser\"), functions: parser },\n];\nconst functionNameRegex = /^[A-Z0-9\\_\\.]+$/;\nclass FunctionRegistry extends Registry {\n    mapping = {};\n    add(name, addDescr) {\n        name = name.toUpperCase();\n        if (!functionNameRegex.test(name)) {\n            throw new Error(_t(\"Invalid function name %s. Function names can exclusively contain alphanumerical values separated by dots (.) or underscore (_)\", name));\n        }\n        const descr = addMetaInfoFromArg(addDescr);\n        validateArguments(descr.args);\n        this.mapping[name] = createComputeFunction(descr, name);\n        super.add(name, descr);\n        return this;\n    }\n}\nconst functionRegistry = new FunctionRegistry();\nfor (let category of categories) {\n    const fns = category.functions;\n    for (let name in fns) {\n        const addDescr = fns[name];\n        addDescr.category = addDescr.category || category.name;\n        name = name.replace(/_/g, \".\");\n        functionRegistry.add(name, { isExported: false, ...addDescr });\n    }\n}\nconst notAvailableError = new NotAvailableError(_t(\"Array arguments to [[FUNCTION_NAME]] are of different size.\"));\nfunction createComputeFunction(descr, functionName) {\n    function vectorizedCompute(...args) {\n        let countVectorizableCol = 1;\n        let countVectorizableRow = 1;\n        let vectorizableColLimit = Infinity;\n        let vectorizableRowLimit = Infinity;\n        let vectorArgsType = undefined;\n        //#region Compute vectorisation limits\n        for (let i = 0; i < args.length; i++) {\n            const argDefinition = descr.args[descr.getArgToFocus(i + 1) - 1];\n            const arg = args[i];\n            if (isMatrix(arg) && !argDefinition.acceptMatrix) {\n                // if argDefinition does not accept a matrix but arg is still a matrix\n                // --> triggers the arguments vectorization\n                const nColumns = arg.length;\n                const nRows = arg[0].length;\n                if (nColumns !== 1 || nRows !== 1) {\n                    vectorArgsType ??= new Array(args.length);\n                    if (nColumns !== 1 && nRows !== 1) {\n                        vectorArgsType[i] = \"matrix\";\n                        countVectorizableCol = Math.max(countVectorizableCol, nColumns);\n                        countVectorizableRow = Math.max(countVectorizableRow, nRows);\n                        vectorizableColLimit = Math.min(vectorizableColLimit, nColumns);\n                        vectorizableRowLimit = Math.min(vectorizableRowLimit, nRows);\n                    }\n                    else if (nColumns !== 1) {\n                        vectorArgsType[i] = \"horizontal\";\n                        countVectorizableCol = Math.max(countVectorizableCol, nColumns);\n                        vectorizableColLimit = Math.min(vectorizableColLimit, nColumns);\n                    }\n                    else if (nRows !== 1) {\n                        vectorArgsType[i] = \"vertical\";\n                        countVectorizableRow = Math.max(countVectorizableRow, nRows);\n                        vectorizableRowLimit = Math.min(vectorizableRowLimit, nRows);\n                    }\n                }\n                else {\n                    args[i] = arg[0][0];\n                }\n            }\n            if (!isMatrix(arg) && argDefinition.acceptMatrixOnly) {\n                throw new BadExpressionError(_t(\"Function %s expects the parameter '%s' to be reference to a cell or range.\", functionName, (i + 1).toString()));\n            }\n        }\n        //#endregion\n        if (countVectorizableCol === 1 && countVectorizableRow === 1) {\n            // either this function is not vectorized or it ends up with a 1x1 dimension\n            return errorHandlingCompute.apply(this, args);\n        }\n        const getArgOffset = (i, j) => args.map((arg, index) => {\n            switch (vectorArgsType?.[index]) {\n                case \"matrix\":\n                    return arg[i][j];\n                case \"horizontal\":\n                    return arg[i][0];\n                case \"vertical\":\n                    return arg[0][j];\n                case undefined:\n                    return arg;\n            }\n        });\n        return generateMatrix(countVectorizableCol, countVectorizableRow, (col, row) => {\n            if (col > vectorizableColLimit - 1 || row > vectorizableRowLimit - 1) {\n                return notAvailableError;\n            }\n            const singleCellComputeResult = errorHandlingCompute.apply(this, getArgOffset(col, row));\n            // In the case where the user tries to vectorize arguments of an array formula, we will get an\n            // array for every combination of the vectorized arguments, which will lead to a 3D matrix and\n            // we won't be able to return the values.\n            // In this case, we keep the first element of each spreading part, just as Excel does, and\n            // create an array with these parts.\n            // For exemple, we have MUNIT(x) that return an unitary matrix of x*x. If we use it with a\n            // range, like MUNIT(A1:A2), we will get two unitary matrices (one for the value in A1 and one\n            // for the value in A2). In this case, we will simply take the first value of each matrix and\n            // return the array [First value of MUNIT(A1), First value of MUNIT(A2)].\n            return isMatrix(singleCellComputeResult)\n                ? singleCellComputeResult[0][0]\n                : singleCellComputeResult;\n        });\n    }\n    function errorHandlingCompute(...args) {\n        for (let i = 0; i < args.length; i++) {\n            const arg = args[i];\n            const argDefinition = descr.args[descr.getArgToFocus(i + 1) - 1];\n            // Early exit if the argument is an error and the function does not accept errors\n            // We only check scalar arguments, not matrix arguments for performance reasons.\n            // Casting helpers are responsible for handling errors in matrix arguments.\n            if (!argDefinition.acceptErrors && !isMatrix(arg) && isEvaluationError(arg?.value)) {\n                return arg;\n            }\n        }\n        try {\n            return computeFunctionToObject.apply(this, args);\n        }\n        catch (e) {\n            return handleError(e, functionName);\n        }\n    }\n    function computeFunctionToObject(...args) {\n        const result = descr.compute.apply(this, args);\n        if (!isMatrix(result)) {\n            if (typeof result === \"object\" && result !== null && \"value\" in result) {\n                replaceFunctionNamePlaceholder(result, functionName);\n                return result;\n            }\n            return { value: result };\n        }\n        if (typeof result[0][0] === \"object\" && result[0][0] !== null && \"value\" in result[0][0]) {\n            matrixForEach(result, (result) => replaceFunctionNamePlaceholder(result, functionName));\n            return result;\n        }\n        return matrixMap(result, (row) => ({ value: row }));\n    }\n    return vectorizedCompute;\n}\nfunction handleError(e, functionName) {\n    // the error could be an user error (instance of EvaluationError)\n    // or a javascript error (instance of Error)\n    // we don't want block the user with an implementation error\n    // so we fallback to a generic error\n    if (hasStringValue(e) && isEvaluationError(e.value)) {\n        if (hasStringMessage(e)) {\n            replaceFunctionNamePlaceholder(e, functionName);\n        }\n        return e;\n    }\n    console.error(e);\n    return new EvaluationError(implementationErrorMessage + (hasStringMessage(e) ? \" \" + e.message : \"\"));\n}\nfunction hasStringValue(obj) {\n    return (obj?.value !== undefined &&\n        typeof obj.value === \"string\");\n}\nfunction replaceFunctionNamePlaceholder(functionResult, functionName) {\n    // for performance reasons: change in place and only if needed\n    if (functionResult.message?.includes(\"[[FUNCTION_NAME]]\")) {\n        functionResult.message = functionResult.message.replace(\"[[FUNCTION_NAME]]\", functionName);\n    }\n}\nconst implementationErrorMessage = _t(\"An unexpected error occurred. Submit a support ticket at odoo.com/help.\");\nfunction hasStringMessage(obj) {\n    return (obj?.message !== undefined &&\n        typeof obj.message === \"string\");\n}\n\nautoCompleteProviders.add(\"functions\", {\n    sequence: 100,\n    autoSelectFirstProposal: true,\n    maxDisplayedProposals: 10,\n    getProposals(tokenAtCursor) {\n        if (tokenAtCursor.type !== \"SYMBOL\") {\n            return [];\n        }\n        const searchTerm = tokenAtCursor.value;\n        if (!this.composer.currentContent.startsWith(\"=\")) {\n            return [];\n        }\n        const values = Object.entries(functionRegistry.content)\n            .filter(([_, { hidden }]) => !hidden)\n            .map(([text, { description }]) => {\n            return {\n                text,\n                description,\n                htmlContent: getHtmlContentFromPattern(searchTerm, text, COMPOSER_ASSISTANT_COLOR, \"o-semi-bold\"),\n            };\n        })\n            .sort((a, b) => {\n            return a.text.length - b.text.length || a.text.localeCompare(b.text);\n        });\n        return values;\n    },\n    selectProposal(tokenAtCursor, value) {\n        let start = tokenAtCursor.end;\n        let end = tokenAtCursor.end;\n        // shouldn't it be REFERENCE ?\n        if ([\"SYMBOL\", \"FUNCTION\"].includes(tokenAtCursor.type)) {\n            start = tokenAtCursor.start;\n        }\n        const tokens = this.composer.currentTokens;\n        value += \"(\";\n        const currentTokenIndex = tokens.map((token) => token.start).indexOf(tokenAtCursor.start);\n        if (currentTokenIndex + 1 < tokens.length) {\n            const nextToken = tokens[currentTokenIndex + 1];\n            if (nextToken?.type === \"LEFT_PAREN\") {\n                end++;\n            }\n        }\n        this.composer.changeComposerCursorSelection(start, end);\n        this.composer.replaceComposerCursorSelection(value);\n    },\n});\n\nclass DOMFocusableElementStore {\n    mutators = [\"setFocusableElement\", \"focus\"];\n    focusableElement = undefined;\n    setFocusableElement(element) {\n        this.focusableElement = element;\n    }\n    focus() {\n        this.focusableElement?.focus();\n    }\n}\n\n/**\n * This file is largely inspired by owl 1.\n * `css` tag has been removed from owl 2 without workaround to manage css.\n * So, the solution was to import the behavior of owl 1 directly in our\n * codebase, with one difference: the css is added to the sheet as soon as the\n * css tag is executed. In owl 1, the css was added as soon as a Component was\n * created for the first time.\n */\nconst STYLESHEETS = {};\nlet nextId = 0;\n/**\n * CSS tag helper for defining inline stylesheets.  With this, one can simply define\n * an inline stylesheet with just the following code:\n * ```js\n *     css`.component-a { color: red; }`;\n * ```\n */\nfunction css(strings, ...args) {\n    const name = `__sheet__${nextId++}`;\n    const value = String.raw(strings, ...args);\n    registerSheet(name, value);\n    activateSheet(name);\n    return name;\n}\nfunction processSheet(str) {\n    const tokens = str.split(/(\\{|\\}|;)/).map((s) => s.trim());\n    const selectorStack = [];\n    const parts = [];\n    let rules = [];\n    function generateSelector(stackIndex, parentSelector) {\n        const parts = [];\n        for (const selector of selectorStack[stackIndex]) {\n            let part = (parentSelector && parentSelector + \" \" + selector) || selector;\n            if (part.includes(\"&\")) {\n                part = selector.replace(/&/g, parentSelector || \"\");\n            }\n            if (stackIndex < selectorStack.length - 1) {\n                part = generateSelector(stackIndex + 1, part);\n            }\n            parts.push(part);\n        }\n        return parts.join(\", \");\n    }\n    function generateRules() {\n        if (rules.length) {\n            parts.push(generateSelector(0) + \" {\");\n            parts.push(...rules);\n            parts.push(\"}\");\n            rules = [];\n        }\n    }\n    while (tokens.length) {\n        let token = tokens.shift();\n        if (token === \"}\") {\n            generateRules();\n            selectorStack.pop();\n        }\n        else {\n            if (tokens[0] === \"{\") {\n                generateRules();\n                selectorStack.push(token.split(/\\s*,\\s*/));\n                tokens.shift();\n            }\n            if (tokens[0] === \";\") {\n                rules.push(\"  \" + token + \";\");\n            }\n        }\n    }\n    return parts.join(\"\\n\");\n}\nfunction registerSheet(id, css) {\n    const sheet = document.createElement(\"style\");\n    sheet.textContent = processSheet(css);\n    STYLESHEETS[id] = sheet;\n}\nfunction activateSheet(id) {\n    const sheet = STYLESHEETS[id];\n    sheet.setAttribute(\"component\", id);\n    document.head.appendChild(sheet);\n}\nfunction getTextDecoration({ strikethrough, underline, }) {\n    if (!strikethrough && !underline) {\n        return \"none\";\n    }\n    return `${strikethrough ? \"line-through\" : \"\"} ${underline ? \"underline\" : \"\"}`;\n}\n/**\n * Convert the cell style to CSS properties.\n */\nfunction cellStyleToCss(style) {\n    const attributes = cellTextStyleToCss(style);\n    if (!style)\n        return attributes;\n    if (style.fillColor) {\n        attributes[\"background\"] = style.fillColor;\n    }\n    return attributes;\n}\n/**\n * Convert the cell text style to CSS properties.\n */\nfunction cellTextStyleToCss(style) {\n    const attributes = {};\n    if (!style)\n        return attributes;\n    if (style.bold) {\n        attributes[\"font-weight\"] = \"bold\";\n    }\n    if (style.italic) {\n        attributes[\"font-style\"] = \"italic\";\n    }\n    if (style.strikethrough || style.underline) {\n        let decoration = style.strikethrough ? \"line-through\" : \"\";\n        decoration = style.underline ? decoration + \" underline\" : decoration;\n        attributes[\"text-decoration\"] = decoration;\n    }\n    if (style.textColor) {\n        attributes[\"color\"] = style.textColor;\n    }\n    return attributes;\n}\n/**\n * Transform CSS properties into a CSS string.\n */\nfunction cssPropertiesToCss(attributes) {\n    let styleStr = \"\";\n    for (const attName in attributes) {\n        if (!attributes[attName]) {\n            continue;\n        }\n        styleStr += `${attName}:${attributes[attName]}; `;\n    }\n    return styleStr;\n}\nfunction getElementMargins(el) {\n    const style = window.getComputedStyle(el);\n    const margins = {\n        top: parseInt(style.marginTop, 10) || 0,\n        bottom: parseInt(style.marginBottom, 10) || 0,\n        left: parseInt(style.marginLeft, 10) || 0,\n        right: parseInt(style.marginRight, 10) || 0,\n    };\n    return margins;\n}\n\nconst macRegex = /Mac/i;\nconst MODIFIER_KEYS = [\"Shift\", \"Control\", \"Alt\", \"Meta\"];\n/**\n * Return true if the event was triggered from\n * a child element.\n */\nfunction isChildEvent(parent, ev) {\n    if (!parent)\n        return false;\n    return !!ev.target && parent.contains(ev.target);\n}\nfunction gridOverlayPosition() {\n    const spreadsheetElement = document.querySelector(\".o-grid-overlay\");\n    if (spreadsheetElement) {\n        const { top, left } = spreadsheetElement?.getBoundingClientRect();\n        return { top, left };\n    }\n    throw new Error(\"Can't find spreadsheet position\");\n}\nfunction getBoundingRectAsPOJO(el) {\n    const rect = el.getBoundingClientRect();\n    return {\n        x: rect.x,\n        y: rect.y,\n        width: rect.width,\n        height: rect.height,\n    };\n}\n/**\n * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.\n */\nfunction* iterateChildren(el) {\n    yield el;\n    if (el.hasChildNodes()) {\n        for (let child of el.childNodes) {\n            yield* iterateChildren(child);\n        }\n    }\n}\nfunction getOpenedMenus() {\n    return Array.from(document.querySelectorAll(\".o-spreadsheet .o-menu\"));\n}\nconst letterRegex = /^[a-zA-Z]$/;\n/**\n * Transform a keyboard event into a shortcut string that represent this event. The letters keys will be uppercased.\n *\n * @argument ev - The keyboard event to transform\n * @argument mode - Use either ev.key of ev.code to get the string shortcut\n *\n * @example\n * event : { ctrlKey: true, key: \"a\" } => \"Ctrl+A\"\n * event : { shift: true, alt: true, key: \"Home\" } => \"Alt+Shift+Home\"\n */\nfunction keyboardEventToShortcutString(ev, mode = \"key\") {\n    let keyDownString = \"\";\n    if (!MODIFIER_KEYS.includes(ev.key)) {\n        if (isCtrlKey(ev))\n            keyDownString += \"Ctrl+\";\n        if (ev.altKey)\n            keyDownString += \"Alt+\";\n        if (ev.shiftKey)\n            keyDownString += \"Shift+\";\n    }\n    const key = mode === \"key\" ? ev.key : ev.code;\n    keyDownString += letterRegex.test(key) ? key.toUpperCase() : key;\n    return keyDownString;\n}\nfunction isMacOS() {\n    return Boolean(macRegex.test(navigator.userAgent));\n}\n/**\n * @param {KeyboardEvent | MouseEvent} ev\n * @returns Returns true if the event was triggered with the \"ctrl\" modifier pressed.\n * On Mac, this is the \"meta\" or \"command\" key.\n */\nfunction isCtrlKey(ev) {\n    return isMacOS() ? ev.metaKey : ev.ctrlKey;\n}\n\n/**\n * Return the o-spreadsheet element position relative\n * to the browser viewport.\n */\nfunction useSpreadsheetRect() {\n    const position = useState({ x: 0, y: 0, width: 0, height: 0 });\n    let spreadsheetElement = null;\n    function updatePosition() {\n        if (!spreadsheetElement) {\n            spreadsheetElement = document.querySelector(\".o-spreadsheet\");\n        }\n        if (spreadsheetElement) {\n            const { top, left, width, height } = spreadsheetElement.getBoundingClientRect();\n            position.x = left;\n            position.y = top;\n            position.width = width;\n            position.height = height;\n        }\n    }\n    onMounted(updatePosition);\n    onPatched(updatePosition);\n    return position;\n}\n/**\n * Return the component (or ref's component) BoundingRect, relative\n * to the upper left corner of the screen (<body> element).\n *\n * Note: when used with a <Portal/> component, it will\n * return the portal position, not the teleported position.\n */\nfunction useAbsoluteBoundingRect(ref) {\n    const rect = useState({ x: 0, y: 0, width: 0, height: 0 });\n    function updateElRect() {\n        const el = ref.el;\n        if (el === null) {\n            return;\n        }\n        const { top, left, width, height } = el.getBoundingClientRect();\n        rect.x = left;\n        rect.y = top;\n        rect.width = width;\n        rect.height = height;\n    }\n    onMounted(updateElRect);\n    onPatched(updateElRect);\n    return rect;\n}\n/**\n * Get the rectangle inside which a popover should stay when being displayed.\n * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the \"o-spreadsheet\"\n * element by default.\n *\n * Coordinates are expressed expressed as absolute DOM position.\n */\nfunction usePopoverContainer() {\n    const container = useState({ x: 0, y: 0, width: 0, height: 0 });\n    const component = useComponent();\n    const spreadsheetRect = useSpreadsheetRect();\n    function updateRect() {\n        const env = component.env;\n        const newRect = \"getPopoverContainerRect\" in env ? env.getPopoverContainerRect() : spreadsheetRect;\n        container.x = newRect.x;\n        container.y = newRect.y;\n        container.width = newRect.width;\n        container.height = newRect.height;\n    }\n    updateRect();\n    onMounted(updateRect);\n    onPatched(updateRect);\n    return container;\n}\n\nconst arrowMap = {\n    ArrowDown: \"down\",\n    ArrowLeft: \"left\",\n    ArrowRight: \"right\",\n    ArrowUp: \"up\",\n};\nfunction updateSelectionWithArrowKeys(ev, selection) {\n    const direction = arrowMap[ev.key];\n    if (ev.shiftKey) {\n        selection.resizeAnchorZone(direction, isCtrlKey(ev) ? \"end\" : 1);\n    }\n    else {\n        selection.moveAnchorCell(direction, isCtrlKey(ev) ? \"end\" : 1);\n    }\n}\n\ncss /* scss */ `\n  .o-autocomplete-dropdown {\n    pointer-events: auto;\n    cursor: pointer;\n    background-color: #fff;\n    max-width: 400px;\n    z-index: 1;\n\n    .o-autocomplete-value-focus {\n      background-color: #f2f2f2;\n    }\n\n    & > div {\n      padding: 1px 5px 5px 5px;\n      .o-autocomplete-description {\n        padding-left: 5px;\n        font-size: 11px;\n      }\n    }\n  }\n`;\nclass TextValueProvider extends Component {\n    static template = \"o-spreadsheet-TextValueProvider\";\n    static props = {\n        proposals: Array,\n        selectedIndex: { type: Number, optional: true },\n        onValueSelected: Function,\n        onValueHovered: Function,\n    };\n    autoCompleteListRef = useRef(\"autoCompleteList\");\n    setup() {\n        useEffect(() => {\n            const selectedIndex = this.props.selectedIndex;\n            if (selectedIndex === undefined) {\n                return;\n            }\n            const selectedElement = this.autoCompleteListRef.el?.children[selectedIndex];\n            selectedElement?.scrollIntoView?.({ block: \"nearest\" });\n        }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);\n    }\n}\n\nclass AutoCompleteStore extends SpreadsheetStore {\n    mutators = [\"useProvider\", \"moveSelection\", \"hide\", \"selectIndex\"];\n    selectedIndex = undefined;\n    provider;\n    get selectedProposal() {\n        if (this.selectedIndex === undefined || this.provider === undefined) {\n            return undefined;\n        }\n        return this.provider.proposals[this.selectedIndex];\n    }\n    useProvider(provider) {\n        this.provider = provider;\n        this.selectedIndex = provider.autoSelectFirstProposal ? 0 : undefined;\n    }\n    hide() {\n        this.provider = undefined;\n        this.selectedIndex = undefined;\n    }\n    selectIndex(index) {\n        this.selectedIndex = index;\n    }\n    moveSelection(direction) {\n        if (!this.provider) {\n            return;\n        }\n        if (this.selectedIndex === undefined) {\n            this.selectedIndex = 0;\n            return;\n        }\n        if (direction === \"previous\") {\n            this.selectedIndex--;\n            if (this.selectedIndex < 0) {\n                this.selectedIndex = this.provider.proposals.length - 1;\n            }\n        }\n        else {\n            this.selectedIndex = (this.selectedIndex + 1) % this.provider.proposals.length;\n        }\n    }\n}\n\nclass ContentEditableHelper {\n    // todo make el private and expose dedicated methods\n    el;\n    constructor(el) {\n        this.el = el;\n    }\n    updateEl(el) {\n        this.el = el;\n    }\n    /**\n     * select the text at position start to end, no matter the children\n     */\n    selectRange(start, end) {\n        let selection = window.getSelection();\n        const { start: currentStart, end: currentEnd } = this.getCurrentSelection();\n        if (currentStart === start && currentEnd === end) {\n            return;\n        }\n        const currentRange = selection.getRangeAt(0);\n        let range;\n        if (this.el.contains(currentRange.startContainer)) {\n            range = currentRange;\n        }\n        else {\n            range = document.createRange();\n            selection.removeAllRanges();\n            selection.addRange(range);\n        }\n        if (start === end && start === 0) {\n            range.setStart(this.el, 0);\n            range.setEnd(this.el, 0);\n        }\n        else {\n            const textLength = this.getText().length;\n            if (start < 0 || end > textLength) {\n                console.warn(`wrong selection asked start ${start}, end ${end}, text content length ${textLength}`);\n                if (start < 0)\n                    start = 0;\n                if (end > textLength)\n                    end = textLength;\n                if (start > textLength)\n                    start = textLength;\n            }\n            let startNode = this.findChildAtCharacterIndex(start);\n            let endNode = this.findChildAtCharacterIndex(end);\n            range.setStart(startNode.node, startNode.offset);\n            selection.extend(endNode.node, endNode.offset);\n        }\n    }\n    /**\n     * finds the dom element that contains the character at `offset`\n     */\n    findChildAtCharacterIndex(offset) {\n        let it = iterateChildren(this.el);\n        let current, previous;\n        let usedCharacters = offset;\n        let isFirstParagraph = true;\n        do {\n            current = it.next();\n            if (!current.done && !current.value.hasChildNodes()) {\n                if (current.value.textContent && current.value.textContent.length < usedCharacters) {\n                    usedCharacters -= current.value.textContent.length;\n                }\n                else if (current.value.textContent &&\n                    current.value.textContent.length >= usedCharacters) {\n                    it.return(current.value);\n                }\n                previous = current.value;\n            }\n            // One new paragraph = one new line character, except for the first paragraph\n            if (!current.done && current.value.nodeName === \"P\") {\n                if (isFirstParagraph) {\n                    isFirstParagraph = false;\n                }\n                else {\n                    usedCharacters--;\n                }\n            }\n        } while (!current.done && usedCharacters);\n        if (current.value) {\n            return { node: current.value, offset: usedCharacters };\n        }\n        return { node: previous, offset: usedCharacters };\n    }\n    /**\n     * Sets (or Replaces all) the text inside the root element in the form of distinctive paragraphs and\n     * span for each element provided in `contents`.\n     *\n     * The function will apply the diff between the current content and the new content to avoid the systematic\n     * destruction of DOM elements which interferes with IME[1]\n     *\n     * Each line of text will be encapsulated in a paragraph element.\n     * Each span will have its own fontcolor and specific class if provided in the HtmlContent object.\n     *\n     * [1] https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor\n     */\n    setText(contents) {\n        if (contents.length === 0) {\n            this.removeAll();\n            return;\n        }\n        const childElements = Array.from(this.el.childNodes);\n        const contentLength = contents.length;\n        for (let i = 0; i < contentLength; i++) {\n            const line = contents[i];\n            const childElement = childElements[i];\n            let newChild = false;\n            let p;\n            if (childElement && childElement.nodeName === \"P\") {\n                p = childElement;\n            }\n            else {\n                newChild = true;\n                p = document.createElement(\"p\");\n            }\n            const lineLength = line.length;\n            const existingChildren = Array.from(p.childNodes);\n            for (let j = 0; j < lineLength; j++) {\n                const content = line[j];\n                const child = existingChildren[j];\n                // child nodes can be multiple types of nodes: Span, Text, Div, etc...\n                // We can only modify a node in place if it has the same type as the content\n                // that we would insert, which are spans.\n                // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste\n                // @ts-ignore (somehow required because jest does not like child.tagName despite the prior check)\n                const childIsSpan = child && \"tagName\" in child && child.tagName === \"SPAN\";\n                if (childIsSpan && compareContentToSpanElement(content, child)) {\n                    continue;\n                }\n                // this is an empty line in the content\n                if (!content.value && !content.class) {\n                    if (child)\n                        p.removeChild(child);\n                    continue;\n                }\n                const span = document.createElement(\"span\");\n                span.innerText = content.value;\n                span.style.color = content.color || \"\";\n                if (content.class) {\n                    span.classList.add(content.class);\n                }\n                if (child) {\n                    p.replaceChild(span, child);\n                }\n                else {\n                    p.appendChild(span);\n                }\n            }\n            if (existingChildren.length > lineLength) {\n                for (let i = lineLength; i < existingChildren.length; i++) {\n                    p.removeChild(existingChildren[i]);\n                }\n            }\n            // Empty line\n            if (!p.hasChildNodes()) {\n                const span = document.createElement(\"span\");\n                span.appendChild(document.createElement(\"br\"));\n                p.appendChild(span);\n            }\n            // replace p if necessary\n            if (newChild) {\n                if (childElement) {\n                    this.el.replaceChild(p, childElement);\n                }\n                else {\n                    this.el.appendChild(p);\n                }\n            }\n        }\n        if (childElements.length > contentLength) {\n            for (let i = contentLength; i < childElements.length; i++) {\n                this.el.removeChild(childElements[i]);\n            }\n        }\n    }\n    scrollSelectionIntoView() {\n        const focusedNode = document.getSelection()?.focusNode;\n        if (!focusedNode || !this.el.contains(focusedNode))\n            return;\n        const element = focusedNode instanceof HTMLElement ? focusedNode : focusedNode.parentElement;\n        element?.scrollIntoView({ block: \"nearest\" });\n    }\n    /**\n     * remove the current selection of the user\n     * */\n    removeSelection() {\n        let selection = window.getSelection();\n        selection.removeAllRanges();\n    }\n    removeAll() {\n        if (this.el) {\n            while (this.el.firstChild) {\n                this.el.removeChild(this.el.firstChild);\n            }\n        }\n    }\n    /**\n     * finds the indexes of the current selection.\n     * */\n    getCurrentSelection() {\n        let { startElement, endElement, startSelectionOffset, endSelectionOffset } = this.getStartAndEndSelection();\n        let startSizeBefore = this.findSelectionIndex(startElement, startSelectionOffset);\n        let endSizeBefore = this.findSelectionIndex(endElement, endSelectionOffset);\n        return {\n            start: startSizeBefore,\n            end: endSizeBefore,\n        };\n    }\n    /**\n     * Computes the text 'index' inside this.el based on the currently selected node and its offset.\n     * The selected node is either a Text node or an Element node.\n     *\n     * case 1 -Text node:\n     * the offset is the number of characters from the start of the node. We have to add this offset to the\n     * content length of all previous nodes.\n     *\n     * case 2 - Element node:\n     * the offset is the number of child nodes before the selected node. We have to add the content length of\n     * all the bnodes prior to the selected node as well as the content of the child node before the offset.\n     *\n     * See the MDN documentation for more details.\n     * https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset\n     * https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset\n     *\n     */\n    findSelectionIndex(nodeToFind, nodeOffset) {\n        let usedCharacters = 0;\n        let it = iterateChildren(this.el);\n        let current = it.next();\n        let isFirstParagraph = true;\n        while (!current.done && current.value !== nodeToFind) {\n            if (!current.value.hasChildNodes()) {\n                if (current.value.textContent) {\n                    usedCharacters += current.value.textContent.length;\n                }\n            }\n            // One new paragraph = one new line character, except for the first paragraph\n            if (current.value.nodeName === \"P\" ||\n                (current.value.nodeName === \"DIV\" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>\n            ) {\n                if (isFirstParagraph) {\n                    isFirstParagraph = false;\n                }\n                else {\n                    usedCharacters++;\n                }\n            }\n            current = it.next();\n        }\n        if (current.value !== nodeToFind) {\n            /** This situation can happen if the code is called while the selection is not currently on the ContentEditableHelper.\n             * In this case, we return 0 because we don't know the size of the text before the selection.\n             *\n             * A known occurence is triggered since the introduction of commit d4663158 (PR #2038).\n             *\n             * FIXME: find a way to test eventhough the selection API is not available in jsDOM.\n             */\n            return 0;\n        }\n        else {\n            if (!current.value.hasChildNodes()) {\n                usedCharacters += nodeOffset;\n            }\n            else {\n                const children = [...current.value.childNodes].slice(0, nodeOffset);\n                usedCharacters += children.reduce((acc, child, index) => {\n                    if (child.textContent !== null) {\n                        // need to account for paragraph nodes that implicitely add a new line\n                        // except for the last paragraph\n                        let chars = child.textContent.length;\n                        if (child.nodeName === \"P\" && index !== children.length - 1) {\n                            chars++;\n                        }\n                        return acc + chars;\n                    }\n                    else {\n                        return acc;\n                    }\n                }, 0);\n            }\n        }\n        if (nodeToFind.nodeName === \"P\" && !isFirstParagraph && nodeToFind.textContent === \"\") {\n            usedCharacters++;\n        }\n        return usedCharacters;\n    }\n    getStartAndEndSelection() {\n        const selection = document.getSelection();\n        return {\n            startElement: selection.anchorNode || this.el,\n            startSelectionOffset: selection.anchorOffset,\n            endElement: selection.focusNode || this.el,\n            endSelectionOffset: selection.focusOffset,\n        };\n    }\n    getText() {\n        let text = \"\";\n        let it = iterateChildren(this.el);\n        let current = it.next();\n        let isFirstParagraph = true;\n        while (!current.done) {\n            if (!current.value.hasChildNodes()) {\n                text += current.value.textContent;\n            }\n            if (current.value.nodeName === \"P\" ||\n                (current.value.nodeName === \"DIV\" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>\n            ) {\n                if (isFirstParagraph) {\n                    isFirstParagraph = false;\n                }\n                else {\n                    text += NEWLINE;\n                }\n            }\n            current = it.next();\n        }\n        return text;\n    }\n}\nfunction compareContentToSpanElement(content, node) {\n    const contentColor = content.color ? toHex(content.color) : \"\";\n    const nodeColor = node.style?.color ? toHex(node.style.color) : \"\";\n    const sameColor = contentColor === nodeColor;\n    const sameClass = deepEquals([content.class], [...node.classList]);\n    const sameContent = node.innerText === content.value;\n    return sameColor && sameClass && sameContent;\n}\n\n// -----------------------------------------------------------------------------\n// Formula Assistant component\n// -----------------------------------------------------------------------------\ncss /* scss */ `\n  .o-formula-assistant {\n    background: #ffffff;\n    .o-formula-assistant-head {\n      background-color: #f2f2f2;\n      padding: 10px;\n    }\n    .collapsed {\n      transform: rotate(180deg);\n    }\n    .o-formula-assistant-core {\n      border-bottom: 1px solid gray;\n    }\n    .o-formula-assistant-arg-description {\n      font-size: 85%;\n    }\n    .o-formula-assistant-focus {\n      div:first-child,\n      span {\n        color: ${COMPOSER_ASSISTANT_COLOR};\n        text-shadow: 0px 0px 1px ${COMPOSER_ASSISTANT_COLOR};\n      }\n      div:last-child {\n        color: black;\n      }\n    }\n    .o-formula-assistant-gray {\n      color: gray;\n    }\n  }\n`;\nclass FunctionDescriptionProvider extends Component {\n    static template = \"o-spreadsheet-FunctionDescriptionProvider\";\n    static props = {\n        functionName: String,\n        functionDescription: Object,\n        argToFocus: Number,\n    };\n    getContext() {\n        return this.props;\n    }\n    get formulaArgSeparator() {\n        return this.env.model.getters.getLocale().formulaArgSeparator + \" \";\n    }\n}\n\nconst functions$2 = functionRegistry.content;\nconst ASSISTANT_WIDTH = 300;\nconst CLOSE_ICON_RADIUS = 9;\nconst selectionIndicatorClass = \"selector-flag\";\nconst selectionIndicatorColor = \"#a9a9a9\";\nconst selectionIndicator = \"\u2423\";\nconst functionColor = \"#4a4e4d\";\nconst operatorColor = \"#3da4ab\";\nconst tokenColors = {\n    OPERATOR: operatorColor,\n    NUMBER: \"#02c39a\",\n    STRING: \"#00a82d\",\n    FUNCTION: functionColor,\n    DEBUGGER: operatorColor,\n    LEFT_PAREN: functionColor,\n    RIGHT_PAREN: functionColor,\n    ARG_SEPARATOR: functionColor,\n    MATCHING_PAREN: \"#000000\",\n};\ncss /* scss */ `\n  .o-composer-container {\n    .o-composer {\n      overflow-y: auto;\n      overflow-x: hidden;\n      word-break: break-all;\n      padding-right: 2px;\n\n      box-sizing: border-box;\n\n      caret-color: black;\n      padding-left: 3px;\n      padding-right: 3px;\n      outline: none;\n\n      p {\n        margin-bottom: 0px;\n\n        span {\n          white-space: pre-wrap;\n          &.${selectionIndicatorClass}:after {\n            content: \"${selectionIndicator}\";\n            color: ${selectionIndicatorColor};\n          }\n        }\n      }\n    }\n    .o-composer[placeholder]:empty:not(:focus):not(.active)::before {\n      content: attr(placeholder);\n      color: #bdbdbd;\n      position: relative;\n      top: 0%;\n      pointer-events: none;\n    }\n\n    .fa-stack {\n      /* reset stack size which is doubled by default */\n      width: ${CLOSE_ICON_RADIUS * 2}px;\n      height: ${CLOSE_ICON_RADIUS * 2}px;\n      line-height: ${CLOSE_ICON_RADIUS * 2}px;\n    }\n\n    .force-open-assistant {\n      left: -1px;\n      top: -1px;\n\n      .fa-question-circle {\n        color: ${PRIMARY_BUTTON_BG};\n      }\n    }\n\n    .o-composer-assistant {\n      position: absolute;\n      margin: 1px 4px;\n\n      .o-semi-bold {\n        /** FIXME: to remove in favor of Bootstrap\n        * 'fw-semibold' when we upgrade to Bootstrap 5.2\n        */\n        font-weight: 600 !important;\n      }\n    }\n  }\n`;\nclass Composer extends Component {\n    static template = \"o-spreadsheet-Composer\";\n    static props = {\n        focus: {\n            validate: (value) => [\"inactive\", \"cellFocus\", \"contentFocus\"].includes(value),\n        },\n        inputStyle: { type: String, optional: true },\n        rect: { type: Object, optional: true },\n        delimitation: { type: Object, optional: true },\n        onComposerCellFocused: { type: Function, optional: true },\n        onComposerContentFocused: Function,\n        isDefaultFocus: { type: Boolean, optional: true },\n        onInputContextMenu: { type: Function, optional: true },\n        composerStore: Object,\n        placeholder: { type: String, optional: true },\n    };\n    static components = { TextValueProvider, FunctionDescriptionProvider };\n    static defaultProps = {\n        inputStyle: \"\",\n        isDefaultFocus: false,\n    };\n    DOMFocusableElementStore;\n    composerRef = useRef(\"o_composer\");\n    contentHelper = new ContentEditableHelper(this.composerRef.el);\n    composerState = useState({\n        positionStart: 0,\n        positionEnd: 0,\n    });\n    autoCompleteState;\n    functionDescriptionState = useState({\n        showDescription: false,\n        functionName: \"\",\n        functionDescription: {},\n        argToFocus: 0,\n    });\n    assistant = useState({\n        forcedClosed: false,\n    });\n    compositionActive = false;\n    spreadsheetRect = useSpreadsheetRect();\n    get assistantStyle() {\n        const composerRect = this.composerRef.el.getBoundingClientRect();\n        const assistantStyle = {};\n        assistantStyle[\"min-width\"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;\n        const proposals = this.autoCompleteState.provider?.proposals;\n        const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);\n        if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {\n            assistantStyle.width = `${ASSISTANT_WIDTH}px`;\n        }\n        if (this.props.delimitation && this.props.rect) {\n            const { x: cellX, y: cellY, height: cellHeight } = this.props.rect;\n            const remainingHeight = this.props.delimitation.height - (cellY + cellHeight);\n            assistantStyle[\"max-height\"] = `${remainingHeight}px`;\n            if (cellY > remainingHeight) {\n                const availableSpaceAbove = cellY;\n                assistantStyle[\"max-height\"] = `${availableSpaceAbove - CLOSE_ICON_RADIUS}px`;\n                // render top\n                // We compensate 2 px of margin on the assistant style + 1px for design reasons\n                assistantStyle.top = `-3px`;\n                assistantStyle.transform = `translate(0, -100%)`;\n            }\n            if (cellX + ASSISTANT_WIDTH > this.props.delimitation.width) {\n                // render left\n                assistantStyle.right = `0px`;\n            }\n        }\n        else {\n            assistantStyle[\"max-height\"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;\n            if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >\n                this.spreadsheetRect.width) {\n                assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;\n            }\n        }\n        return cssPropertiesToCss(assistantStyle);\n    }\n    // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent\n    shouldProcessInputEvents = false;\n    tokens = [];\n    keyMapping = {\n        Enter: (ev) => this.processEnterKey(ev, \"down\"),\n        \"Shift+Enter\": (ev) => this.processEnterKey(ev, \"up\"),\n        \"Alt+Enter\": this.processNewLineEvent,\n        \"Ctrl+Enter\": this.processNewLineEvent,\n        Escape: this.processEscapeKey,\n        F2: () => console.warn(\"Not implemented\"),\n        F4: (ev) => this.processF4Key(ev),\n        Tab: (ev) => this.processTabKey(ev, \"right\"),\n        \"Shift+Tab\": (ev) => this.processTabKey(ev, \"left\"),\n    };\n    keyCodeMapping = {\n        NumpadDecimal: this.processNumpadDecimal,\n    };\n    setup() {\n        this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);\n        this.autoCompleteState = useLocalStore(AutoCompleteStore);\n        onMounted(() => {\n            const el = this.composerRef.el;\n            if (this.props.isDefaultFocus) {\n                this.DOMFocusableElementStore.setFocusableElement(el);\n            }\n            this.contentHelper.updateEl(el);\n        });\n        useEffect(() => {\n            this.processContent();\n            if (document.activeElement === this.contentHelper.el &&\n                this.props.composerStore.editionMode === \"inactive\" &&\n                !this.props.isDefaultFocus) {\n                this.DOMFocusableElementStore.focus();\n            }\n        });\n        useEffect(() => {\n            this.processTokenAtCursor();\n        }, () => [this.props.composerStore.editionMode !== \"inactive\"]);\n    }\n    // ---------------------------------------------------------------------------\n    // Handlers\n    // ---------------------------------------------------------------------------\n    processArrowKeys(ev) {\n        const tokenAtCursor = this.props.composerStore.tokenAtCursor;\n        if ((this.props.composerStore.isSelectingRange ||\n            this.props.composerStore.editionMode === \"inactive\") &&\n            !([\"ArrowUp\", \"ArrowDown\"].includes(ev.key) &&\n                this.autoCompleteState.provider &&\n                tokenAtCursor?.type !== \"REFERENCE\")) {\n            this.functionDescriptionState.showDescription = false;\n            this.autoCompleteState.hide();\n            // Prevent the default content editable behavior which moves the cursor\n            ev.preventDefault();\n            ev.stopPropagation();\n            updateSelectionWithArrowKeys(ev, this.env.model.selection);\n            return;\n        }\n        const content = this.props.composerStore.currentContent;\n        if (this.props.focus === \"cellFocus\" &&\n            !this.autoCompleteState.provider &&\n            !content.startsWith(\"=\")) {\n            this.props.composerStore.stopEdition();\n            return;\n        }\n        // All arrow keys are processed: up and down should move autocomplete, left\n        // and right should move the cursor.\n        ev.stopPropagation();\n        this.handleArrowKeysForAutocomplete(ev);\n    }\n    handleArrowKeysForAutocomplete(ev) {\n        // only for arrow up and down\n        if ([\"ArrowUp\", \"ArrowDown\"].includes(ev.key) && this.autoCompleteState.provider) {\n            ev.preventDefault();\n            this.autoCompleteState.moveSelection(ev.key === \"ArrowDown\" ? \"next\" : \"previous\");\n        }\n    }\n    processTabKey(ev, direction) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        if (this.props.composerStore.editionMode !== \"inactive\") {\n            const state = this.autoCompleteState;\n            if (state.provider && state.selectedIndex !== undefined) {\n                const autoCompleteValue = state.provider.proposals[state.selectedIndex]?.text;\n                if (autoCompleteValue) {\n                    this.autoComplete(autoCompleteValue);\n                    return;\n                }\n            }\n            this.props.composerStore.stopEdition(direction);\n        }\n    }\n    processEnterKey(ev, direction) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        const state = this.autoCompleteState;\n        if (state.provider && state.selectedIndex !== undefined) {\n            const autoCompleteValue = state.provider.proposals[state.selectedIndex]?.text;\n            if (autoCompleteValue) {\n                this.autoComplete(autoCompleteValue);\n                return;\n            }\n        }\n        this.props.composerStore.stopEdition(direction);\n    }\n    processNewLineEvent(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        const content = this.contentHelper.getText();\n        const selection = this.contentHelper.getCurrentSelection();\n        const start = Math.min(selection.start, selection.end);\n        const end = Math.max(selection.start, selection.end);\n        this.props.composerStore.stopComposerRangeSelection();\n        this.props.composerStore.setCurrentContent(content.slice(0, start) + NEWLINE + content.slice(end), {\n            start: start + 1,\n            end: start + 1,\n        });\n        this.processContent();\n    }\n    processEscapeKey(ev) {\n        this.props.composerStore.cancelEdition();\n        ev.stopPropagation();\n        ev.preventDefault();\n    }\n    processF4Key(ev) {\n        ev.stopPropagation();\n        this.props.composerStore.cycleReferences();\n        this.processContent();\n    }\n    processNumpadDecimal(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        const locale = this.env.model.getters.getLocale();\n        const selection = this.contentHelper.getCurrentSelection();\n        const currentContent = this.props.composerStore.currentContent;\n        const content = currentContent.slice(0, selection.start) +\n            locale.decimalSeparator +\n            currentContent.slice(selection.end);\n        // Update composer even by hand rather than dispatching an InputEvent because untrusted inputs\n        // events aren't handled natively by contentEditable\n        this.props.composerStore.setCurrentContent(content, {\n            start: selection.start + 1,\n            end: selection.start + 1,\n        });\n        // We need to do the process content here in case there is no render between the keyDown and the\n        // keyUp event\n        this.processContent();\n    }\n    onCompositionStart() {\n        this.compositionActive = true;\n    }\n    onCompositionEnd() {\n        this.compositionActive = false;\n    }\n    onKeydown(ev) {\n        if (this.props.composerStore.editionMode === \"inactive\") {\n            return;\n        }\n        if (ev.key.startsWith(\"Arrow\")) {\n            this.processArrowKeys(ev);\n            return;\n        }\n        let handler = this.keyMapping[keyboardEventToShortcutString(ev)] ||\n            this.keyCodeMapping[keyboardEventToShortcutString(ev, \"code\")];\n        if (handler) {\n            handler.call(this, ev);\n        }\n        else {\n            ev.stopPropagation();\n        }\n    }\n    onPaste(ev) {\n        if (this.props.composerStore.editionMode !== \"inactive\") {\n            // let the browser clipboard work\n            ev.stopPropagation();\n        }\n        else {\n            // the user meant to paste in the sheet, not open the composer with the pasted content\n            // While we're not editing, we still have the focus and should therefore prevent\n            // the native \"paste\" to occur.\n            ev.preventDefault();\n        }\n    }\n    /*\n     * Triggered automatically by the content-editable between the keydown and key up\n     * */\n    onInput(ev) {\n        if (!this.shouldProcessInputEvents) {\n            return;\n        }\n        ev.stopPropagation();\n        let content;\n        if (this.props.composerStore.editionMode === \"inactive\") {\n            content = ev.data || \"\";\n        }\n        else {\n            content = this.contentHelper.getText();\n        }\n        if (this.props.focus === \"inactive\") {\n            return this.props.onComposerCellFocused?.(content);\n        }\n        let selection = this.contentHelper.getCurrentSelection();\n        this.props.composerStore.stopComposerRangeSelection();\n        this.props.composerStore.setCurrentContent(content, selection);\n        this.processTokenAtCursor();\n    }\n    onKeyup(ev) {\n        if (this.contentHelper.el === document.activeElement) {\n            if (this.autoCompleteState.provider && [\"ArrowUp\", \"ArrowDown\"].includes(ev.key)) {\n                return;\n            }\n            if (this.props.composerStore.isSelectingRange && ev.key?.startsWith(\"Arrow\")) {\n                return;\n            }\n            const { start: oldStart, end: oldEnd } = this.props.composerStore.composerSelection;\n            const { start, end } = this.contentHelper.getCurrentSelection();\n            if (start !== oldStart || end !== oldEnd) {\n                this.props.composerStore.changeComposerCursorSelection(start, end);\n            }\n            this.processTokenAtCursor();\n        }\n    }\n    onBlur(ev) {\n        if (this.props.composerStore.editionMode === \"inactive\") {\n            return;\n        }\n        const target = ev.relatedTarget;\n        if (!target || !(target instanceof HTMLElement)) {\n            this.props.composerStore.stopEdition();\n            return;\n        }\n        if (target.attributes.getNamedItem(\"composerFocusableElement\")) {\n            this.contentHelper.el.focus();\n            return;\n        }\n        if (target.classList.contains(\"o-composer\")) {\n            return;\n        }\n        this.props.composerStore.stopEdition();\n    }\n    updateAutoCompleteIndex(index) {\n        this.autoCompleteState.selectIndex(clip(0, index, 10));\n    }\n    /**\n     * This is required to ensure the content helper selection is\n     * properly updated on \"onclick\" events. Depending on the browser,\n     * the callback onClick from the composer will be executed before\n     * the selection was updated in the dom, which means we capture an\n     * wrong selection which is then forced upon the content helper on\n     * processContent.\n     */\n    onMousedown(ev) {\n        if (ev.button > 0) {\n            // not main button, probably a context menu\n            return;\n        }\n        this.contentHelper.removeSelection();\n    }\n    onClick() {\n        if (this.env.model.getters.isReadonly()) {\n            return;\n        }\n        const newSelection = this.contentHelper.getCurrentSelection();\n        this.props.composerStore.stopComposerRangeSelection();\n        this.props.onComposerContentFocused();\n        this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);\n        this.processTokenAtCursor();\n    }\n    onDblClick() {\n        if (this.env.model.getters.isReadonly()) {\n            return;\n        }\n        const composerContent = this.props.composerStore.currentContent;\n        const isValidFormula = composerContent.startsWith(\"=\");\n        if (isValidFormula) {\n            const tokens = this.props.composerStore.currentTokens;\n            const currentSelection = this.contentHelper.getCurrentSelection();\n            if (currentSelection.start === currentSelection.end)\n                return;\n            const currentSelectedText = composerContent.substring(currentSelection.start, currentSelection.end);\n            const token = tokens.filter((token) => token.value.includes(currentSelectedText) &&\n                token.start <= currentSelection.start &&\n                token.end >= currentSelection.end)[0];\n            if (!token) {\n                return;\n            }\n            if (token.type === \"REFERENCE\") {\n                this.props.composerStore.changeComposerCursorSelection(token.start, token.end);\n            }\n        }\n    }\n    onContextMenu(ev) {\n        if (this.props.composerStore.editionMode === \"inactive\") {\n            this.props.onInputContextMenu?.(ev);\n        }\n    }\n    closeAssistant() {\n        this.assistant.forcedClosed = true;\n    }\n    openAssistant() {\n        this.assistant.forcedClosed = false;\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    processContent() {\n        if (this.compositionActive) {\n            return;\n        }\n        this.shouldProcessInputEvents = false;\n        if (this.props.focus !== \"inactive\" && document.activeElement !== this.contentHelper.el) {\n            this.contentHelper.el.focus();\n        }\n        const content = this.getContentLines();\n        this.contentHelper.setText(content);\n        if (content.length !== 0 && content.length[0] !== 0) {\n            if (this.props.focus !== \"inactive\") {\n                // Put the cursor back where it was before the rendering\n                const { start, end } = this.props.composerStore.composerSelection;\n                this.contentHelper.selectRange(start, end);\n            }\n            this.contentHelper.scrollSelectionIntoView();\n        }\n        this.shouldProcessInputEvents = true;\n    }\n    /**\n     * Get the HTML content corresponding to the current composer token, divided by lines.\n     */\n    getContentLines() {\n        let value = this.props.composerStore.currentContent;\n        const isValidFormula = value.startsWith(\"=\");\n        if (value === \"\") {\n            return [];\n        }\n        else if (isValidFormula && this.props.focus !== \"inactive\") {\n            return this.splitHtmlContentIntoLines(this.getColoredTokens());\n        }\n        return this.splitHtmlContentIntoLines([{ value }]);\n    }\n    getColoredTokens() {\n        const tokens = this.props.composerStore.currentTokens;\n        const tokenAtCursor = this.props.composerStore.tokenAtCursor;\n        const result = [];\n        const { end, start } = this.props.composerStore.composerSelection;\n        for (const token of tokens) {\n            switch (token.type) {\n                case \"OPERATOR\":\n                case \"NUMBER\":\n                case \"ARG_SEPARATOR\":\n                case \"STRING\":\n                    result.push({ value: token.value, color: tokenColors[token.type] || \"#000\" });\n                    break;\n                case \"REFERENCE\":\n                    const { xc, sheetName } = splitReference(token.value);\n                    result.push({ value: token.value, color: this.rangeColor(xc, sheetName) || \"#000\" });\n                    break;\n                case \"SYMBOL\":\n                    const value = token.value;\n                    const upperCaseValue = value.toUpperCase();\n                    if (upperCaseValue === \"TRUE\" || upperCaseValue === \"FALSE\") {\n                        result.push({ value: token.value, color: tokenColors.NUMBER });\n                    }\n                    else if (upperCaseValue in functionRegistry.content) {\n                        result.push({ value: token.value, color: tokenColors.FUNCTION });\n                    }\n                    else {\n                        result.push({ value: token.value, color: \"#000\" });\n                    }\n                    break;\n                case \"LEFT_PAREN\":\n                case \"RIGHT_PAREN\":\n                    // Compute the matching parenthesis\n                    if (tokenAtCursor &&\n                        [\"LEFT_PAREN\", \"RIGHT_PAREN\"].includes(tokenAtCursor.type) &&\n                        tokenAtCursor.parenIndex &&\n                        tokenAtCursor.parenIndex === token.parenIndex) {\n                        result.push({ value: token.value, color: tokenColors.MATCHING_PAREN || \"#000\" });\n                    }\n                    else {\n                        result.push({ value: token.value, color: tokenColors[token.type] || \"#000\" });\n                    }\n                    break;\n                default:\n                    result.push({ value: token.value, color: \"#000\" });\n                    break;\n            }\n            if (this.props.composerStore.showSelectionIndicator && end === start && end === token.end) {\n                result[result.length - 1].class = selectionIndicatorClass;\n            }\n        }\n        return result;\n    }\n    /**\n     * Split an array of HTMLContents into lines. Each NEWLINE character encountered will create a new\n     * line. Contents can be split into multiple parts if they contain multiple NEWLINE characters.\n     */\n    splitHtmlContentIntoLines(contents) {\n        const contentSplitInLines = [];\n        let currentLine = [];\n        for (const content of contents) {\n            if (content.value.includes(NEWLINE)) {\n                const lines = content.value.split(NEWLINE);\n                const lastLine = lines.pop();\n                for (const line of lines) {\n                    currentLine.push({ color: content.color, value: line }); // don't copy class, only last line should keep it\n                    contentSplitInLines.push(currentLine);\n                    currentLine = [];\n                }\n                currentLine.push({ ...content, value: lastLine });\n            }\n            else {\n                currentLine.push(content);\n            }\n        }\n        if (currentLine.length) {\n            contentSplitInLines.push(currentLine);\n        }\n        // Remove useless empty contents\n        const filteredLines = [];\n        for (const line of contentSplitInLines) {\n            if (line.every(this.isContentEmpty)) {\n                filteredLines.push([line[0]]);\n            }\n            else {\n                filteredLines.push(line.filter((content) => !this.isContentEmpty(content)));\n            }\n        }\n        return filteredLines;\n    }\n    isContentEmpty(content) {\n        return !(content.value || content.class);\n    }\n    rangeColor(xc, sheetName) {\n        if (this.props.focus === \"inactive\") {\n            return undefined;\n        }\n        const highlights = this.props.composerStore.highlights;\n        const refSheet = sheetName\n            ? this.env.model.getters.getSheetIdByName(sheetName)\n            : this.props.composerStore.sheetId;\n        const highlight = highlights.find((highlight) => {\n            if (highlight.sheetId !== refSheet)\n                return false;\n            const range = this.env.model.getters.getRangeFromSheetXC(refSheet, xc);\n            let zone = range.zone;\n            zone = getZoneArea(zone) === 1 ? this.env.model.getters.expandZone(refSheet, zone) : zone;\n            return isEqual(zone, highlight.zone);\n        });\n        return highlight && highlight.color ? highlight.color : undefined;\n    }\n    /**\n     * Compute the state of the composer from the tokenAtCursor.\n     * If the token is a function or symbol (that isn't a cell/range reference) we have to initialize\n     * the autocomplete engine otherwise we initialize the formula assistant.\n     */\n    processTokenAtCursor() {\n        let content = this.props.composerStore.currentContent;\n        if (this.autoCompleteState.provider) {\n            this.autoCompleteState.hide();\n        }\n        this.functionDescriptionState.showDescription = false;\n        const autoCompleteProvider = this.props.composerStore.autocompleteProvider;\n        if (autoCompleteProvider) {\n            this.autoCompleteState.useProvider(autoCompleteProvider);\n        }\n        const token = this.props.composerStore.tokenAtCursor;\n        if (content.startsWith(\"=\") && token && token.type !== \"SYMBOL\") {\n            const tokenContext = token.functionContext;\n            const parentFunction = tokenContext?.parent.toUpperCase();\n            if (tokenContext &&\n                parentFunction &&\n                parentFunction in functions$2 &&\n                token.type !== \"UNKNOWN\") {\n                // initialize Formula Assistant\n                const description = functions$2[parentFunction];\n                const argPosition = tokenContext.argPosition;\n                this.functionDescriptionState.functionName = parentFunction;\n                this.functionDescriptionState.functionDescription = description;\n                this.functionDescriptionState.argToFocus = description.getArgToFocus(argPosition + 1) - 1;\n                this.functionDescriptionState.showDescription = true;\n            }\n        }\n    }\n    autoComplete(value) {\n        if (!value || this.assistant.forcedClosed) {\n            return;\n        }\n        this.autoCompleteState.provider?.selectProposal(value);\n        this.processTokenAtCursor();\n    }\n}\n\nclass FunctionCodeBuilder {\n    scope;\n    code = \"\";\n    constructor(scope = new Scope()) {\n        this.scope = scope;\n    }\n    append(...lines) {\n        this.code += lines.map((line) => line.toString()).join(\"\\n\") + \"\\n\";\n    }\n    return(expression) {\n        return new FunctionCodeImpl(this.scope, this.code, expression);\n    }\n    toString() {\n        return indentCode(this.code);\n    }\n}\nclass FunctionCodeImpl {\n    scope;\n    returnExpression;\n    code;\n    constructor(scope, code, returnExpression) {\n        this.scope = scope;\n        this.returnExpression = returnExpression;\n        this.code = indentCode(code);\n    }\n    toString() {\n        return this.code;\n    }\n    assignResultToVariable() {\n        if (this.scope.isAlreadyDeclared(this.returnExpression)) {\n            return this;\n        }\n        const variableName = this.scope.nextVariableName();\n        const code = new FunctionCodeBuilder(this.scope);\n        code.append(this.code);\n        code.append(`const ${variableName} = ${this.returnExpression};`);\n        return code.return(variableName);\n    }\n}\nclass Scope {\n    nextId = 1;\n    declaredVariables = new Set();\n    nextVariableName() {\n        const name = `_${this.nextId++}`;\n        this.declaredVariables.add(name);\n        return name;\n    }\n    isAlreadyDeclared(name) {\n        return this.declaredVariables.has(name);\n    }\n}\n/**\n * Takes a list of strings that might be single or multiline\n * and maps them in a list of single line strings.\n */\nfunction splitLines(str) {\n    return str\n        .split(\"\\n\")\n        .map((line) => line.trim())\n        .filter((line) => line !== \"\");\n}\nfunction indentCode(code) {\n    let result = \"\";\n    let indentLevel = 0;\n    const lines = splitLines(code);\n    for (const line of lines) {\n        if (line.startsWith(\"}\")) {\n            indentLevel--;\n        }\n        result += \"\\t\".repeat(indentLevel) + line + \"\\n\";\n        if (line.endsWith(\"{\")) {\n            indentLevel++;\n        }\n    }\n    return result.trim();\n}\n\nconst functions$1 = functionRegistry.content;\nconst OPERATOR_MAP = {\n    // export for test\n    \"=\": \"EQ\",\n    \"+\": \"ADD\",\n    \"-\": \"MINUS\",\n    \"*\": \"MULTIPLY\",\n    \"/\": \"DIVIDE\",\n    \">=\": \"GTE\",\n    \"<>\": \"NE\",\n    \">\": \"GT\",\n    \"<=\": \"LTE\",\n    \"<\": \"LT\",\n    \"^\": \"POWER\",\n    \"&\": \"CONCATENATE\",\n};\nconst UNARY_OPERATOR_MAP = {\n    // export for test\n    \"-\": \"UMINUS\",\n    \"+\": \"UPLUS\",\n    \"%\": \"UNARY.PERCENT\",\n};\n// this cache contains all compiled function code, grouped by \"structure\". For\n// example, \"=2*sum(A1:A4)\" and \"=2*sum(B1:B4)\" are compiled into the same\n// structural function.\n// It is only exported for testing purposes\nconst functionCache = {};\n// -----------------------------------------------------------------------------\n// COMPILER\n// -----------------------------------------------------------------------------\nfunction compile(formula) {\n    const tokens = rangeTokenize(formula);\n    return compileTokens(tokens);\n}\nfunction compileTokens(tokens) {\n    try {\n        return compileTokensOrThrow(tokens);\n    }\n    catch (error) {\n        return {\n            tokens,\n            dependencies: [],\n            execute: function () {\n                return error;\n            },\n            isBadExpression: true,\n        };\n    }\n}\nfunction compileTokensOrThrow(tokens) {\n    const { dependencies, constantValues, symbols } = formulaArguments(tokens);\n    const cacheKey = compilationCacheKey(tokens, dependencies, constantValues);\n    if (!functionCache[cacheKey]) {\n        const ast = parseTokens([...tokens]);\n        const scope = new Scope();\n        if (ast.type === \"BIN_OPERATION\" && ast.value === \":\") {\n            throw new BadExpressionError(_t(\"Invalid formula\"));\n        }\n        if (ast.type === \"EMPTY\") {\n            throw new BadExpressionError(_t(\"Invalid formula\"));\n        }\n        const compiledAST = compileAST(ast);\n        const code = new FunctionCodeBuilder();\n        code.append(`// ${cacheKey}`);\n        code.append(compiledAST);\n        code.append(`return ${compiledAST.returnExpression};`);\n        let baseFunction = new Function(\"deps\", // the dependencies in the current formula\n        \"ref\", // a function to access a certain dependency at a given index\n        \"range\", // same as above, but guarantee that the result is in the form of a range\n        \"getSymbolValue\", \"ctx\", code.toString());\n        // @ts-ignore\n        functionCache[cacheKey] = baseFunction;\n        /**\n         * This function compile the function arguments. It is mostly straightforward,\n         * except that there is a non trivial transformation in one situation:\n         *\n         * If a function argument is asking for a range, and get a cell, we transform\n         * the cell value into a range. This allow the grid model to differentiate\n         * between a cell value and a non cell value.\n         */\n        function compileFunctionArgs(ast) {\n            const { args } = ast;\n            const functionName = ast.value.toUpperCase();\n            const functionDefinition = functions$1[functionName];\n            if (!functionDefinition) {\n                throw new UnknownFunctionError(_t('Unknown function: \"%s\"', ast.value));\n            }\n            assertEnoughArgs(ast);\n            const compiledArgs = [];\n            for (let i = 0; i < args.length; i++) {\n                const argToFocus = functionDefinition.getArgToFocus(i + 1) - 1;\n                const argDefinition = functionDefinition.args[argToFocus];\n                const currentArg = args[i];\n                const argTypes = argDefinition.type || [];\n                // detect when an argument need to be evaluated as a meta argument\n                const isMeta = argTypes.includes(\"META\");\n                const hasRange = argTypes.some((t) => isRangeType(t));\n                compiledArgs.push(compileAST(currentArg, isMeta, hasRange));\n            }\n            return compiledArgs;\n        }\n        /**\n         * This function compiles all the information extracted by the parser into an\n         * executable code for the evaluation of the cells content. It uses a cache to\n         * not reevaluate identical code structures.\n         *\n         * The function is sensitive to parameter \u201cisMeta\u201d. This\n         * parameter may vary when compiling function arguments:\n         * isMeta: In some cases the function arguments expects information on the\n         * cell/range other than the associated value(s). For example the COLUMN\n         * function needs to receive as argument the coordinates of a cell rather\n         * than its value. For this we have meta arguments.\n         */\n        function compileAST(ast, isMeta = false, hasRange = false) {\n            const code = new FunctionCodeBuilder(scope);\n            if (ast.type !== \"REFERENCE\" && !(ast.type === \"BIN_OPERATION\" && ast.value === \":\")) {\n                if (isMeta) {\n                    throw new BadExpressionError(_t(\"Argument must be a reference to a cell or range.\"));\n                }\n            }\n            if (ast.debug) {\n                code.append(\"debugger;\");\n            }\n            switch (ast.type) {\n                case \"BOOLEAN\":\n                    return code.return(`{ value: ${ast.value} }`);\n                case \"NUMBER\":\n                    return code.return(`{ value: this.constantValues.numbers[${constantValues.numbers.indexOf(ast.value)}] }`);\n                case \"STRING\":\n                    return code.return(`{ value: this.constantValues.strings[${constantValues.strings.indexOf(ast.value)}] }`);\n                case \"REFERENCE\":\n                    const referenceIndex = dependencies.indexOf(ast.value);\n                    if ((!isMeta && ast.value.includes(\":\")) || hasRange) {\n                        return code.return(`range(deps[${referenceIndex}])`);\n                    }\n                    else {\n                        return code.return(`ref(deps[${referenceIndex}], ${isMeta ? \"true\" : \"false\"})`);\n                    }\n                case \"FUNCALL\":\n                    const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());\n                    code.append(...args);\n                    const fnName = ast.value.toUpperCase();\n                    return code.return(`ctx['${fnName}'](${args.map((arg) => arg.returnExpression)})`);\n                case \"UNARY_OPERATION\": {\n                    const fnName = UNARY_OPERATOR_MAP[ast.value];\n                    const operand = compileAST(ast.operand, false, false).assignResultToVariable();\n                    code.append(operand);\n                    return code.return(`ctx['${fnName}'](${operand.returnExpression})`);\n                }\n                case \"BIN_OPERATION\": {\n                    const fnName = OPERATOR_MAP[ast.value];\n                    const left = compileAST(ast.left, false, false).assignResultToVariable();\n                    const right = compileAST(ast.right, false, false).assignResultToVariable();\n                    code.append(left);\n                    code.append(right);\n                    return code.return(`ctx['${fnName}'](${left.returnExpression}, ${right.returnExpression})`);\n                }\n                case \"SYMBOL\":\n                    const symbolIndex = symbols.indexOf(ast.value);\n                    return code.return(`getSymbolValue(this.symbols[${symbolIndex}])`);\n                case \"EMPTY\":\n                    return code.return(\"undefined\");\n            }\n        }\n    }\n    const compiledFormula = {\n        execute: functionCache[cacheKey],\n        dependencies,\n        constantValues,\n        symbols,\n        tokens,\n        isBadExpression: false,\n    };\n    return compiledFormula;\n}\n/**\n * Compute a cache key for the formula.\n * References, numbers and strings are replaced with placeholders because\n * the compiled formula does not depend on their actual value.\n * Both `=A1+1+\"2\"` and `=A2+2+\"3\"` are compiled to the exact same function.\n *\n * Spaces are also ignored to compute the cache key.\n *\n * A formula `=A1+A2+SUM(2, 2, \"2\")` have the cache key `=|0|+|1|+SUM(|N0|,|N0|,|S0|)`\n */\nfunction compilationCacheKey(tokens, dependencies, constantValues, symbols) {\n    let cacheKey = \"\";\n    for (const token of tokens) {\n        switch (token.type) {\n            case \"STRING\":\n                const value = removeStringQuotes(token.value);\n                cacheKey += `|S${constantValues.strings.indexOf(value)}|`;\n                break;\n            case \"NUMBER\":\n                cacheKey += `|N${constantValues.numbers.indexOf(parseNumber(token.value, DEFAULT_LOCALE))}|`;\n                break;\n            case \"REFERENCE\":\n            case \"INVALID_REFERENCE\":\n                if (token.value.includes(\":\")) {\n                    cacheKey += `R|${dependencies.indexOf(token.value)}|`;\n                }\n                else {\n                    cacheKey += `C|${dependencies.indexOf(token.value)}|`;\n                }\n                break;\n            case \"SPACE\":\n                cacheKey += \"\";\n                break;\n            default:\n                cacheKey += token.value;\n                break;\n        }\n    }\n    return cacheKey;\n}\n/**\n * Return formula arguments which are references, strings and numbers.\n */\nfunction formulaArguments(tokens) {\n    const constantValues = {\n        numbers: [],\n        strings: [],\n    };\n    const dependencies = [];\n    const symbols = [];\n    for (const token of tokens) {\n        switch (token.type) {\n            case \"INVALID_REFERENCE\":\n            case \"REFERENCE\":\n                dependencies.push(token.value);\n                break;\n            case \"STRING\":\n                const value = removeStringQuotes(token.value);\n                if (!constantValues.strings.includes(value)) {\n                    constantValues.strings.push(value);\n                }\n                break;\n            case \"NUMBER\": {\n                const value = parseNumber(token.value, DEFAULT_LOCALE);\n                if (!constantValues.numbers.includes(value)) {\n                    constantValues.numbers.push(value);\n                }\n                break;\n            }\n            case \"SYMBOL\": {\n                // function name symbols are also included here\n                symbols.push(unquote(token.value, \"'\"));\n            }\n        }\n    }\n    return {\n        dependencies,\n        constantValues,\n        symbols,\n    };\n}\n/**\n * Check if arguments are supplied in the correct quantities\n */\nfunction assertEnoughArgs(ast) {\n    const nbrArg = ast.args.length;\n    const functionName = ast.value.toUpperCase();\n    const functionDefinition = functions$1[functionName];\n    if (nbrArg < functionDefinition.minArgRequired) {\n        throw new BadExpressionError(_t(\"Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.\", functionName, functionDefinition.minArgRequired.toString(), nbrArg.toString()));\n    }\n    if (nbrArg > functionDefinition.maxArgPossible) {\n        throw new BadExpressionError(_t(\"Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.\", functionName, functionDefinition.maxArgPossible.toString(), nbrArg.toString()));\n    }\n    const repeatableArgs = functionDefinition.nbrArgRepeating;\n    if (repeatableArgs > 1) {\n        const unrepeatableArgs = functionDefinition.args.length - repeatableArgs;\n        const repeatingArgs = nbrArg - unrepeatableArgs;\n        if (repeatingArgs % repeatableArgs !== 0) {\n            throw new BadExpressionError(_t(\"Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments\", functionName, unrepeatableArgs.toString(), repeatableArgs.toString()));\n        }\n    }\n}\nfunction isRangeType(type) {\n    return type.startsWith(\"RANGE\");\n}\n\nconst functions = functionRegistry.content;\nfunction isExportableToExcel(tokens) {\n    try {\n        const nonExportableFunctions = iterateAstNodes(parseTokens(tokens)).filter((ast) => ast.type === \"FUNCALL\" && !functions[ast.value.toUpperCase()]?.isExported);\n        return nonExportableFunctions.length === 0;\n    }\n    catch (error) {\n        return false;\n    }\n}\nfunction getFunctionsFromTokens(tokens, functionNames) {\n    // Parsing is an expensive operation, so we first check if the\n    // formula contains one of the function names\n    if (!tokens.some((t) => t.type === \"SYMBOL\" && functionNames.includes(t.value.toUpperCase()))) {\n        return [];\n    }\n    let ast;\n    try {\n        ast = parseTokens(tokens);\n    }\n    catch {\n        return [];\n    }\n    return getFunctionsFromAST(ast, functionNames);\n}\nfunction getFunctionsFromAST(ast, functionNames) {\n    return iterateAstNodes(ast)\n        .filter((node) => node.type === \"FUNCALL\" && functionNames.includes(node.value.toUpperCase()))\n        .map((node) => ({\n        functionName: node.value.toUpperCase(),\n        args: node.args,\n    }));\n}\n\nconst PIVOT_FUNCTIONS = [\"PIVOT.VALUE\", \"PIVOT.HEADER\", \"PIVOT\"];\n/**\n * Create a proposal entry for the compose autowcomplete\n * to insert a field name string in a formula.\n */\nfunction makeFieldProposal(field, granularity) {\n    const groupBy = granularity ? `${field.name}:${granularity}` : field.name;\n    const quotedGroupBy = `\"${groupBy}\"`;\n    const fuzzySearchKey = field.string !== field.name\n        ? field.string + quotedGroupBy // search on translated name and on technical name\n        : quotedGroupBy;\n    return {\n        text: quotedGroupBy,\n        description: field.string + (field.help ? ` (${field.help})` : \"\"),\n        htmlContent: [{ value: quotedGroupBy, color: tokenColors.STRING }],\n        fuzzySearchKey,\n    };\n}\nfunction makeMeasureProposal(measure) {\n    const quotedMeasure = `\"${measure.id}\"`;\n    const fuzzySearchKey = measure.displayName + measure.fieldName + quotedMeasure;\n    return {\n        text: quotedMeasure,\n        description: measure.displayName,\n        htmlContent: [{ value: quotedMeasure, color: tokenColors.STRING }],\n        fuzzySearchKey,\n    };\n}\n/**\n * Perform the autocomplete of the composer by inserting the value\n * at the cursor position, replacing the current token if necessary.\n * Must be bound to the autocomplete provider.\n */\nfunction insertTokenAfterArgSeparator(tokenAtCursor, value) {\n    let start = tokenAtCursor.end;\n    const end = tokenAtCursor.end;\n    if (tokenAtCursor.type !== \"ARG_SEPARATOR\") {\n        // replace the whole token\n        start = tokenAtCursor.start;\n    }\n    this.composer.stopComposerRangeSelection();\n    this.composer.changeComposerCursorSelection(start, end);\n    this.composer.replaceComposerCursorSelection(value);\n}\n/**\n * Perform the autocomplete of the composer by inserting the value\n * at the cursor position, replacing the current token if necessary.\n * Must be bound to the autocomplete provider.\n * @param {EnrichedToken} tokenAtCursor\n * @param {string} value\n */\nfunction insertTokenAfterLeftParenthesis(tokenAtCursor, value) {\n    let start = tokenAtCursor.end;\n    const end = tokenAtCursor.end;\n    if (tokenAtCursor.type !== \"LEFT_PAREN\") {\n        // replace the whole token\n        start = tokenAtCursor.start;\n    }\n    this.composer.stopComposerRangeSelection();\n    this.composer.changeComposerCursorSelection(start, end);\n    this.composer.replaceComposerCursorSelection(value);\n}\n/**\n * Extract the pivot id (always the first argument) from the function\n * context of the given token.\n */\nfunction extractFormulaIdFromToken(tokenAtCursor) {\n    const idAst = tokenAtCursor.functionContext?.args[0];\n    if (!idAst || ![\"STRING\", \"NUMBER\"].includes(idAst.type)) {\n        return;\n    }\n    return idAst.value;\n}\n/**\n * Get the first Pivot function description of the given formula.\n */\nfunction getFirstPivotFunction(tokens) {\n    return getFunctionsFromTokens(tokens, PIVOT_FUNCTIONS)[0];\n}\n/**\n * Parse a spreadsheet formula and detect the number of PIVOT functions that are\n * present in the given formula.\n */\nfunction getNumberOfPivotFunctions(tokens) {\n    return getFunctionsFromTokens(tokens, PIVOT_FUNCTIONS).length;\n}\n\n/**\n * Registry to enable or disable the support of positional arguments\n * (with a leading #) in pivot functions\n * e.g. =PIVOT.VALUE(1,\"probability\",\"#stage\",1)\n */\nconst supportedPivotPositionalFormulaRegistry = new Registry();\nsupportedPivotPositionalFormulaRegistry.add(\"SPREADSHEET\", false);\n\nautoCompleteProviders.add(\"pivot_ids\", {\n    sequence: 50,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        const functionContext = tokenAtCursor.functionContext;\n        const pivotFunction = [\"PIVOT.VALUE\", \"PIVOT.HEADER\", \"PIVOT\"];\n        if (!functionContext ||\n            !pivotFunction.includes(functionContext.parent.toUpperCase()) ||\n            functionContext.argPosition !== 0) {\n            return;\n        }\n        const pivotIds = this.getters.getPivotIds();\n        if (pivotIds.includes(tokenAtCursor.value)) {\n            return;\n        }\n        return pivotIds\n            .map((pivotId) => {\n            const definition = this.getters.getPivotCoreDefinition(pivotId);\n            const formulaId = this.getters.getPivotFormulaId(pivotId);\n            const str = `${formulaId}`;\n            return {\n                text: str,\n                description: definition.name,\n                htmlContent: [{ value: str, color: tokenColors.NUMBER }],\n                fuzzySearchKey: str + definition.name,\n            };\n        })\n            .filter(isDefined);\n    },\n    selectProposal: insertTokenAfterLeftParenthesis,\n});\nautoCompleteProviders.add(\"pivot_measures\", {\n    sequence: 50,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        const functionContext = tokenAtCursor.functionContext;\n        if (functionContext?.parent.toUpperCase() !== \"PIVOT.VALUE\" ||\n            functionContext.argPosition !== 1) {\n            return [];\n        }\n        const pivotFormulaId = extractFormulaIdFromToken(tokenAtCursor);\n        const pivotId = this.getters.getPivotId(pivotFormulaId);\n        if (!pivotId || !this.getters.isExistingPivot(pivotId)) {\n            return [];\n        }\n        const pivot = this.getters.getPivot(pivotId);\n        pivot.init();\n        if (!pivot.isValid()) {\n            return [];\n        }\n        return pivot.definition.measures\n            .map((measure) => {\n            if (measure.fieldName === \"__count\") {\n                const text = '\"__count\"';\n                return {\n                    text,\n                    description: _t(\"Count\"),\n                    htmlContent: [{ value: text, color: tokenColors.STRING }],\n                    fuzzySearchKey: _t(\"Count\") + text,\n                };\n            }\n            return makeMeasureProposal(measure);\n        })\n            .filter(isDefined);\n    },\n    selectProposal: insertTokenAfterArgSeparator,\n});\nautoCompleteProviders.add(\"pivot_group_fields\", {\n    sequence: 50,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        const functionContext = tokenAtCursor.functionContext;\n        if (!functionContext ||\n            (!canAutoCompletePivotField(tokenAtCursor) && !canAutoCompletePivotHeaderField(tokenAtCursor))) {\n            return;\n        }\n        const pivotFormulaId = extractFormulaIdFromToken(tokenAtCursor);\n        const pivotId = this.getters.getPivotId(pivotFormulaId);\n        if (!pivotId || !this.getters.isExistingPivot(pivotId)) {\n            return;\n        }\n        const pivot = this.getters.getPivot(pivotId);\n        pivot.init();\n        const fields = pivot.getFields();\n        const { columns, rows } = pivot.definition;\n        let args = functionContext.args;\n        if (functionContext?.parent.toUpperCase() === \"PIVOT.VALUE\") {\n            args = args.filter((ast, index) => index % 2 === 0); // keep only the field names\n            args = args.slice(1, functionContext.argPosition); // remove the first even argument (the pivot id)\n        }\n        else {\n            args = args.filter((ast, index) => index % 2 === 1); // keep only the field names\n        }\n        const argGroupBys = args.map((ast) => ast?.value).filter(isDefined);\n        const colFields = columns.map((groupBy) => groupBy.nameWithGranularity);\n        const rowFields = rows.map((groupBy) => groupBy.nameWithGranularity);\n        const proposals = [];\n        let previousGroupBy = [\"ARG_SEPARATOR\", \"SPACE\"].includes(tokenAtCursor.type)\n            ? argGroupBys.at(-1)\n            : argGroupBys.at(-2);\n        const isPositionalSupported = supportedPivotPositionalFormulaRegistry.get(pivot.type);\n        if (isPositionalSupported && previousGroupBy?.startsWith(\"#\")) {\n            previousGroupBy = previousGroupBy.slice(1);\n        }\n        if (previousGroupBy === undefined) {\n            proposals.push(colFields[0]);\n            proposals.push(rowFields[0]);\n        }\n        if (rowFields.includes(previousGroupBy)) {\n            const nextRowGroupBy = rowFields[rowFields.indexOf(previousGroupBy) + 1];\n            proposals.push(nextRowGroupBy);\n            proposals.push(colFields[0]);\n        }\n        if (colFields.includes(previousGroupBy)) {\n            const nextGroupBy = colFields[colFields.indexOf(previousGroupBy) + 1];\n            proposals.push(nextGroupBy);\n        }\n        const groupBys = proposals.filter(isDefined);\n        return groupBys\n            .map((groupBy) => {\n            const [fieldName, granularity] = groupBy.split(\":\");\n            const field = fields[fieldName];\n            return field ? makeFieldProposal(field, granularity) : undefined;\n        })\n            .concat(groupBys.map((groupBy) => {\n            if (!isPositionalSupported) {\n                return undefined;\n            }\n            const fieldName = groupBy.split(\":\")[0];\n            const field = fields[fieldName];\n            if (!field) {\n                return undefined;\n            }\n            const positionalFieldArg = `\"#${groupBy}\"`;\n            const positionalProposal = {\n                text: positionalFieldArg,\n                description: _t(\"%s (positional)\", field.string) + (field.help ? ` (${field.help})` : \"\"),\n                htmlContent: [{ value: positionalFieldArg, color: tokenColors.STRING }],\n                fuzzySearchKey: field.string + positionalFieldArg, // search on translated name and on technical name\n            };\n            return positionalProposal;\n        }))\n            .filter(isDefined);\n    },\n    selectProposal: insertTokenAfterArgSeparator,\n});\nfunction canAutoCompletePivotField(tokenAtCursor) {\n    const functionContext = tokenAtCursor.functionContext;\n    return (functionContext?.parent.toUpperCase() === \"PIVOT.VALUE\" &&\n        functionContext.argPosition >= 2 && // the first two arguments are the pivot id and the measure\n        functionContext.argPosition % 2 === 0 // only the even arguments are the group bys\n    );\n}\nfunction canAutoCompletePivotHeaderField(tokenAtCursor) {\n    const functionContext = tokenAtCursor.functionContext;\n    return (functionContext?.parent.toUpperCase() === \"PIVOT.HEADER\" &&\n        functionContext.argPosition >= 1 && // the first argument is the pivot id\n        functionContext.argPosition % 2 === 1 // only the odd arguments are the group bys\n    );\n}\nautoCompleteProviders.add(\"pivot_group_values\", {\n    sequence: 50,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        const functionContext = tokenAtCursor.functionContext;\n        if (!functionContext ||\n            !tokenAtCursor ||\n            (!canAutoCompletePivotGroupValue(tokenAtCursor) &&\n                !canAutoCompletePivotHeaderGroupValue(tokenAtCursor))) {\n            return;\n        }\n        const pivotFormulaId = extractFormulaIdFromToken(tokenAtCursor);\n        const pivotId = this.getters.getPivotId(pivotFormulaId);\n        if (!pivotId || !this.getters.isExistingPivot(pivotId)) {\n            return;\n        }\n        const pivot = this.getters.getPivot(pivotId);\n        if (!pivot.isValid()) {\n            return;\n        }\n        const argPosition = functionContext.argPosition;\n        const groupByField = tokenAtCursor.functionContext?.args[argPosition - 1]?.value;\n        if (!groupByField) {\n            return;\n        }\n        let dimension;\n        try {\n            dimension = pivot.definition.getDimension(groupByField);\n        }\n        catch (error) {\n            return undefined;\n        }\n        if (dimension.granularity === \"month_number\") {\n            return Object.values(MONTHS).map((monthDisplayName, index) => ({\n                text: `${index + 1}`,\n                fuzzySearchKey: monthDisplayName.toString(),\n                description: monthDisplayName.toString(),\n                htmlContent: [{ value: `${index + 1}`, color: tokenColors.NUMBER }],\n            }));\n        }\n        else if (dimension.granularity === \"quarter_number\") {\n            return [1, 2, 3, 4].map((quarter) => ({\n                text: `${quarter}`,\n                fuzzySearchKey: `${quarter}`,\n                description: _t(\"Quarter %s\", quarter),\n                htmlContent: [{ value: `${quarter}`, color: tokenColors.NUMBER }],\n            }));\n        }\n        else if (dimension.granularity === \"day_of_month\") {\n            return range(1, 32).map((dayOfMonth) => ({\n                text: `${dayOfMonth}`,\n                fuzzySearchKey: `${dayOfMonth}`,\n                description: \"\",\n                htmlContent: [{ value: `${dayOfMonth}`, color: tokenColors.NUMBER }],\n            }));\n        }\n        else if (dimension.granularity === \"iso_week_number\") {\n            return range(0, 54).map((isoWeekNumber) => ({\n                text: `${isoWeekNumber}`,\n                fuzzySearchKey: `${isoWeekNumber}`,\n                description: \"\",\n                htmlContent: [{ value: `${isoWeekNumber}`, color: tokenColors.NUMBER }],\n            }));\n        }\n        else if (dimension.granularity === \"day_of_week\") {\n            return range(1, 8).map((dayOfWeekNumber) => ({\n                text: `${dayOfWeekNumber}`,\n                fuzzySearchKey: `${dayOfWeekNumber}`,\n                description: \"\",\n                htmlContent: [{ value: `${dayOfWeekNumber}`, color: tokenColors.NUMBER }],\n            }));\n        }\n        else if (dimension.granularity === \"hour_number\") {\n            return range(0, 24).map((hourNumber) => ({\n                text: `${hourNumber}`,\n                fuzzySearchKey: `${hourNumber}`,\n                description: \"\",\n                htmlContent: [{ value: `${hourNumber}`, color: tokenColors.NUMBER }],\n            }));\n        }\n        else if (dimension.granularity === \"minute_number\") {\n            return range(0, 60).map((minuteNumber) => ({\n                text: `${minuteNumber}`,\n                fuzzySearchKey: `${minuteNumber}`,\n                description: \"\",\n                htmlContent: [{ value: `${minuteNumber}`, color: tokenColors.NUMBER }],\n            }));\n        }\n        else if (dimension.granularity === \"second_number\") {\n            return range(0, 60).map((secondNumber) => ({\n                text: `${secondNumber}`,\n                fuzzySearchKey: `${secondNumber}`,\n                description: \"\",\n                htmlContent: [{ value: `${secondNumber}`, color: tokenColors.NUMBER }],\n            }));\n        }\n        return pivot.getPossibleFieldValues(dimension).map(({ value, label }) => {\n            const isString = typeof value === \"string\";\n            const text = isString ? `\"${value}\"` : value.toString();\n            const color = isString ? tokenColors.STRING : tokenColors.NUMBER;\n            const usedLabel = label === value ? \"\" : label;\n            return {\n                text,\n                description: usedLabel,\n                htmlContent: [{ value: text, color }],\n                fuzzySearchKey: value + usedLabel,\n            };\n        });\n    },\n    selectProposal: insertTokenAfterArgSeparator,\n});\nfunction canAutoCompletePivotGroupValue(tokenAtCursor) {\n    const functionContext = tokenAtCursor.functionContext;\n    return (functionContext?.parent.toUpperCase() === \"PIVOT.VALUE\" &&\n        functionContext.argPosition >= 2 && // the first two arguments are the pivot id and the measure\n        functionContext.argPosition % 2 === 1 // only the odd arguments are the group by values\n    );\n}\nfunction canAutoCompletePivotHeaderGroupValue(tokenAtCursor) {\n    const functionContext = tokenAtCursor.functionContext;\n    return (functionContext?.parent.toUpperCase() === \"PIVOT.HEADER\" &&\n        functionContext.argPosition >= 1 && // the first argument is the pivot id\n        functionContext.argPosition % 2 === 0 // only the even arguments are the group by values\n    );\n}\n\nautoCompleteProviders.add(\"sheet_names\", {\n    sequence: 150,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        if (tokenAtCursor.type === \"SYMBOL\" ||\n            (tokenAtCursor.type === \"UNKNOWN\" && tokenAtCursor.value.startsWith(\"'\"))) {\n            return this.getters.getSheetIds().map((sheetId) => {\n                const sheetName = getCanonicalSymbolName(this.getters.getSheetName(sheetId));\n                return {\n                    text: sheetName,\n                    fuzzySearchKey: sheetName.startsWith(\"'\") ? sheetName : \"'\" + sheetName, // typing a single quote is a way to avoid matching function names\n                };\n            });\n        }\n        return [];\n    },\n    selectProposal(tokenAtCursor, value) {\n        const start = tokenAtCursor.start;\n        const end = tokenAtCursor.end;\n        this.composer.changeComposerCursorSelection(start, end);\n        this.composer.replaceComposerCursorSelection(value + \"!\");\n    },\n});\n\n/**\n * Add the `https` prefix to the url if it's missing\n */\nfunction withHttps(url) {\n    return !/^https?:\\/\\//i.test(url) ? `https://${url}` : url;\n}\nconst urlRegistry = new Registry();\nfunction createWebLink(url, label) {\n    url = withHttps(url);\n    return {\n        url,\n        label: label || url,\n        isExternal: true,\n        isUrlEditable: true,\n    };\n}\nurlRegistry.add(\"sheet_URL\", {\n    match: (url) => isSheetUrl(url),\n    createLink: (url, label) => {\n        return {\n            label,\n            url,\n            isExternal: false,\n            isUrlEditable: false,\n        };\n    },\n    urlRepresentation(url, getters) {\n        const sheetId = parseSheetUrl(url);\n        return getters.tryGetSheetName(sheetId) || _t(\"Invalid sheet\");\n    },\n    open(url, env) {\n        const sheetId = parseSheetUrl(url);\n        const result = env.model.dispatch(\"ACTIVATE_SHEET\", {\n            sheetIdFrom: env.model.getters.getActiveSheetId(),\n            sheetIdTo: sheetId,\n        });\n        if (result.isCancelledBecause(\"SheetIsHidden\" /* CommandResult.SheetIsHidden */)) {\n            env.notifyUser({\n                type: \"warning\",\n                sticky: false,\n                text: _t(\"Cannot open the link because the linked sheet is hidden.\"),\n            });\n        }\n    },\n    sequence: 0,\n});\nconst WebUrlSpec = {\n    createLink: createWebLink,\n    match: (url) => isWebLink(url),\n    open: (url) => window.open(url, \"_blank\"),\n    urlRepresentation: (url) => url,\n    sequence: 0,\n};\nfunction findMatchingSpec(url) {\n    return (urlRegistry\n        .getAll()\n        .sort((a, b) => a.sequence - b.sequence)\n        .find((urlType) => urlType.match(url)) || WebUrlSpec);\n}\nfunction urlRepresentation(link, getters) {\n    return findMatchingSpec(link.url).urlRepresentation(link.url, getters);\n}\nfunction openLink(link, env) {\n    findMatchingSpec(link.url).open(link.url, env);\n}\nfunction detectLink(value) {\n    if (typeof value !== \"string\") {\n        return undefined;\n    }\n    if (isMarkdownLink(value)) {\n        const { label, url } = parseMarkdownLink(value);\n        return findMatchingSpec(url).createLink(url, label);\n    }\n    else if (isWebLink(value)) {\n        return createWebLink(value);\n    }\n    return undefined;\n}\n\nfunction evaluateLiteral(literalCell, localeFormat) {\n    const value = isTextFormat(localeFormat.format) ? literalCell.content : literalCell.parsedValue;\n    const functionResult = { value, format: localeFormat.format };\n    return createEvaluatedCell(functionResult, localeFormat.locale);\n}\nfunction parseLiteral(content, locale) {\n    if (content.startsWith(\"=\")) {\n        throw new Error(`Cannot parse \"${content}\" because it's not a literal value. It's a formula`);\n    }\n    if (content === \"\") {\n        return null;\n    }\n    if (isNumber(content, DEFAULT_LOCALE)) {\n        return parseNumber(content, DEFAULT_LOCALE);\n    }\n    const internalDate = parseDateTime(content, locale);\n    if (internalDate) {\n        return internalDate.value;\n    }\n    if (isBoolean(content)) {\n        return content.toUpperCase() === \"TRUE\" ? true : false;\n    }\n    return content;\n}\nfunction createEvaluatedCell(functionResult, locale = DEFAULT_LOCALE, cell) {\n    const link = detectLink(functionResult.value);\n    if (!link) {\n        return _createEvaluatedCell(functionResult, locale, cell);\n    }\n    const value = parseLiteral(link.label, locale);\n    const format = functionResult.format ||\n        (typeof value === \"number\"\n            ? detectDateFormat(link.label, locale) || detectNumberFormat(link.label)\n            : undefined);\n    const linkPayload = {\n        value,\n        format,\n    };\n    return {\n        ..._createEvaluatedCell(linkPayload, locale, cell),\n        link,\n    };\n}\nfunction _createEvaluatedCell(functionResult, locale, cell) {\n    let { value, format, message } = functionResult;\n    format = cell?.format || format;\n    const formattedValue = formatValue(value, { format, locale });\n    if (isEvaluationError(value)) {\n        return errorCell(value, message);\n    }\n    if (isTextFormat(format)) {\n        // TO DO:\n        // with the next line, the value of the cell is transformed depending on the format.\n        // This shouldn't happen, by doing this, the formulas handling numbers are not able\n        // to interpret the value as a number.\n        return textCell(toString(value), format, formattedValue);\n    }\n    if (value === null) {\n        return emptyCell(format);\n    }\n    if (typeof value === \"number\") {\n        if (isDateTimeFormat(format || \"\")) {\n            return dateTimeCell(value, format, formattedValue);\n        }\n        return numberCell(value, format, formattedValue);\n    }\n    if (typeof value === \"boolean\") {\n        return booleanCell(value, format, formattedValue);\n    }\n    return textCell(value, format, formattedValue);\n}\nfunction textCell(value, format, formattedValue) {\n    return {\n        value,\n        format,\n        formattedValue,\n        type: CellValueType.text,\n        isAutoSummable: true,\n        defaultAlign: \"left\",\n    };\n}\nfunction numberCell(value, format, formattedValue) {\n    return {\n        value: value || 0, // necessary to avoid \"-0\" and NaN values,\n        format,\n        formattedValue,\n        type: CellValueType.number,\n        isAutoSummable: true,\n        defaultAlign: \"right\",\n    };\n}\nconst emptyCell = memoize(function emptyCell(format) {\n    return {\n        value: null,\n        format,\n        formattedValue: \"\",\n        type: CellValueType.empty,\n        isAutoSummable: true,\n        defaultAlign: \"left\",\n    };\n});\nfunction dateTimeCell(value, format, formattedValue) {\n    return {\n        value,\n        format,\n        formattedValue,\n        type: CellValueType.number,\n        isAutoSummable: false,\n        defaultAlign: \"right\",\n    };\n}\nfunction booleanCell(value, format, formattedValue) {\n    return {\n        value,\n        format,\n        formattedValue,\n        type: CellValueType.boolean,\n        isAutoSummable: false,\n        defaultAlign: \"center\",\n    };\n}\nfunction errorCell(value, message) {\n    return {\n        value,\n        formattedValue: value,\n        message,\n        type: CellValueType.error,\n        isAutoSummable: false,\n        defaultAlign: \"center\",\n    };\n}\n\n/**\n * An AutofillModifierImplementation is used to describe how to handle a\n * AutofillModifier.\n */\nconst autofillModifiersRegistry = new Registry();\nautofillModifiersRegistry\n    .add(\"ALPHANUMERIC_INCREMENT_MODIFIER\", {\n    apply: (rule, data) => {\n        rule.current += rule.increment;\n        const content = `${rule.prefix}${rule.current\n            .toString()\n            .padStart(rule.numberPostfixLength || 0, \"0\")}`;\n        return {\n            cellData: {\n                border: data.border,\n                style: data.cell && data.cell.style,\n                format: data.cell && data.cell.format,\n                content,\n            },\n            tooltip: { props: { content } },\n        };\n    },\n})\n    .add(\"INCREMENT_MODIFIER\", {\n    apply: (rule, data, getters) => {\n        rule.current += rule.increment;\n        const content = rule.current.toString();\n        const locale = getters.getLocale();\n        const tooltipValue = formatValue(rule.current, { format: data.cell?.format, locale });\n        return {\n            cellData: {\n                border: data.border,\n                style: data.cell && data.cell.style,\n                format: data.cell && data.cell.format,\n                content,\n            },\n            tooltip: content ? { props: { content: tooltipValue } } : undefined,\n        };\n    },\n})\n    .add(\"DATE_INCREMENT_MODIFIER\", {\n    apply: (rule, data, getters) => {\n        const date = toJsDate(rule.current, getters.getLocale());\n        date.setFullYear(date.getFullYear() + rule.increment.years || 0);\n        date.setMonth(date.getMonth() + rule.increment.months || 0);\n        date.setDate(date.getDate() + rule.increment.days || 0);\n        const value = jsDateToNumber(date);\n        rule.current = value;\n        const locale = getters.getLocale();\n        const tooltipValue = formatValue(value, { format: data.cell?.format, locale });\n        return {\n            cellData: {\n                border: data.border,\n                style: data.cell && data.cell.style,\n                format: data.cell && data.cell.format,\n                content: value.toString(),\n            },\n            tooltip: value ? { props: { content: tooltipValue } } : undefined,\n        };\n    },\n})\n    .add(\"COPY_MODIFIER\", {\n    apply: (rule, data, getters) => {\n        const content = data.cell?.content || \"\";\n        const localeFormat = { locale: getters.getLocale(), format: data.cell?.format };\n        return {\n            cellData: {\n                border: data.border,\n                style: data.cell && data.cell.style,\n                format: data.cell && data.cell.format,\n                content,\n            },\n            tooltip: content\n                ? {\n                    props: {\n                        content: data.cell\n                            ? evaluateLiteral(data.cell, localeFormat).formattedValue\n                            : \"\",\n                    },\n                }\n                : undefined,\n        };\n    },\n})\n    .add(\"FORMULA_MODIFIER\", {\n    apply: (rule, data, getters, direction) => {\n        rule.current += rule.increment;\n        let x = 0;\n        let y = 0;\n        switch (direction) {\n            case \"up\" /* DIRECTION.UP */:\n                x = 0;\n                y = -rule.current;\n                break;\n            case \"down\" /* DIRECTION.DOWN */:\n                x = 0;\n                y = rule.current;\n                break;\n            case \"left\" /* DIRECTION.LEFT */:\n                x = -rule.current;\n                y = 0;\n                break;\n            case \"right\" /* DIRECTION.RIGHT */:\n                x = rule.current;\n                y = 0;\n                break;\n        }\n        const cell = data.cell;\n        if (!cell || !cell.isFormula) {\n            return { cellData: {} };\n        }\n        const sheetId = data.sheetId;\n        const content = getters.getTranslatedCellFormula(sheetId, x, y, cell.compiledFormula.tokens);\n        return {\n            cellData: {\n                border: data.border,\n                style: cell.style,\n                format: cell.format,\n                content,\n            },\n            tooltip: content ? { props: { content } } : undefined,\n        };\n    },\n});\n\nconst autofillRulesRegistry = new Registry();\nconst numberPostfixRegExp = /(\\d+)$/;\nconst stringPrefixRegExp = /^(.*\\D+)/;\nconst alphaNumericValueRegExp = /^(.*\\D+)(\\d+)$/;\n/**\n * Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).\n * Return the one which contains the given cell\n */\nfunction getGroup(cell, cells, filter) {\n    let group = [];\n    let found = false;\n    for (let x of cells) {\n        if (x === cell) {\n            found = true;\n        }\n        const cellValue = x === undefined || x.isFormula\n            ? undefined\n            : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });\n        if (cellValue && filter(cellValue)) {\n            group.push(cellValue);\n        }\n        else {\n            if (found) {\n                return group;\n            }\n            group = [];\n        }\n    }\n    return group;\n}\n/**\n * Get the average steps between numbers\n */\nfunction getAverageIncrement(group) {\n    const averages = [];\n    let last = group[0];\n    for (let i = 1; i < group.length; i++) {\n        const current = group[i];\n        averages.push(current - last);\n        last = current;\n    }\n    return averages.reduce((a, b) => a + b, 0) / averages.length;\n}\n/**\n * Get the step for a group\n */\nfunction calculateIncrementBasedOnGroup(group) {\n    let increment = 1;\n    if (group.length >= 2) {\n        increment = getAverageIncrement(group) * group.length;\n    }\n    return increment;\n}\n/**\n * Iterates on a list of date intervals.\n * if every interval is the same, return the interval\n * Otherwise return undefined\n *\n */\nfunction getEqualInterval(intervals) {\n    if (intervals.length < 2) {\n        return intervals[0] || { years: 0, months: 0, days: 0 };\n    }\n    const equal = intervals.every((interval) => interval.years === intervals[0].years &&\n        interval.months === intervals[0].months &&\n        interval.days === intervals[0].days);\n    return equal ? intervals[0] : undefined;\n}\n/**\n * Based on a group of dates, calculate the increment that should be applied\n * to the next date.\n *\n * This will compute the date difference in calendar terms (years, months, days)\n * In order to make abstraction of leap years and months with different number of days.\n *\n * In case the dates are not equidistant in calendar terms, no rule can be extrapolated\n * In case of equidistant dates, we either have in that order:\n *  - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval\n *  - exact day interval (e.g. +n days) in which case we increment by the same day interval\n *  - equidistant dates but not the same interval, in which case we return increment of the same interval\n *\n * */\nfunction calculateDateIncrementBasedOnGroup(group) {\n    if (group.length < 2) {\n        return 1;\n    }\n    const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));\n    const datesIntervals = getDateIntervals(jsDates);\n    const datesEquidistantInterval = getEqualInterval(datesIntervals);\n    if (datesEquidistantInterval === undefined) {\n        // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated\n        return undefined;\n    }\n    // The dates are apart by an exact interval of years, months or days\n    // but not a combination of them\n    const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;\n    const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)\n    if (!exactDateInterval || isSameDay) {\n        const timeIntervals = jsDates\n            .map((date, index) => {\n            if (index === 0) {\n                return 0;\n            }\n            const previous = jsDates[index - 1];\n            const days = Math.floor(date.getTime()) - Math.floor(previous.getTime());\n            return days;\n        })\n            .slice(1);\n        const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);\n        if (equidistantDates) {\n            return group.length * (group[1] - group[0]);\n        }\n    }\n    return {\n        years: datesEquidistantInterval.years * group.length,\n        months: datesEquidistantInterval.months * group.length,\n        days: datesEquidistantInterval.days * group.length,\n    };\n}\nautofillRulesRegistry\n    .add(\"simple_value_copy\", {\n    condition: (cell, cells) => {\n        return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));\n    },\n    generateRule: () => {\n        return { type: \"COPY_MODIFIER\" };\n    },\n    sequence: 10,\n})\n    .add(\"increment_alphanumeric_value\", {\n    condition: (cell) => !cell.isFormula &&\n        evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&\n        alphaNumericValueRegExp.test(cell.content),\n    generateRule: (cell, cells) => {\n        const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);\n        const prefix = cell.content.match(stringPrefixRegExp)[0];\n        const numberPostfixLength = cell.content.length - prefix.length;\n        const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&\n            alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is\n            .filter((cell) => prefix === (cell.value ?? \"\").toString().match(stringPrefixRegExp)[0])\n            .map((cell) => parseInt((cell.value ?? \"\").toString().match(numberPostfixRegExp)[0]));\n        const increment = calculateIncrementBasedOnGroup(group);\n        return {\n            type: \"ALPHANUMERIC_INCREMENT_MODIFIER\",\n            prefix,\n            current: numberPostfix,\n            increment,\n            numberPostfixLength,\n        };\n    },\n    sequence: 15,\n})\n    .add(\"copy_text\", {\n    condition: (cell) => !cell.isFormula &&\n        evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,\n    generateRule: () => {\n        return { type: \"COPY_MODIFIER\" };\n    },\n    sequence: 20,\n})\n    .add(\"update_formula\", {\n    condition: (cell) => cell.isFormula,\n    generateRule: (_, cells) => {\n        return { type: \"FORMULA_MODIFIER\", increment: cells.length, current: 0 };\n    },\n    sequence: 30,\n})\n    .add(\"increment_dates\", {\n    condition: (cell, cells) => {\n        return (!cell.isFormula &&\n            evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&\n            !!cell.format &&\n            isDateTimeFormat(cell.format));\n    },\n    generateRule: (cell, cells) => {\n        const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&\n            !!evaluatedCell.format &&\n            isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));\n        const increment = calculateDateIncrementBasedOnGroup(group);\n        if (increment === undefined) {\n            return { type: \"COPY_MODIFIER\" };\n        }\n        /**  requires to detect the current date (requires to be an integer value  with the right format)\n         * detect  if year  or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)\n         */\n        const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });\n        if (typeof increment === \"object\") {\n            return {\n                type: \"DATE_INCREMENT_MODIFIER\",\n                increment,\n                current: evaluation.type === CellValueType.number ? evaluation.value : 0,\n            };\n        }\n        return {\n            type: \"INCREMENT_MODIFIER\",\n            increment,\n            current: evaluation.type === CellValueType.number ? evaluation.value : 0,\n        };\n    },\n    sequence: 25,\n})\n    .add(\"increment_number\", {\n    condition: (cell) => !cell.isFormula &&\n        evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,\n    generateRule: (cell, cells) => {\n        const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&\n            !isDateTimeFormat(evaluatedCell.format || \"\")).map((cell) => Number(cell.value));\n        const increment = calculateIncrementBasedOnGroup(group);\n        const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });\n        return {\n            type: \"INCREMENT_MODIFIER\",\n            increment,\n            current: evaluation.type === CellValueType.number ? evaluation.value : 0,\n        };\n    },\n    sequence: 40,\n});\n/**\n * Returns the date intervals between consecutive dates of an array\n * in the format of { years: number, months: number, days: number }\n *\n * The split is necessary to make abstraction of leap years and\n * months with different number of days.\n *\n * @param dates\n */\nfunction getDateIntervals(dates) {\n    if (dates.length < 2) {\n        return [{ years: 0, months: 0, days: 0 }];\n    }\n    const res = dates.map((date, index) => {\n        if (index === 0) {\n            return { years: 0, months: 0, days: 0 };\n        }\n        const previous = DateTime.fromTimestamp(dates[index - 1].getTime());\n        const years = getTimeDifferenceInWholeYears(previous, date);\n        const months = getTimeDifferenceInWholeMonths(previous, date) % 12;\n        previous.setFullYear(previous.getFullYear() + years);\n        previous.setMonth(previous.getMonth() + months);\n        const days = getTimeDifferenceInWholeDays(previous, date);\n        return {\n            years,\n            months,\n            days,\n        };\n    });\n    return res.slice(1);\n}\n\nconst cellPopoverRegistry = new Registry();\n\nconst GAUGE_PADDING_SIDE = 30;\nconst GAUGE_PADDING_TOP = 10;\nconst GAUGE_PADDING_BOTTOM = 20;\nconst GAUGE_LABELS_FONT_SIZE = 12;\nconst GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;\nconst GAUGE_BACKGROUND_COLOR = \"#F3F2F1\";\nconst GAUGE_TEXT_COLOR = \"#666666\";\nconst GAUGE_TEXT_COLOR_HIGH_CONTRAST = \"#C8C8C8\";\nconst GAUGE_INFLECTION_MARKER_COLOR = \"#666666aa\";\nconst GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;\nconst GAUGE_TITLE_SECTION_HEIGHT = 25;\nconst GAUGE_TITLE_FONT_SIZE = SCORECARD_GAUGE_CHART_FONT_SIZE;\nconst GAUGE_TITLE_PADDING_LEFT = SCORECARD_GAUGE_CHART_PADDING;\nconst GAUGE_TITLE_PADDING_TOP = SCORECARD_GAUGE_CHART_PADDING;\nfunction drawGaugeChart(canvas, runtime) {\n    const canvasBoundingRect = canvas.getBoundingClientRect();\n    canvas.width = canvasBoundingRect.width;\n    canvas.height = canvasBoundingRect.height;\n    const ctx = canvas.getContext(\"2d\");\n    const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);\n    drawBackground(ctx, config);\n    drawGauge(ctx, config);\n    drawInflectionValues(ctx, config);\n    drawLabels(ctx, config);\n    drawTitle(ctx, config);\n}\nfunction drawGauge(ctx, config) {\n    ctx.save();\n    const gauge = config.gauge;\n    const arcCenterX = gauge.rect.x + gauge.rect.width / 2;\n    const arcCenterY = gauge.rect.y + gauge.rect.height;\n    const arcRadius = gauge.rect.height - gauge.arcWidth / 2;\n    if (arcRadius < 0) {\n        return;\n    }\n    const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);\n    // Gauge background\n    ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;\n    ctx.beginPath();\n    ctx.lineWidth = gauge.arcWidth;\n    ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);\n    ctx.stroke();\n    // Gauge value\n    ctx.strokeStyle = gauge.color;\n    ctx.beginPath();\n    ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);\n    ctx.stroke();\n    ctx.restore();\n}\nfunction drawBackground(ctx, config) {\n    ctx.save();\n    ctx.fillStyle = config.backgroundColor;\n    ctx.fillRect(0, 0, config.width, config.height);\n    ctx.restore();\n}\nfunction drawLabels(ctx, config) {\n    for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {\n        ctx.save();\n        ctx.textAlign = \"center\";\n        ctx.fillStyle = label.color;\n        ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;\n        ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);\n        ctx.restore();\n    }\n}\nfunction drawInflectionValues(ctx, config) {\n    const { x: rectX, y: rectY, width, height } = config.gauge.rect;\n    for (const inflectionValue of config.inflectionValues) {\n        ctx.save();\n        ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment\n        ctx.rotate(Math.PI / 2 - inflectionValue.rotation);\n        ctx.lineWidth = 2;\n        ctx.strokeStyle = GAUGE_INFLECTION_MARKER_COLOR;\n        ctx.beginPath();\n        ctx.moveTo(0, -(height - config.gauge.arcWidth));\n        ctx.lineTo(0, -height - 3);\n        ctx.stroke();\n        ctx.textAlign = \"center\";\n        ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;\n        ctx.fillStyle = inflectionValue.color;\n        const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;\n        ctx.fillText(inflectionValue.label, 0, textY);\n        ctx.restore();\n    }\n}\nfunction drawTitle(ctx, config) {\n    ctx.save();\n    const title = config.title;\n    ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);\n    ctx.textBaseline = \"middle\";\n    ctx.fillStyle = title.color;\n    ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);\n    ctx.restore();\n}\nfunction getGaugeRenderingConfig(boundingRect, runtime, ctx) {\n    const maxValue = runtime.maxValue;\n    const minValue = runtime.minValue;\n    const gaugeValue = runtime.gaugeValue;\n    const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);\n    const gaugeArcWidth = gaugeRect.width / 6;\n    const gaugePercentage = gaugeValue\n        ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)\n        : 0;\n    const gaugeValuePosition = {\n        x: boundingRect.width / 2,\n        y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,\n    };\n    let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;\n    // Scale down the font size if the gaugeRect is too small\n    if (gaugeRect.height < 300) {\n        gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);\n    }\n    // Scale down the font size if the text is too long\n    const maxTextWidth = gaugeRect.width / 2;\n    const gaugeLabel = gaugeValue?.label || \"-\";\n    if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, \"px\") > maxTextWidth) {\n        gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, \"px\"));\n    }\n    const minLabelPosition = {\n        x: gaugeRect.x + gaugeArcWidth / 2,\n        y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,\n    };\n    const maxLabelPosition = {\n        x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,\n        y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,\n    };\n    const textColor = getContrastedTextColor(runtime.background);\n    const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);\n    let x = 0, titleWidth = 0, titleHeight = 0;\n    if (runtime.title.text) {\n        ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { ...runtime.title, fontSize: GAUGE_TITLE_FONT_SIZE }, \"px\"));\n    }\n    switch (runtime.title.align) {\n        case \"right\":\n            x = boundingRect.width - titleWidth - GAUGE_TITLE_PADDING_LEFT;\n            break;\n        case \"center\":\n            x = (boundingRect.width - titleWidth) / 2;\n            break;\n        case \"left\":\n        default:\n            x = GAUGE_TITLE_PADDING_LEFT;\n            break;\n    }\n    return {\n        width: boundingRect.width,\n        height: boundingRect.height,\n        title: {\n            label: runtime.title.text ?? \"\",\n            fontSize: GAUGE_TITLE_FONT_SIZE,\n            textPosition: {\n                x,\n                y: GAUGE_TITLE_PADDING_TOP + titleHeight / 2,\n            },\n            color: runtime.title.color ?? textColor,\n            bold: runtime.title.bold,\n            italic: runtime.title.italic,\n        },\n        backgroundColor: runtime.background,\n        gauge: {\n            rect: gaugeRect,\n            arcWidth: gaugeArcWidth,\n            percentage: clip(gaugePercentage, 0, 1),\n            color: getGaugeColor(runtime),\n        },\n        inflectionValues,\n        gaugeValue: {\n            label: gaugeLabel,\n            textPosition: gaugeValuePosition,\n            fontSize: gaugeValueFontSize,\n            color: textColor,\n        },\n        minLabel: {\n            label: runtime.minValue.label,\n            textPosition: minLabelPosition,\n            fontSize: GAUGE_LABELS_FONT_SIZE,\n            color: textColor,\n        },\n        maxLabel: {\n            label: runtime.maxValue.label,\n            textPosition: maxLabelPosition,\n            fontSize: GAUGE_LABELS_FONT_SIZE,\n            color: textColor,\n        },\n    };\n}\n/**\n * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving\n * space for the title and labels.\n */\nfunction getGaugeRect(boundingRect, title) {\n    const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;\n    const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;\n    const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;\n    let gaugeWidth;\n    let gaugeHeight;\n    if (drawWidth > 2 * drawHeight) {\n        gaugeWidth = 2 * drawHeight;\n        gaugeHeight = drawHeight;\n    }\n    else {\n        gaugeWidth = drawWidth;\n        gaugeHeight = drawWidth / 2;\n    }\n    const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;\n    const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;\n    return {\n        x: gaugeX,\n        y: gaugeY,\n        width: gaugeWidth,\n        height: gaugeHeight,\n    };\n}\n/**\n * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).\n *\n * Also compute an offset for the text so that it doesn't overlap with other text.\n */\nfunction getInflectionValues(runtime, gaugeRect, textColor, ctx) {\n    const maxValue = runtime.maxValue;\n    const minValue = runtime.minValue;\n    const gaugeCircleCenter = {\n        x: gaugeRect.x + gaugeRect.width / 2,\n        y: gaugeRect.y + gaugeRect.height,\n    };\n    const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };\n    const inflectionValues = [];\n    const inflectionValuesTextRects = [];\n    for (const inflectionValue of runtime.inflectionValues) {\n        const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);\n        const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, \"px\");\n        const angle = Math.PI - Math.PI * percentage;\n        const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle\n        gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text\n        gaugeCircleCenter.x, // center of the gauge circle\n        gaugeCircleCenter.y, // center of the gauge circle\n        labelWidth + 2, // width of the text + some margin\n        GAUGE_LABELS_FONT_SIZE // height of the text\n        );\n        let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))\n            ? GAUGE_LABELS_FONT_SIZE\n            : 0;\n        inflectionValuesTextRects.push(textRect);\n        inflectionValues.push({\n            rotation: angle,\n            label: inflectionValue.label,\n            fontSize: GAUGE_LABELS_FONT_SIZE,\n            color: textColor,\n            offset,\n        });\n    }\n    return inflectionValues;\n}\nfunction getGaugeColor(runtime) {\n    const gaugeValue = runtime.gaugeValue?.value;\n    if (gaugeValue === undefined) {\n        return GAUGE_BACKGROUND_COLOR;\n    }\n    for (let i = 0; i < runtime.inflectionValues.length; i++) {\n        const inflectionValue = runtime.inflectionValues[i];\n        if (inflectionValue.operator === \"<\" && gaugeValue < inflectionValue.value) {\n            return runtime.colors[i];\n        }\n        else if (inflectionValue.operator === \"<=\" && gaugeValue <= inflectionValue.value) {\n            return runtime.colors[i];\n        }\n    }\n    return runtime.colors.at(-1);\n}\nfunction getContrastedTextColor(backgroundColor) {\n    return relativeLuminance(backgroundColor) > 0.3\n        ? GAUGE_TEXT_COLOR\n        : GAUGE_TEXT_COLOR_HIGH_CONTRAST;\n}\nfunction getSegmentsOfRectangle(rectangle) {\n    return [\n        { start: rectangle.topLeft, end: rectangle.topRight },\n        { start: rectangle.topRight, end: rectangle.bottomRight },\n        { start: rectangle.bottomRight, end: rectangle.bottomLeft },\n        { start: rectangle.bottomLeft, end: rectangle.topLeft },\n    ];\n}\n/**\n * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)\n * is not handled.\n */\nfunction doSegmentIntersect(segment1, segment2) {\n    const A = segment1.start;\n    const B = segment1.end;\n    const C = segment2.start;\n    const D = segment2.end;\n    /**\n     * Line segment intersection algorithm\n     * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/\n     */\n    function ccw(a, b, c) {\n        return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);\n    }\n    return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);\n}\nfunction doRectanglesIntersect(rect1, rect2) {\n    const segments1 = getSegmentsOfRectangle(rect1);\n    const segments2 = getSegmentsOfRectangle(rect2);\n    for (const segment1 of segments1) {\n        for (const segment2 of segments2) {\n            if (doSegmentIntersect(segment1, segment2)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n/**\n *  Get the rectangle that is tangent to a circle at a given angle.\n *\n * @param angle angle between X axis and the point where the rectangle is tangent to the circle\n */\nfunction getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {\n    const cos = Math.cos(angle);\n    const sin = Math.sin(angle);\n    // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle\n    const x = cos * radius;\n    const y = sin * radius;\n    // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle\n    const x2 = sin * (rectWidth / 2); // cos(angle + 90\u00b0) = sin(angle)\n    const y2 = cos * (rectWidth / 2);\n    const bottomRight = {\n        x: x + x2 + circleCenterX,\n        y: circleCenterY - (y - y2),\n    };\n    const bottomLeft = {\n        x: x - x2 + circleCenterX,\n        y: circleCenterY - (y + y2),\n    };\n    // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)\n    const xp = cos * (radius + rectHeight);\n    const yp = sin * (radius + rectHeight);\n    const topLeft = {\n        x: xp - x2 + circleCenterX,\n        y: circleCenterY - (yp + y2),\n    };\n    const topRight = {\n        x: xp + x2 + circleCenterX,\n        y: circleCenterY - (yp - y2),\n    };\n    return { bottomLeft, bottomRight, topRight, topLeft };\n}\n\nclass GaugeChartComponent extends Component {\n    static template = \"o-spreadsheet-GaugeChartComponent\";\n    canvas = useRef(\"chartContainer\");\n    get runtime() {\n        return this.env.model.getters.getChartRuntime(this.props.figure.id);\n    }\n    setup() {\n        useEffect(() => drawGaugeChart(this.canvas.el, this.runtime), () => {\n            const canvas = this.canvas.el;\n            const rect = canvas.getBoundingClientRect();\n            return [rect.width, rect.height, this.runtime, this.canvas.el];\n        });\n    }\n}\nGaugeChartComponent.props = {\n    figure: Object,\n};\n\n/**\n * Convert a JS color hexadecimal to an excel compatible color.\n *\n * In Excel the color don't start with a '#' and the format is AARRGGBB instead of RRGGBBAA\n */\nfunction toXlsxHexColor(color) {\n    color = toHex(color).replace(\"#\", \"\");\n    // alpha channel goes first\n    if (color.length === 8) {\n        return color.slice(6) + color.slice(0, 6);\n    }\n    return color;\n}\n\n/**\n * Represent a raw XML string\n */\nclass XMLString {\n    xmlString;\n    /**\n     * @param xmlString should be a well formed, properly escaped XML string\n     */\n    constructor(xmlString) {\n        this.xmlString = xmlString;\n    }\n    toString() {\n        return this.xmlString;\n    }\n}\nconst XLSX_CHART_TYPES = [\n    \"areaChart\",\n    \"area3DChart\",\n    \"lineChart\",\n    \"line3DChart\",\n    \"stockChart\",\n    \"radarChart\",\n    \"scatterChart\",\n    \"pieChart\",\n    \"pie3DChart\",\n    \"doughnutChart\",\n    \"barChart\",\n    \"bar3DChart\",\n    \"ofPieChart\",\n    \"surfaceChart\",\n    \"surface3DChart\",\n    \"bubbleChart\",\n    \"comboChart\",\n];\n\n/** In XLSX color format (no #)  */\nconst AUTO_COLOR = \"000000\";\nconst XLSX_ICONSET_MAP = {\n    arrow: \"3Arrows\",\n    smiley: \"3Symbols\",\n    dot: \"3TrafficLights1\",\n};\nconst NAMESPACE = {\n    styleSheet: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n    sst: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n    Relationships: \"http://schemas.openxmlformats.org/package/2006/relationships\",\n    Types: \"http://schemas.openxmlformats.org/package/2006/content-types\",\n    worksheet: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n    workbook: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n    drawing: \"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing\",\n    table: \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n    revision: \"http://schemas.microsoft.com/office/spreadsheetml/2014/revision\",\n    revision3: \"http://schemas.microsoft.com/office/spreadsheetml/2016/revision3\",\n    markupCompatibility: \"http://schemas.openxmlformats.org/markup-compatibility/2006\",\n};\nconst DRAWING_NS_A = \"http://schemas.openxmlformats.org/drawingml/2006/main\";\nconst DRAWING_NS_C = \"http://schemas.openxmlformats.org/drawingml/2006/chart\";\nconst CONTENT_TYPES = {\n    workbook: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\",\n    sheet: \"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\",\n    sharedStrings: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml\",\n    styles: \"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\",\n    drawing: \"application/vnd.openxmlformats-officedocument.drawing+xml\",\n    chart: \"application/vnd.openxmlformats-officedocument.drawingml.chart+xml\",\n    themes: \"application/vnd.openxmlformats-officedocument.theme+xml\",\n    table: \"application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml\",\n    pivot: \"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml\",\n    externalLink: \"application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml\",\n};\nconst XLSX_RELATION_TYPE = {\n    document: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\",\n    sheet: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\",\n    sharedStrings: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\",\n    styles: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\",\n    drawing: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing\",\n    chart: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart\",\n    theme: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme\",\n    table: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/table\",\n    hyperlink: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink\",\n    image: \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\",\n};\nconst RELATIONSHIP_NSR = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships\";\nconst HEIGHT_FACTOR = 0.75; // 100px => 75 u\n/**\n * Excel says its default column width is 8.43 characters (64px)\n * which makes WIDTH_FACTOR = 0.1317, but it doesn't work well\n * 0.143 is a value from dev's experiments.\n */\nconst WIDTH_FACTOR = 0.143;\n/** unit : maximum number of characters a column can hold at the standard font size. What. */\nconst EXCEL_DEFAULT_COL_WIDTH = 8.43;\n/** unit : points */\nconst EXCEL_DEFAULT_ROW_HEIGHT = 12.75;\nconst EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS = 30;\nconst EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS = 100;\nconst FIRST_NUMFMT_ID = 164;\nconst FORCE_DEFAULT_ARGS_FUNCTIONS = {\n    FLOOR: [{ type: \"NUMBER\", value: 1 }],\n    CEILING: [{ type: \"NUMBER\", value: 1 }],\n    ROUND: [{ type: \"NUMBER\", value: 0 }],\n    ROUNDUP: [{ type: \"NUMBER\", value: 0 }],\n    ROUNDDOWN: [{ type: \"NUMBER\", value: 0 }],\n};\n/**\n * This list contains all \"future\" functions that are not compatible with older versions of Excel\n * For more information, see https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/5d1b6d44-6fc1-4ecd-8fef-0b27406cc2bf\n */\nconst NON_RETROCOMPATIBLE_FUNCTIONS = [\n    \"ACOT\",\n    \"ACOTH\",\n    \"AGGREGATE\",\n    \"ARABIC\",\n    \"BASE\",\n    \"BETA.DIST\",\n    \"BETA.INV\",\n    \"BINOM.DIST\",\n    \"BINOM.DIST.RANGE\",\n    \"BINOM.INV\",\n    \"BITAND\",\n    \"BITLSHIFT\",\n    \"BITOR\",\n    \"BITRSHIFT\",\n    \"BITXOR\",\n    \"BYCOL\",\n    \"BYROW\",\n    \"CEILING.MATH\",\n    \"CEILING.PRECISE\",\n    \"CHISQ.DIST\",\n    \"CHISQ.DIST.RT\",\n    \"CHISQ.INV\",\n    \"CHISQ.INV.RT\",\n    \"CHISQ.TEST\",\n    \"CHOOSECOLS\",\n    \"CHOOSEROWS\",\n    \"COMBINA\",\n    \"CONCAT\",\n    \"CONFIDENCE.NORM\",\n    \"CONFIDENCE.T\",\n    \"COT\",\n    \"COTH\",\n    \"COVARIANCE.P\",\n    \"COVARIANCE.S\",\n    \"CSC\",\n    \"CSCH\",\n    \"DAYS\",\n    \"DECIMAL\",\n    \"DROP\",\n    \"ERF.PRECISE\",\n    \"ERFC.PRECISE\",\n    \"EXPAND\",\n    \"EXPON.DIST\",\n    \"F.DIST\",\n    \"F.DIST.RT\",\n    \"F.INV\",\n    \"F.INV.RT\",\n    \"F.TEST\",\n    \"FIELDVALUE\",\n    \"FILTERXML\",\n    \"FLOOR.MATH\",\n    \"FLOOR.PRECISE\",\n    \"FORECAST.ETS\",\n    \"FORECAST.ETS.CONFINT\",\n    \"FORECAST.ETS.SEASONALITY\",\n    \"FORECAST.ETS.STAT\",\n    \"FORECAST.LINEAR\",\n    \"FORMULATEXT\",\n    \"GAMMA\",\n    \"GAMMA.DIST\",\n    \"GAMMA.INV\",\n    \"GAMMALN.PRECISE\",\n    \"GAUSS\",\n    \"HSTACK\",\n    \"HYPGEOM.DIST\",\n    \"IFNA\",\n    \"IFS\",\n    \"IMCOSH\",\n    \"IMCOT\",\n    \"IMCSC\",\n    \"IMCSCH\",\n    \"IMSEC\",\n    \"IMSECH\",\n    \"IMSINH\",\n    \"IMTAN\",\n    \"ISFORMULA\",\n    \"ISOMITTED\",\n    \"ISOWEEKNUM\",\n    \"LAMBDA\",\n    \"LET\",\n    \"LOGNORM.DIST\",\n    \"LOGNORM.INV\",\n    \"MAKEARRAY\",\n    \"MAP\",\n    \"MAXIFS\",\n    \"MINIFS\",\n    \"MODE.MULT\",\n    \"MODE.SNGL\",\n    \"MUNIT\",\n    \"NEGBINOM.DIST\",\n    \"NORM.DIST\",\n    \"NORM.INV\",\n    \"NORM.S.DIST\",\n    \"NORM.S.INV\",\n    \"NUMBERVALUE\",\n    \"PDURATION\",\n    \"PERCENTILE.EXC\",\n    \"PERCENTILE.INC\",\n    \"PERCENTRANK.EXC\",\n    \"PERCENTRANK.INC\",\n    \"PERMUTATIONA\",\n    \"PHI\",\n    \"POISSON.DIST\",\n    \"PQSOURCE\",\n    \"PYTHON_STR\",\n    \"PYTHON_TYPE\",\n    \"PYTHON_TYPENAME\",\n    \"QUARTILE.EXC\",\n    \"QUARTILE.INC\",\n    \"QUERYSTRING\",\n    \"RANDARRAY\",\n    \"RANK.AVG\",\n    \"RANK.EQ\",\n    \"REDUCE\",\n    \"RRI\",\n    \"SCAN\",\n    \"SEC\",\n    \"SECH\",\n    \"SEQUENCE\",\n    \"SHEET\",\n    \"SHEETS\",\n    \"SKEW.P\",\n    \"SORTBY\",\n    \"STDEV.P\",\n    \"STDEV.S\",\n    \"SWITCH\",\n    \"T.DIST\",\n    \"T.DIST.2T\",\n    \"T.DIST.RT\",\n    \"T.INV\",\n    \"T.INV.2T\",\n    \"T.TEST\",\n    \"TAKE\",\n    \"TEXTAFTER\",\n    \"TEXTBEFORE\",\n    \"TEXTJOIN\",\n    \"TEXTSPLIT\",\n    \"TOCOL\",\n    \"TOROW\",\n    \"UNICHAR\",\n    \"UNICODE\",\n    \"UNIQUE\",\n    \"VAR.P\",\n    \"VAR.S\",\n    \"VSTACK\",\n    \"WEBSERVICE\",\n    \"WEIBULL.DIST\",\n    \"WRAPCOLS\",\n    \"WRAPROWS\",\n    \"XLOOKUP\",\n    \"XOR\",\n    \"Z.TEST\",\n];\nconst CONTENT_TYPES_FILE = \"[Content_Types].xml\";\n\n/**\n * Registry to draw icons on cells\n */\nconst iconsOnCellRegistry = new Registry();\n\ncss /* scss */ `\n  .o-spreadsheet {\n    .o-icon {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: ${ICON_EDGE_LENGTH}px;\n      height: ${ICON_EDGE_LENGTH}px;\n      font-size: ${ICON_EDGE_LENGTH}px;\n      vertical-align: middle;\n\n      .small-text {\n        font: bold 9px sans-serif;\n      }\n      .heavy-text {\n        font: bold 16px sans-serif;\n      }\n    }\n    .fa-small {\n      font-size: 14px;\n    }\n  }\n`;\n// -----------------------------------------------------------------------------\n// We need here the svg of the icons that we need to convert to images for the renderer\n// -----------------------------------------------------------------------------\nconst ARROW_DOWN = '<svg class=\"o-icon arrow-down\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 448 512\"><path fill=\"#E06666\" d=\"M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z\"></path></svg>';\nconst ARROW_UP = '<svg class=\"o-icon arrow-up\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 448 512\"><path fill=\"#6AA84F\" d=\"M34.9 289.5l-22.2-22.2c-9.4-9.4-9.4-24.6 0-33.9L207 39c9.4-9.4 24.6-9.4 33.9 0l194.3 194.3c9.4 9.4 9.4 24.6 0 33.9L413 289.4c-9.5 9.5-25 9.3-34.3-.4L264 168.6V456c0 13.3-10.7 24-24 24h-32c-13.3 0-24-10.7-24-24V168.6L69.2 289.1c-9.3 9.8-24.8 10-34.3.4z\"></path></svg>';\nconst ARROW_RIGHT = '<svg class=\"o-icon arrow-right\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 448 512\"><path fill=\"#F0AD4E\" d=\"M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z\"></path></svg>';\nconst SMILE = '<svg class=\"o-icon smile\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 496 512\"><path fill=\"#6AA84F\" d=\"M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z\"></path></svg>';\nconst MEH = '<svg class=\"o-icon meh\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 496 512\"><path fill=\"#F0AD4E\" d=\"M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm8 144H160c-13.2 0-24 10.8-24 24s10.8 24 24 24h176c13.2 0 24-10.8 24-24s-10.8-24-24-24z\"></path></svg>';\nconst FROWN = '<svg class=\"o-icon frown\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 496 512\"><path fill=\"#E06666\" d=\"M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm-80 128c-40.2 0-78 17.7-103.8 48.6-8.5 10.2-7.1 25.3 3.1 33.8 10.2 8.4 25.3 7.1 33.8-3.1 16.6-19.9 41-31.4 66.9-31.4s50.3 11.4 66.9 31.4c8.1 9.7 23.1 11.9 33.8 3.1 10.2-8.5 11.5-23.6 3.1-33.8C326 321.7 288.2 304 248 304z\"></path></svg>';\nconst GREEN_DOT = '<svg class=\"o-icon green-dot\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 512 512\"><path fill=\"#6AA84F\" d=\"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z\"></path></svg>';\nconst YELLOW_DOT = '<svg class=\"o-icon yellow-dot\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 512 512\"><path fill=\"#F0AD4E\" d=\"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z\"></path></svg>';\nconst RED_DOT = '<svg class=\"o-icon red-dot\" width=\"10\" height=\"10\" focusable=\"false\" viewBox=\"0 0 512 512\"><path fill=\"#E06666\" d=\"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z\"></path></svg>';\nfunction getIconSrc(svg) {\n    /** We have to add xmlns, as it's not added by owl in the canvas */\n    svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" ${svg.slice(4)}`;\n    return \"data:image/svg+xml; charset=utf8, \" + encodeURIComponent(svg);\n}\nconst ICONS = {\n    arrowGood: {\n        template: \"ARROW_UP\",\n        img: getIconSrc(ARROW_UP),\n    },\n    arrowNeutral: {\n        template: \"ARROW_RIGHT\",\n        img: getIconSrc(ARROW_RIGHT),\n    },\n    arrowBad: {\n        template: \"ARROW_DOWN\",\n        img: getIconSrc(ARROW_DOWN),\n    },\n    smileyGood: {\n        template: \"SMILE\",\n        img: getIconSrc(SMILE),\n    },\n    smileyNeutral: {\n        template: \"MEH\",\n        img: getIconSrc(MEH),\n    },\n    smileyBad: {\n        template: \"FROWN\",\n        img: getIconSrc(FROWN),\n    },\n    dotGood: {\n        template: \"GREEN_DOT\",\n        img: getIconSrc(GREEN_DOT),\n    },\n    dotNeutral: {\n        template: \"YELLOW_DOT\",\n        img: getIconSrc(YELLOW_DOT),\n    },\n    dotBad: {\n        template: \"RED_DOT\",\n        img: getIconSrc(RED_DOT),\n    },\n};\nconst ICON_SETS = {\n    arrows: {\n        good: \"arrowGood\",\n        neutral: \"arrowNeutral\",\n        bad: \"arrowBad\",\n    },\n    smiley: {\n        good: \"smileyGood\",\n        neutral: \"smileyNeutral\",\n        bad: \"smileyBad\",\n    },\n    dots: {\n        good: \"dotGood\",\n        neutral: \"dotNeutral\",\n        bad: \"dotBad\",\n    },\n};\niconsOnCellRegistry.add(\"conditional_formatting\", (getters, position) => {\n    const icon = getters.getConditionalIcon(position);\n    if (icon) {\n        return ICONS[icon].img;\n    }\n});\n\n/**\n * Map of the different types of conversions warnings and their name in error messages\n */\nvar WarningTypes;\n(function (WarningTypes) {\n    WarningTypes[\"DiagonalBorderNotSupported\"] = \"Diagonal Borders\";\n    WarningTypes[\"BorderStyleNotSupported\"] = \"Border style\";\n    WarningTypes[\"FillStyleNotSupported\"] = \"Fill Style\";\n    WarningTypes[\"FontNotSupported\"] = \"Font\";\n    WarningTypes[\"HorizontalAlignmentNotSupported\"] = \"Horizontal Alignment\";\n    WarningTypes[\"VerticalAlignmentNotSupported\"] = \"Vertical Alignments\";\n    WarningTypes[\"MultipleRulesCfNotSupported\"] = \"Multiple rules conditional formats\";\n    WarningTypes[\"CfTypeNotSupported\"] = \"Conditional format type\";\n    WarningTypes[\"CfFormatBorderNotSupported\"] = \"Borders in conditional formats\";\n    WarningTypes[\"CfFormatAlignmentNotSupported\"] = \"Alignment in conditional formats\";\n    WarningTypes[\"CfFormatNumFmtNotSupported\"] = \"Num formats in conditional formats\";\n    WarningTypes[\"CfIconSetEmptyIconNotSupported\"] = \"IconSets with empty icons\";\n    WarningTypes[\"BadlyFormattedHyperlink\"] = \"Badly formatted hyperlink\";\n    WarningTypes[\"NumFmtIdNotSupported\"] = \"Number format\";\n})(WarningTypes || (WarningTypes = {}));\nclass XLSXImportWarningManager {\n    _parsingWarnings = new Set();\n    _conversionWarnings = new Set();\n    addParsingWarning(warning) {\n        this._parsingWarnings.add(warning);\n    }\n    addConversionWarning(warning) {\n        this._conversionWarnings.add(warning);\n    }\n    get warnings() {\n        return [...this._parsingWarnings, ...this._conversionWarnings];\n    }\n    /**\n     * Add a warning \"... is not supported\" to the manager.\n     *\n     * @param type the type of the warning to add\n     * @param name optional, name of the element that was not supported\n     * @param supported optional, list of the supported elements\n     */\n    generateNotSupportedWarning(type, name, supported) {\n        let warning = `${type} ${name ? '\"' + name + '\" is' : \"are\"} not yet supported. `;\n        if (supported) {\n            warning += `Only ${supported.join(\", \")} are currently supported.`;\n        }\n        if (!this._conversionWarnings.has(warning)) {\n            this._conversionWarnings.add(warning);\n        }\n    }\n}\n\nconst SUPPORTED_BORDER_STYLES = [\"thin\", \"medium\", \"thick\", \"dashed\", \"dotted\"];\nconst SUPPORTED_HORIZONTAL_ALIGNMENTS = [\n    \"general\",\n    \"left\",\n    \"center\",\n    \"right\",\n];\nconst SUPPORTED_VERTICAL_ALIGNMENTS = [\"top\", \"center\", \"bottom\"];\nconst SUPPORTED_FONTS = [\"Arial\"];\nconst SUPPORTED_FILL_PATTERNS = [\"solid\"];\nconst SUPPORTED_CF_TYPES = [\n    \"expression\",\n    \"cellIs\",\n    \"colorScale\",\n    \"iconSet\",\n    \"containsText\",\n    \"notContainsText\",\n    \"beginsWith\",\n    \"endsWith\",\n    \"containsBlanks\",\n    \"notContainsBlanks\",\n];\n/** Map between cell type in XLSX file and human readable cell type  */\nconst CELL_TYPE_CONVERSION_MAP = {\n    b: \"boolean\",\n    d: \"date\",\n    e: \"error\",\n    inlineStr: \"inlineStr\",\n    n: \"number\",\n    s: \"sharedString\",\n    str: \"str\",\n};\n/** Conversion map Border Style in XLSX <=> Border style in o_spreadsheet*/\nconst BORDER_STYLE_CONVERSION_MAP = {\n    dashDot: \"thin\",\n    dashDotDot: \"thin\",\n    dashed: \"dashed\",\n    dotted: \"dotted\",\n    double: \"thin\",\n    hair: \"thin\",\n    medium: \"medium\",\n    mediumDashDot: \"thin\",\n    mediumDashDotDot: \"thin\",\n    mediumDashed: \"thin\",\n    none: undefined,\n    slantDashDot: \"thin\",\n    thick: \"thick\",\n    thin: \"thin\",\n};\n/** Conversion map Horizontal Alignment in XLSX <=> Horizontal Alignment in o_spreadsheet*/\nconst H_ALIGNMENT_CONVERSION_MAP = {\n    general: undefined,\n    left: \"left\",\n    center: \"center\",\n    right: \"right\",\n    fill: \"left\",\n    justify: \"left\",\n    centerContinuous: \"center\",\n    distributed: \"center\",\n};\n/** Conversion map Vertical Alignment in XLSX => Vertical Alignment in o_spreadsheet */\nconst V_ALIGNMENT_CONVERSION_MAP = {\n    top: \"top\",\n    center: \"middle\",\n    bottom: \"bottom\",\n    justify: \"middle\",\n    distributed: \"middle\",\n};\n/** Conversion map Vertical Alignment in o-spreadsheet => Vertical Alignment in XLSX */\nconst V_ALIGNMENT_EXPORT_CONVERSION_MAP = {\n    top: \"top\",\n    middle: \"center\",\n    bottom: \"bottom\",\n};\n/** Convert the \"CellIs\" cf operator.\n * We have all the operators that the xlsx have, but ours begin with a uppercase character */\nfunction convertCFCellIsOperator(xlsxCfOperator) {\n    return (xlsxCfOperator.slice(0, 1).toUpperCase() +\n        xlsxCfOperator.slice(1));\n}\n/** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */\nconst CF_TYPE_CONVERSION_MAP = {\n    aboveAverage: undefined,\n    expression: undefined,\n    cellIs: undefined, // exist but isn't an operator in o_spreadsheet\n    colorScale: undefined, // exist but isn't an operator in o_spreadsheet\n    dataBar: undefined,\n    iconSet: undefined, // exist but isn't an operator in o_spreadsheet\n    top10: undefined,\n    uniqueValues: undefined,\n    duplicateValues: undefined,\n    containsText: \"ContainsText\",\n    notContainsText: \"NotContains\",\n    beginsWith: \"BeginsWith\",\n    endsWith: \"EndsWith\",\n    containsBlanks: \"IsEmpty\",\n    notContainsBlanks: \"IsNotEmpty\",\n    containsErrors: undefined,\n    notContainsErrors: undefined,\n    timePeriod: undefined,\n};\n/** Conversion map CF thresholds types in XLSX <=> Cf thresholds types in o_spreadsheet */\nconst CF_THRESHOLD_CONVERSION_MAP = {\n    num: \"number\",\n    percent: \"percentage\",\n    max: \"value\",\n    min: \"value\",\n    percentile: \"percentile\",\n    formula: \"formula\",\n};\n/**\n * Conversion map between Excels IconSets and our own IconSets. The string is the key of the iconset in the ICON_SETS constant.\n *\n * NoIcons is undefined instead of an empty string because we don't support it and need to mange it separately.\n */\nconst ICON_SET_CONVERSION_MAP = {\n    NoIcons: undefined,\n    \"3Arrows\": \"arrows\",\n    \"3ArrowsGray\": \"arrows\",\n    \"3Symbols\": \"smiley\",\n    \"3Symbols2\": \"smiley\",\n    \"3Signs\": \"dots\",\n    \"3Flags\": \"dots\",\n    \"3TrafficLights1\": \"dots\",\n    \"3TrafficLights2\": \"dots\",\n    \"4Arrows\": \"arrows\",\n    \"4ArrowsGray\": \"arrows\",\n    \"4RedToBlack\": \"dots\",\n    \"4Rating\": \"smiley\",\n    \"4TrafficLights\": \"dots\",\n    \"5Arrows\": \"arrows\",\n    \"5ArrowsGray\": \"arrows\",\n    \"5Rating\": \"smiley\",\n    \"5Quarters\": \"dots\",\n    \"3Stars\": \"smiley\",\n    \"3Triangles\": \"arrows\",\n    \"5Boxes\": \"dots\",\n};\n/** Map between legend position in XLSX file and human readable position  */\nconst DRAWING_LEGEND_POSITION_CONVERSION_MAP = {\n    b: \"bottom\",\n    t: \"top\",\n    l: \"left\",\n    r: \"right\",\n    tr: \"right\",\n};\n/** Conversion map chart types in XLSX <=> Cf chart types o_spreadsheet (undefined for unsupported chart types)*/\nconst CHART_TYPE_CONVERSION_MAP = {\n    areaChart: undefined,\n    area3DChart: undefined,\n    lineChart: \"line\",\n    line3DChart: undefined,\n    stockChart: undefined,\n    radarChart: undefined,\n    scatterChart: \"scatter\",\n    pieChart: \"pie\",\n    pie3DChart: undefined,\n    doughnutChart: \"pie\",\n    barChart: \"bar\",\n    bar3DChart: undefined,\n    ofPieChart: undefined,\n    surfaceChart: undefined,\n    surface3DChart: undefined,\n    bubbleChart: undefined,\n    comboChart: \"combo\",\n};\n/** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/\nconst SUBTOTAL_FUNCTION_CONVERSION_MAP = {\n    \"1\": \"AVERAGE\",\n    \"2\": \"COUNT\",\n    \"3\": \"COUNTA\",\n    \"4\": \"MAX\",\n    \"5\": \"MIN\",\n    \"6\": \"PRODUCT\",\n    \"7\": \"STDEV\",\n    \"8\": \"STDEVP\",\n    \"9\": \"SUM\",\n    \"10\": \"VAR\",\n    \"11\": \"VARP\",\n    \"101\": \"AVERAGE\",\n    \"102\": \"COUNT\",\n    \"103\": \"COUNTA\",\n    \"104\": \"MAX\",\n    \"105\": \"MIN\",\n    \"106\": \"PRODUCT\",\n    \"107\": \"STDEV\",\n    \"108\": \"STDEVP\",\n    \"109\": \"SUM\",\n    \"110\": \"VAR\",\n    \"111\": \"VARP\",\n};\n/** Mapping between Excel format indexes (see XLSX_FORMAT_MAP) and some supported formats  */\nconst XLSX_FORMATS_CONVERSION_MAP = {\n    0: \"\",\n    1: \"0\",\n    2: \"0.00\",\n    3: \"#,#00\",\n    4: \"#,##0.00\",\n    9: \"0%\",\n    10: \"0.00%\",\n    11: undefined,\n    12: undefined,\n    13: undefined,\n    14: \"m/d/yyyy\",\n    15: \"m/d/yyyy\",\n    16: \"m/d/yyyy\",\n    17: \"m/d/yyyy\",\n    18: \"hh:mm:ss a\",\n    19: \"hh:mm:ss a\",\n    20: \"hhhh:mm:ss\",\n    21: \"hhhh:mm:ss\",\n    22: \"m/d/yy h:mm\",\n    37: undefined,\n    38: undefined,\n    39: undefined,\n    40: undefined,\n    45: \"hhhh:mm:ss\",\n    46: \"hhhh:mm:ss\",\n    47: \"hhhh:mm:ss\",\n    48: undefined,\n    49: \"@\",\n};\n/**\n * Mapping format index to format defined by default\n *\n * OpenXML $18.8.30\n * */\nconst XLSX_FORMAT_MAP = {\n    \"0\": 1,\n    \"0.00\": 2,\n    \"#,#00\": 3,\n    \"#,##0.00\": 4,\n    \"0%\": 9,\n    \"0.00%\": 10,\n    \"0.00E+00\": 11,\n    \"# ?/?\": 12,\n    \"# ??/??\": 13,\n    \"mm-dd-yy\": 14,\n    \"d-mm-yy\": 15,\n    \"mm-yy\": 16,\n    \"mmm-yy\": 17,\n    \"h:mm AM/PM\": 18,\n    \"h:mm:ss AM/PM\": 19,\n    \"h:mm\": 20,\n    \"h:mm:ss\": 21,\n    \"m/d/yy h:mm\": 22,\n    \"#,##0 ;(#,##0)\": 37,\n    \"#,##0 ;[Red](#,##0)\": 38,\n    \"#,##0.00;(#,##0.00)\": 39,\n    \"#,##0.00;[Red](#,##0.00)\": 40,\n    \"mm:ss\": 45,\n    \"[h]:mm:ss\": 46,\n    \"mmss.0\": 47,\n    \"##0.0E+0\": 48,\n    \"@\": 49,\n    \"hh:mm:ss a\": 19, // TODO: discuss: this format is not recognized by excel for example (doesn't follow their guidelines I guess)\n};\n/** OpenXML $18.8.27 */\nconst XLSX_INDEXED_COLORS = {\n    0: \"000000\",\n    1: \"FFFFFF\",\n    2: \"FF0000\",\n    3: \"00FF00\",\n    4: \"0000FF\",\n    5: \"FFFF00\",\n    6: \"FF00FF\",\n    7: \"00FFFF\",\n    8: \"000000\",\n    9: \"FFFFFF\",\n    10: \"FF0000\",\n    11: \"00FF00\",\n    12: \"0000FF\",\n    13: \"FFFF00\",\n    14: \"FF00FF\",\n    15: \"00FFFF\",\n    16: \"800000\",\n    17: \"008000\",\n    18: \"000080\",\n    19: \"808000\",\n    20: \"800080\",\n    21: \"008080\",\n    22: \"C0C0C0\",\n    23: \"808080\",\n    24: \"9999FF\",\n    25: \"993366\",\n    26: \"FFFFCC\",\n    27: \"CCFFFF\",\n    28: \"660066\",\n    29: \"FF8080\",\n    30: \"0066CC\",\n    31: \"CCCCFF\",\n    32: \"000080\",\n    33: \"FF00FF\",\n    34: \"FFFF00\",\n    35: \"00FFFF\",\n    36: \"800080\",\n    37: \"800000\",\n    38: \"008080\",\n    39: \"0000FF\",\n    40: \"00CCFF\",\n    41: \"CCFFFF\",\n    42: \"CCFFCC\",\n    43: \"FFFF99\",\n    44: \"99CCFF\",\n    45: \"FF99CC\",\n    46: \"CC99FF\",\n    47: \"FFCC99\",\n    48: \"3366FF\",\n    49: \"33CCCC\",\n    50: \"99CC00\",\n    51: \"FFCC00\",\n    52: \"FF9900\",\n    53: \"FF6600\",\n    54: \"666699\",\n    55: \"969696\",\n    56: \"003366\",\n    57: \"339966\",\n    58: \"003300\",\n    59: \"333300\",\n    60: \"993300\",\n    61: \"993366\",\n    62: \"333399\",\n    63: \"333333\",\n    64: \"000000\", // system foreground\n    65: \"FFFFFF\", // system background\n};\nconst IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {\n    \"image/avif\": \"avif\",\n    \"image/bmp\": \"bmp\",\n    \"image/gif\": \"gif\",\n    \"image/vnd.microsoft.icon\": \"ico\",\n    \"image/jpeg\": \"jpeg\",\n    \"image/png\": \"png\",\n    \"image/tiff\": \"tiff\",\n    \"image/webp\": \"webp\",\n};\nconst IMAGE_EXTENSION_TO_MIMETYPE_MAPPING = {\n    avif: \"image/avif\",\n    bmp: \"image/bmp\",\n    gif: \"image/gif\",\n    ico: \"image/vnd.microsoft.icon\",\n    jpeg: \"image/jpeg\",\n    png: \"image/png\",\n    tiff: \"image/tiff\",\n    webp: \"image/webp\",\n    jpg: \"image/jpeg\",\n};\n\n/**\n * Most of the functions could stay private, but are exported for testing purposes\n */\n/**\n *\n * Extract the color referenced inside of an XML element and return it as an hex string #RRGGBBAA (or #RRGGBB\n * if alpha = FF)\n *\n *  The color is an attribute of the element that can be :\n *  - rgb : an rgb string\n *  - theme : a reference to a theme element\n *  - auto : automatic coloring. Return const AUTO_COLOR in constants.ts.\n *  - indexed : a legacy indexing scheme for colors. The only value that should be present in a xlsx is\n *      64 = System Foreground, that we can replace with AUTO_COLOR.\n */\nfunction convertColor(xlsxColor) {\n    if (!xlsxColor) {\n        return undefined;\n    }\n    let rgb;\n    if (xlsxColor.rgb) {\n        rgb = xlsxColor.rgb;\n    }\n    else if (xlsxColor.auto) {\n        rgb = AUTO_COLOR;\n    }\n    else if (xlsxColor.indexed) {\n        rgb = XLSX_INDEXED_COLORS[xlsxColor.indexed];\n    }\n    else {\n        return undefined;\n    }\n    rgb = xlsxColorToHEXA(rgb);\n    if (xlsxColor.tint) {\n        rgb = applyTint(rgb, xlsxColor.tint);\n    }\n    rgb = rgb.toUpperCase();\n    // Remove unnecessary alpha\n    if (rgb.length === 9 && rgb.endsWith(\"FF\")) {\n        rgb = rgb.slice(0, 7);\n    }\n    return rgb;\n}\n/**\n * Convert a hex color AARRGGBB (or RRGGBB)(representation inside XLSX Xmls) to a standard js color\n * representation #RRGGBBAA\n */\nfunction xlsxColorToHEXA(color) {\n    if (color.length === 6)\n        return \"#\" + color + \"FF\";\n    return \"#\" + color.slice(2) + color.slice(0, 2);\n}\n/**\n *  Apply tint to a color (see OpenXml spec \u00a718.3.1.15);\n */\nfunction applyTint(color, tint) {\n    const rgba = colorToRGBA(color);\n    const hsla = rgbaToHSLA(rgba);\n    if (tint < 0) {\n        hsla.l = hsla.l * (1 + tint);\n    }\n    if (tint > 0) {\n        hsla.l = hsla.l * (1 - tint) + (100 - 100 * (1 - tint));\n    }\n    return rgbaToHex(hslaToRGBA(hsla));\n}\n/**\n * Convert a hex + alpha color string to an integer representation. Also remove the alpha.\n *\n * eg. #FF0000FF => 4278190335\n */\nfunction hexaToInt(hex) {\n    if (hex.length === 9) {\n        hex = hex.slice(0, 7);\n    }\n    return parseInt(hex.replace(\"#\", \"\"), 16);\n}\n/**\n * When defining style (fontColor, borderColor for instance)\n * Excel will specify rgb=\"FF000000\"\n * In that case, We should not consider this value as user-defined but\n * rather like an instruction: \"Use your system default\"\n */\nconst DEFAULT_SYSTEM_COLOR = \"FF000000\";\n\n/**\n * Get the relative path between two files\n *\n * Eg.:\n * from \"folder1/file1.txt\" to \"folder2/file2.txt\" => \"../folder2/file2.txt\"\n */\nfunction getRelativePath(from, to) {\n    const fromPathParts = from.split(\"/\");\n    const toPathParts = to.split(\"/\");\n    let relPath = \"\";\n    let startIndex = 0;\n    for (let i = 0; i < fromPathParts.length - 1; i++) {\n        if (fromPathParts[i] === toPathParts[i]) {\n            startIndex++;\n        }\n        else {\n            relPath += \"../\";\n        }\n    }\n    relPath += toPathParts.slice(startIndex).join(\"/\");\n    return relPath;\n}\n/**\n * Convert an array of element into an object where the objects keys were the elements position in the array.\n * Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.\n *\n * eg. : [\"a\", \"b\"] => {0:\"a\", 1:\"b\"}\n */\nfunction arrayToObject(array, indexOffset = 0) {\n    const obj = {};\n    for (let i = 0; i < array.length; i++) {\n        if (array[i]) {\n            obj[i + indexOffset] = array[i];\n        }\n    }\n    return obj;\n}\n/**\n * In xlsx we can have string with unicode characters with the format _x00fa_.\n * Replace with characters understandable by JS\n */\nfunction fixXlsxUnicode(str) {\n    return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {\n        return String.fromCharCode(parseInt(code, 16));\n    });\n}\n/** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */\nfunction getSheetDataHeader(sheetData, dimension, index) {\n    if (dimension === \"COL\") {\n        if (!sheetData.cols[index]) {\n            sheetData.cols[index] = {};\n        }\n        return sheetData.cols[index];\n    }\n    if (!sheetData.rows[index]) {\n        sheetData.rows[index] = {};\n    }\n    return sheetData.rows[index];\n}\n\nconst XLSX_DATE_FORMAT_REGEX = /^(yy|yyyy|m{1,5}|d{1,4}|h{1,2}|s{1,2}|am\\/pm|a\\/m|\\s|-|\\/|\\.|:)+$/i;\n/**\n * Convert excel format to o_spreadsheet format\n *\n * Excel format are defined in openXML \u00a718.8.31\n */\nfunction convertXlsxFormat(numFmtId, formats, warningManager) {\n    if (numFmtId === 0) {\n        return undefined;\n    }\n    // Format is either defined in the imported data, or the formatId is defined in openXML \u00a718.8.30\n    let format = XLSX_FORMATS_CONVERSION_MAP[numFmtId] || formats.find((f) => f.id === numFmtId)?.format;\n    if (format) {\n        try {\n            let convertedFormat = format.replace(/\\[(.*)-[A-Z0-9]{3}\\]/g, \"[$1]\"); // remove currency and locale/date system/number system info (ECMA \u00a718.8.31)\n            convertedFormat = convertedFormat.replace(/\\[\\$\\]/g, \"\"); // remove empty bocks\n            convertedFormat = convertedFormat.replace(/_.{1}/g, \"\"); // _ === ignore width of next char for align purposes. Not supported ATM\n            convertedFormat = convertedFormat.replace(/\\*.{1}/g, \"\"); // * === repeat next character enough to fill the line. Not supported ATM\n            if (isXlsxDateFormat(convertedFormat)) {\n                convertedFormat = convertDateFormat$1(convertedFormat);\n            }\n            if (isFormatSupported(convertedFormat)) {\n                return convertedFormat;\n            }\n        }\n        catch (e) { }\n    }\n    warningManager.generateNotSupportedWarning(WarningTypes.NumFmtIdNotSupported, format || `nmFmtId ${numFmtId}`);\n    return undefined;\n}\nfunction isFormatSupported(format) {\n    try {\n        formatValue(0, { format, locale: DEFAULT_LOCALE });\n        return true;\n    }\n    catch (e) {\n        return false;\n    }\n}\nfunction isXlsxDateFormat(format) {\n    return XLSX_DATE_FORMAT_REGEX.test(format);\n}\nfunction convertDateFormat$1(format) {\n    // Some of these aren't defined neither in the OpenXML spec not the Xlsx extension of OpenXML,\n    // but can still occur and are supported by Excel/Google sheets\n    format = format.toLowerCase();\n    format = format.replace(/mmmmm/g, \"mmm\");\n    format = format.replace(/am\\/pm|a\\/m/g, \"a\");\n    format = format.replace(/hhhh/g, \"hh\");\n    format = format.replace(/\\bh\\b/g, \"hh\");\n    return format;\n}\n\nfunction convertBorders(data, warningManager) {\n    const borderArray = data.borders.map((border) => {\n        addBorderWarnings(border, warningManager);\n        const b = {\n            top: convertBorderDescr$1(border.top, warningManager),\n            bottom: convertBorderDescr$1(border.bottom, warningManager),\n            left: convertBorderDescr$1(border.left, warningManager),\n            right: convertBorderDescr$1(border.right, warningManager),\n        };\n        Object.keys(b).forEach((key) => b[key] === undefined && delete b[key]);\n        return b;\n    });\n    return arrayToObject(borderArray, 1);\n}\nfunction convertBorderDescr$1(borderDescr, warningManager) {\n    if (!borderDescr)\n        return undefined;\n    addBorderDescrWarnings(borderDescr, warningManager);\n    const style = BORDER_STYLE_CONVERSION_MAP[borderDescr.style];\n    return style ? { style, color: convertColor(borderDescr.color) } : undefined;\n}\nfunction convertStyles(data, warningManager) {\n    const stylesArray = data.styles.map((style) => {\n        return convertStyle({\n            fontStyle: data.fonts[style.fontId],\n            fillStyle: data.fills[style.fillId],\n            alignment: style.alignment,\n        }, warningManager);\n    });\n    return arrayToObject(stylesArray, 1);\n}\nfunction convertStyle(styleStruct, warningManager) {\n    addStyleWarnings(styleStruct?.fontStyle, styleStruct?.fillStyle, warningManager);\n    addHorizontalAlignmentWarnings(styleStruct?.alignment?.horizontal, warningManager);\n    addVerticalAlignmentWarnings(styleStruct?.alignment?.vertical, warningManager);\n    return {\n        bold: styleStruct.fontStyle?.bold,\n        italic: styleStruct.fontStyle?.italic,\n        strikethrough: styleStruct.fontStyle?.strike,\n        underline: styleStruct.fontStyle?.underline,\n        verticalAlign: styleStruct.alignment?.vertical\n            ? V_ALIGNMENT_CONVERSION_MAP[styleStruct.alignment.vertical]\n            : undefined,\n        align: styleStruct.alignment?.horizontal\n            ? H_ALIGNMENT_CONVERSION_MAP[styleStruct.alignment.horizontal]\n            : undefined,\n        // In xlsx fills, bgColor is the color of the fill, and fgColor is the color of the pattern above the background, except in solid fills\n        fillColor: styleStruct.fillStyle?.patternType === \"solid\"\n            ? convertColor(styleStruct.fillStyle?.fgColor)\n            : convertColor(styleStruct.fillStyle?.bgColor),\n        textColor: convertColor(styleStruct.fontStyle?.color),\n        fontSize: styleStruct.fontStyle?.size,\n        wrapping: styleStruct.alignment?.wrapText ? \"wrap\" : \"overflow\",\n    };\n}\nfunction convertFormats(data, warningManager) {\n    const formats = [];\n    for (let style of data.styles) {\n        const format = convertXlsxFormat(style.numFmtId, data.numFmts, warningManager);\n        if (format) {\n            formats[style.numFmtId] = format;\n        }\n    }\n    return arrayToObject(formats, 1);\n}\n// ---------------------------------------------------------------------------\n// Warnings\n// ---------------------------------------------------------------------------\nfunction addStyleWarnings(font, fill, warningManager) {\n    if (font && font.name && !SUPPORTED_FONTS.includes(font.name)) {\n        warningManager.generateNotSupportedWarning(WarningTypes.FontNotSupported, font.name, SUPPORTED_FONTS);\n    }\n    if (fill && fill.patternType && !SUPPORTED_FILL_PATTERNS.includes(fill.patternType)) {\n        warningManager.generateNotSupportedWarning(WarningTypes.FillStyleNotSupported, fill.patternType, SUPPORTED_FILL_PATTERNS);\n    }\n}\nfunction addBorderDescrWarnings(borderDescr, warningManager) {\n    if (!SUPPORTED_BORDER_STYLES.includes(borderDescr.style)) {\n        warningManager.generateNotSupportedWarning(WarningTypes.BorderStyleNotSupported, borderDescr.style, SUPPORTED_BORDER_STYLES);\n    }\n}\nfunction addBorderWarnings(border, warningManager) {\n    if (border.diagonal) {\n        warningManager.generateNotSupportedWarning(WarningTypes.DiagonalBorderNotSupported);\n    }\n}\nfunction addHorizontalAlignmentWarnings(alignment, warningManager) {\n    if (alignment && !SUPPORTED_HORIZONTAL_ALIGNMENTS.includes(alignment)) {\n        warningManager.generateNotSupportedWarning(WarningTypes.HorizontalAlignmentNotSupported, alignment, SUPPORTED_HORIZONTAL_ALIGNMENTS);\n    }\n}\nfunction addVerticalAlignmentWarnings(alignment, warningManager) {\n    if (alignment && !SUPPORTED_VERTICAL_ALIGNMENTS.includes(alignment)) {\n        warningManager.generateNotSupportedWarning(WarningTypes.VerticalAlignmentNotSupported, alignment, SUPPORTED_VERTICAL_ALIGNMENTS);\n    }\n}\n\nfunction convertConditionalFormats(xlsxCfs, dxfs, warningManager) {\n    const cfs = [];\n    let cfId = 1;\n    for (let cf of xlsxCfs) {\n        if (cf.cfRules.length === 0)\n            continue;\n        addCfConversionWarnings(cf, dxfs, warningManager);\n        const rule = cf.cfRules[0];\n        let operator;\n        const values = [];\n        if (rule.dxfId === undefined && !(rule.type === \"colorScale\" || rule.type === \"iconSet\"))\n            continue;\n        switch (rule.type) {\n            case \"aboveAverage\":\n            case \"containsErrors\":\n            case \"notContainsErrors\":\n            case \"dataBar\":\n            case \"duplicateValues\":\n            case \"expression\":\n            case \"top10\":\n            case \"uniqueValues\":\n            case \"timePeriod\":\n                // Not supported\n                continue;\n            case \"colorScale\":\n                const colorScale = convertColorScale(cfId++, cf);\n                if (colorScale) {\n                    cfs.push(colorScale);\n                }\n                continue;\n            case \"iconSet\":\n                const iconSet = convertIconSet(cfId++, cf, warningManager);\n                if (iconSet) {\n                    cfs.push(iconSet);\n                }\n                continue;\n            case \"containsText\":\n            case \"notContainsText\":\n            case \"beginsWith\":\n            case \"endsWith\":\n                if (!rule.text)\n                    continue;\n                operator = CF_TYPE_CONVERSION_MAP[rule.type];\n                values.push(rule.text);\n                break;\n            case \"containsBlanks\":\n            case \"notContainsBlanks\":\n                operator = CF_TYPE_CONVERSION_MAP[rule.type];\n                break;\n            case \"cellIs\":\n                if (!rule.operator || !rule.formula || rule.formula.length === 0)\n                    continue;\n                operator = convertCFCellIsOperator(rule.operator);\n                values.push(rule.formula[0]);\n                if (rule.formula.length === 2) {\n                    values.push(rule.formula[1]);\n                }\n                break;\n        }\n        if (operator && rule.dxfId !== undefined) {\n            cfs.push({\n                id: (cfId++).toString(),\n                ranges: cf.sqref,\n                stopIfTrue: rule.stopIfTrue,\n                rule: {\n                    type: \"CellIsRule\",\n                    operator: operator,\n                    values: values,\n                    style: convertStyle({ fontStyle: dxfs[rule.dxfId].font, fillStyle: dxfs[rule.dxfId].fill }, warningManager),\n                },\n            });\n        }\n    }\n    return cfs;\n}\nfunction convertColorScale(id, xlsxCf) {\n    const scale = xlsxCf.cfRules[0].colorScale;\n    if (!scale ||\n        scale.cfvos.length !== scale.colors.length ||\n        scale.cfvos.length < 2 ||\n        scale.cfvos.length > 3) {\n        return undefined;\n    }\n    const thresholds = [];\n    for (let i = 0; i < scale.cfvos.length; i++) {\n        thresholds.push({\n            color: hexaToInt(convertColor(scale.colors[i]) || \"#FFFFFF\"),\n            type: CF_THRESHOLD_CONVERSION_MAP[scale.cfvos[i].type],\n            value: scale.cfvos[i].value,\n        });\n    }\n    const minimum = thresholds[0];\n    const maximum = thresholds.length === 2 ? thresholds[1] : thresholds[2];\n    const midpoint = thresholds.length === 3 ? thresholds[1] : undefined;\n    return {\n        id: id.toString(),\n        stopIfTrue: xlsxCf.cfRules[0].stopIfTrue,\n        ranges: xlsxCf.sqref,\n        rule: { type: \"ColorScaleRule\", minimum, midpoint, maximum },\n    };\n}\n/**\n * Convert Icons Sets.\n *\n * In the Xlsx extension of OpenXml, the IconSets can either be simply an IconSet, or a list of Icons\n *  (ie. their respective IconSet and their id in this set).\n *\n * In the case of a list of icons :\n *  - The order of the icons is lower => middle => upper\n *  - The their ids are :  0 : bad, 1 : neutral, 2 : good\n */\nfunction convertIconSet(id, xlsxCf, warningManager) {\n    const xlsxIconSet = xlsxCf.cfRules[0].iconSet;\n    if (!xlsxIconSet)\n        return undefined;\n    let cfVos = xlsxIconSet.cfvos;\n    let cfIcons = xlsxIconSet.cfIcons;\n    if (cfVos.length < 3 || (cfIcons && cfIcons.length < 3)) {\n        return undefined;\n    }\n    // We don't support icon sets with more than 3 icons, so take the extrema and the middle.\n    if (cfVos.length > 3) {\n        cfVos = [cfVos[0], cfVos[Math.floor(cfVos.length / 2)], cfVos[cfVos.length - 1]];\n    }\n    if (cfIcons && cfIcons.length > 3) {\n        cfIcons = [cfIcons[0], cfIcons[Math.floor(cfIcons.length / 2)], cfIcons[cfIcons.length - 1]];\n    }\n    // In xlsx, the thresholds are NOT in the first cfVo, but on the second and third\n    const thresholds = [];\n    for (let i = 1; i <= 2; i++) {\n        const type = CF_THRESHOLD_CONVERSION_MAP[cfVos[i].type];\n        if (type === \"value\") {\n            return undefined;\n        }\n        thresholds.push({\n            value: cfVos[i].value || \"\",\n            operator: cfVos[i].gte ? \"ge\" : \"gt\",\n            type: type,\n        });\n    }\n    let icons = {\n        lower: cfIcons\n            ? convertIcons(cfIcons[0].iconSet, cfIcons[0].iconId)\n            : convertIcons(xlsxIconSet.iconSet, 0),\n        middle: cfIcons\n            ? convertIcons(cfIcons[1].iconSet, cfIcons[1].iconId)\n            : convertIcons(xlsxIconSet.iconSet, 1),\n        upper: cfIcons\n            ? convertIcons(cfIcons[2].iconSet, cfIcons[2].iconId)\n            : convertIcons(xlsxIconSet.iconSet, 2),\n    };\n    if (xlsxIconSet.reverse) {\n        icons = { upper: icons.lower, middle: icons.middle, lower: icons.upper };\n    }\n    // We don't support empty icons in an IconSet, put a dot icon instead\n    for (let key of Object.keys(icons)) {\n        if (!icons[key]) {\n            warningManager.generateNotSupportedWarning(WarningTypes.CfIconSetEmptyIconNotSupported);\n            switch (key) {\n                case \"upper\":\n                    icons[key] = ICON_SETS.dots.good;\n                    break;\n                case \"middle\":\n                    icons[key] = ICON_SETS.dots.neutral;\n                    break;\n                case \"lower\":\n                    icons[key] = ICON_SETS.dots.bad;\n                    break;\n            }\n        }\n    }\n    return {\n        id: id.toString(),\n        stopIfTrue: xlsxCf.cfRules[0].stopIfTrue,\n        ranges: xlsxCf.sqref,\n        rule: {\n            type: \"IconSetRule\",\n            icons: icons,\n            upperInflectionPoint: thresholds[1],\n            lowerInflectionPoint: thresholds[0],\n        },\n    };\n}\n/**\n * Convert an icon from a XLSX.\n *\n * The indexes are : 0 : bad, 1 : neutral, 2 : good\n */\nfunction convertIcons(xlsxIconSet, index) {\n    const iconSet = ICON_SET_CONVERSION_MAP[xlsxIconSet];\n    if (!iconSet)\n        return \"\";\n    return index === 0\n        ? ICON_SETS[iconSet].bad\n        : index === 1\n            ? ICON_SETS[iconSet].neutral\n            : ICON_SETS[iconSet].good;\n}\n// ---------------------------------------------------------------------------\n// Warnings\n// ---------------------------------------------------------------------------\nfunction addCfConversionWarnings(cf, dxfs, warningManager) {\n    if (cf.cfRules.length > 1) {\n        warningManager.generateNotSupportedWarning(WarningTypes.MultipleRulesCfNotSupported);\n    }\n    if (!SUPPORTED_CF_TYPES.includes(cf.cfRules[0].type)) {\n        warningManager.generateNotSupportedWarning(WarningTypes.CfTypeNotSupported, cf.cfRules[0].type);\n    }\n    if (cf.cfRules[0].dxfId) {\n        const dxf = dxfs[cf.cfRules[0].dxfId];\n        if (dxf.border) {\n            warningManager.generateNotSupportedWarning(WarningTypes.CfFormatBorderNotSupported);\n        }\n        if (dxf.alignment) {\n            warningManager.generateNotSupportedWarning(WarningTypes.CfFormatAlignmentNotSupported);\n        }\n        if (dxf.numFmt) {\n            warningManager.generateNotSupportedWarning(WarningTypes.CfFormatNumFmtNotSupported);\n        }\n    }\n}\n\n// -------------------------------------\n//            CF HELPERS\n// -------------------------------------\n/**\n * Convert the conditional formatting o-spreadsheet operator to\n * the corresponding excel operator.\n * */\nfunction convertOperator(operator) {\n    switch (operator) {\n        case \"IsNotEmpty\":\n            return \"notContainsBlanks\";\n        case \"IsEmpty\":\n            return \"containsBlanks\";\n        case \"NotContains\":\n            return \"notContainsBlanks\";\n        default:\n            return operator.charAt(0).toLowerCase() + operator.slice(1);\n    }\n}\n// -------------------------------------\n//        WORKSHEET HELPERS\n// -------------------------------------\nfunction getCellType(value) {\n    switch (typeof value) {\n        case \"boolean\":\n            return \"b\";\n        case \"string\":\n            return \"str\";\n        case \"number\":\n            return \"n\";\n        default:\n            return undefined;\n    }\n}\nfunction convertHeightToExcel(height) {\n    return Math.round(HEIGHT_FACTOR * height * 100) / 100;\n}\nfunction convertWidthToExcel(width) {\n    return Math.round(WIDTH_FACTOR * width * 100) / 100;\n}\nfunction convertHeightFromExcel(height) {\n    if (!height)\n        return height;\n    return Math.round((height / HEIGHT_FACTOR) * 100) / 100;\n}\nfunction convertWidthFromExcel(width) {\n    if (!width)\n        return width;\n    return Math.round((width / WIDTH_FACTOR) * 100) / 100;\n}\nfunction extractStyle(cell, data) {\n    let style = {};\n    if (cell.style) {\n        style = data.styles[cell.style];\n    }\n    const format = extractFormat(cell, data);\n    const styles = {\n        font: {\n            size: style?.fontSize || DEFAULT_FONT_SIZE,\n            color: { rgb: style?.textColor ? style.textColor : \"000000\" },\n            family: 2,\n            name: \"Arial\",\n        },\n        fill: style?.fillColor\n            ? {\n                fgColor: { rgb: style.fillColor },\n            }\n            : { reservedAttribute: \"none\" },\n        numFmt: format ? { format: format, id: 0 /* id not used for export */ } : undefined,\n        border: cell.border || 0,\n        alignment: {\n            horizontal: style.align,\n            vertical: style.verticalAlign\n                ? V_ALIGNMENT_EXPORT_CONVERSION_MAP[style.verticalAlign]\n                : undefined,\n            wrapText: style.wrapping === \"wrap\" || undefined,\n        },\n    };\n    styles.font[\"strike\"] = !!style?.strikethrough || undefined;\n    styles.font[\"underline\"] = !!style?.underline || undefined;\n    styles.font[\"bold\"] = !!style?.bold || undefined;\n    styles.font[\"italic\"] = !!style?.italic || undefined;\n    return styles;\n}\nfunction extractFormat(cell, data) {\n    if (cell.format) {\n        return data.formats[cell.format];\n    }\n    return undefined;\n}\nfunction normalizeStyle(construct, styles) {\n    // Normalize this\n    const numFmtId = convertFormat(styles[\"numFmt\"], construct.numFmts);\n    const style = {\n        fontId: pushElement(styles.font, construct.fonts),\n        fillId: pushElement(styles.fill, construct.fills),\n        borderId: styles.border,\n        numFmtId,\n        alignment: {\n            vertical: styles.alignment.vertical,\n            horizontal: styles.alignment.horizontal,\n            wrapText: styles.alignment.wrapText,\n        },\n    };\n    return pushElement(style, construct.styles);\n}\nfunction convertFormat(format, numFmtStructure) {\n    if (!format) {\n        return 0;\n    }\n    let formatId = XLSX_FORMAT_MAP[format.format];\n    if (!formatId) {\n        formatId = pushElement(format, numFmtStructure) + FIRST_NUMFMT_ID;\n    }\n    return formatId;\n}\n/**\n * Add a relation to the given file and return its id.\n */\nfunction addRelsToFile(relsFiles, path, rel) {\n    let relsFile = relsFiles.find((file) => file.path === path);\n    // the id is a one-based int casted as string\n    let id;\n    if (!relsFile) {\n        id = \"rId1\";\n        relsFiles.push({ path, rels: [{ ...rel, id }] });\n    }\n    else {\n        id = `rId${(relsFile.rels.length + 1).toString()}`;\n        relsFile.rels.push({\n            ...rel,\n            id,\n        });\n    }\n    return id;\n}\nfunction pushElement(property, propertyList) {\n    let len = propertyList.length;\n    const operator = typeof property === \"object\" ? deepEquals : (a, b) => a === b;\n    for (let i = 0; i < len; i++) {\n        if (operator(property, propertyList[i])) {\n            return i;\n        }\n    }\n    propertyList[propertyList.length] = property;\n    return propertyList.length - 1;\n}\nconst chartIds = [];\n/**\n * Convert a chart o-spreadsheet id to a xlsx id which\n * are unsigned integers (starting from 1).\n */\nfunction convertChartId(chartId) {\n    const xlsxId = chartIds.findIndex((id) => id === chartId);\n    if (xlsxId === -1) {\n        chartIds.push(chartId);\n        return chartIds.length;\n    }\n    return xlsxId + 1;\n}\nconst imageIds = [];\n/**\n * Convert a image o-spreadsheet id to a xlsx id which\n * are unsigned integers (starting from 1).\n */\nfunction convertImageId(imageId) {\n    const xlsxId = imageIds.findIndex((id) => id === imageId);\n    if (xlsxId === -1) {\n        imageIds.push(imageId);\n        return imageIds.length;\n    }\n    return xlsxId + 1;\n}\n/**\n * Convert a value expressed in dot to EMU.\n * EMU = English Metrical Unit\n * There are 914400 EMU per inch.\n *\n * /!\\ A value expressed in EMU cannot be fractional.\n * See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement\n */\nfunction convertDotValueToEMU(value) {\n    const DPI = 96;\n    return Math.round((value * 914400) / DPI);\n}\nfunction getRangeSize(reference, defaultSheetIndex, data) {\n    let xc = reference;\n    let sheetName = undefined;\n    ({ xc, sheetName } = splitReference(reference));\n    let rangeSheetIndex;\n    if (sheetName) {\n        const index = data.sheets.findIndex((sheet) => sheet.name === sheetName);\n        if (index < 0) {\n            throw new Error(\"Unable to find a sheet with the name \" + sheetName);\n        }\n        rangeSheetIndex = index;\n    }\n    else {\n        rangeSheetIndex = Number(defaultSheetIndex);\n    }\n    const zone = toUnboundedZone(xc);\n    if (zone.right === undefined) {\n        zone.right = data.sheets[rangeSheetIndex].colNumber;\n    }\n    if (zone.bottom === undefined) {\n        zone.bottom = data.sheets[rangeSheetIndex].rowNumber;\n    }\n    return (zone.right - zone.left + 1) * (zone.bottom - zone.top + 1);\n}\nfunction convertEMUToDotValue(value) {\n    const DPI = 96;\n    return Math.round((value * DPI) / 914400);\n}\n/**\n * Get the position of the start of a column in Excel (in px).\n */\nfunction getColPosition(colIndex, sheetData) {\n    let position = 0;\n    for (let i = 0; i < colIndex; i++) {\n        const colAtIndex = sheetData.cols.find((col) => i >= col.min && i <= col.max);\n        if (colAtIndex?.width) {\n            position += colAtIndex.width;\n        }\n        else if (sheetData.sheetFormat?.defaultColWidth) {\n            position += sheetData.sheetFormat.defaultColWidth;\n        }\n        else {\n            position += EXCEL_DEFAULT_COL_WIDTH;\n        }\n    }\n    return position / WIDTH_FACTOR;\n}\n/**\n * Get the position of the start of a row in Excel (in px).\n */\nfunction getRowPosition(rowIndex, sheetData) {\n    let position = 0;\n    for (let i = 0; i < rowIndex; i++) {\n        const rowAtIndex = sheetData.rows[i];\n        if (rowAtIndex?.height) {\n            position += rowAtIndex.height;\n        }\n        else if (sheetData.sheetFormat?.defaultRowHeight) {\n            position += sheetData.sheetFormat.defaultRowHeight;\n        }\n        else {\n            position += EXCEL_DEFAULT_ROW_HEIGHT;\n        }\n    }\n    return position / HEIGHT_FACTOR;\n}\n\nfunction convertFigures(sheetData) {\n    let id = 1;\n    return sheetData.figures\n        .map((figure) => convertFigure(figure, (id++).toString(), sheetData))\n        .filter(isDefined);\n}\nfunction convertFigure(figure, id, sheetData) {\n    let x1, y1;\n    let height, width;\n    if (figure.anchors.length === 1) {\n        // one cell anchor\n        ({ x: x1, y: y1 } = getPositionFromAnchor(figure.anchors[0], sheetData));\n        width = convertEMUToDotValue(figure.figureSize.cx);\n        height = convertEMUToDotValue(figure.figureSize.cy);\n    }\n    else {\n        ({ x: x1, y: y1 } = getPositionFromAnchor(figure.anchors[0], sheetData));\n        const { x: x2, y: y2 } = getPositionFromAnchor(figure.anchors[1], sheetData);\n        width = x2 - x1;\n        height = y2 - y1;\n    }\n    const figureData = { id, x: x1, y: y1 };\n    if (isChartData(figure.data)) {\n        return {\n            ...figureData,\n            width,\n            height,\n            tag: \"chart\",\n            data: convertChartData(figure.data),\n        };\n    }\n    else if (isImageData(figure.data)) {\n        return {\n            ...figureData,\n            width: convertEMUToDotValue(figure.data.size.cx),\n            height: convertEMUToDotValue(figure.data.size.cy),\n            tag: \"image\",\n            data: {\n                path: figure.data.imageSrc,\n                mimetype: figure.data.mimetype,\n            },\n        };\n    }\n    return undefined;\n}\nfunction isChartData(data) {\n    return \"dataSets\" in data;\n}\nfunction isImageData(data) {\n    return \"imageSrc\" in data;\n}\nfunction convertChartData(chartData) {\n    const dataSetsHaveTitle = chartData.dataSets.some((ds) => \"reference\" in (ds.label ?? {}));\n    const labelRange = chartData.labelRange\n        ? convertExcelRangeToSheetXC(chartData.labelRange, dataSetsHaveTitle)\n        : undefined;\n    const dataSets = chartData.dataSets.map((data) => {\n        let label = undefined;\n        if (data.label && \"text\" in data.label) {\n            label = data.label.text;\n        }\n        return {\n            dataRange: convertExcelRangeToSheetXC(data.range, dataSetsHaveTitle),\n            label,\n            backgroundColor: data.backgroundColor,\n        };\n    });\n    // For doughnut charts, in chartJS first dataset = outer dataset, in excel first dataset = inner dataset\n    if (chartData.type === \"pie\") {\n        dataSets.reverse();\n    }\n    return {\n        dataSets,\n        dataSetsHaveTitle,\n        labelRange,\n        title: chartData.title ?? { text: \"\" },\n        type: chartData.type,\n        background: convertColor({ rgb: chartData.backgroundColor }) || \"#FFFFFF\",\n        legendPosition: chartData.legendPosition,\n        stacked: chartData.stacked || false,\n        aggregated: false,\n        cumulative: chartData.cumulative || false,\n        labelsAsText: false,\n    };\n}\nfunction convertExcelRangeToSheetXC(range, dataSetsHaveTitle) {\n    let { sheetName, xc } = splitReference(range);\n    let zone = toUnboundedZone(xc);\n    if (dataSetsHaveTitle && zone.bottom !== undefined && zone.right !== undefined) {\n        const height = zone.bottom - zone.top + 1;\n        const width = zone.right - zone.left + 1;\n        if (height === 1) {\n            zone = { ...zone, left: zone.left - 1 };\n        }\n        else if (width === 1) {\n            zone = { ...zone, top: zone.top - 1 };\n        }\n    }\n    const dataXC = zoneToXc(zone);\n    return getFullReference(sheetName, dataXC);\n}\nfunction getPositionFromAnchor(anchor, sheetData) {\n    return {\n        x: getColPosition(anchor.col, sheetData) + convertEMUToDotValue(anchor.colOffset),\n        y: getRowPosition(anchor.row, sheetData) + convertEMUToDotValue(anchor.rowOffset),\n    };\n}\n\n/**\n * Match external reference (ex. '[1]Sheet 3'!$B$4)\n *\n * First match group is the external reference id\n * Second match group is the sheet id\n * Third match group is the reference of the cell\n */\nconst externalReferenceRegex = new RegExp(/'?\\[([0-9]*)\\](.*)'?!(\\$?[a-zA-Z]*\\$?[0-9]*)/g);\nconst subtotalRegex = new RegExp(/SUBTOTAL\\(([0-9]*),/g);\nconst cellRegex = new RegExp(cellReference.source, \"ig\");\nfunction convertFormulasContent(sheet, data) {\n    const sfMap = getSharedFormulasMap(sheet);\n    for (let cell of sheet.rows.map((row) => row.cells).flat()) {\n        if (cell?.formula) {\n            cell.formula.content =\n                cell.formula.sharedIndex !== undefined && !cell.formula.content\n                    ? \"=\" + adaptFormula(cell.xc, sfMap[cell.formula.sharedIndex])\n                    : \"=\" + cell.formula.content;\n            cell.formula.content = convertFormula(cell.formula.content, data);\n        }\n    }\n}\nfunction getSharedFormulasMap(sheet) {\n    const formulas = {};\n    for (let row of sheet.rows) {\n        for (let cell of row.cells) {\n            if (cell.formula && cell.formula.sharedIndex !== undefined && cell.formula.content) {\n                formulas[cell.formula.sharedIndex] = { refCellXc: cell.xc, formula: cell.formula.content };\n            }\n        }\n    }\n    return formulas;\n}\n/**\n * Convert an XLSX formula into something we can evaluate.\n * - remove _xlfn. flags before function names\n * - convert the SUBTOTAL(index, formula) function to the function given by its index\n * - change #REF! into #REF\n * - convert external references into their value\n */\nfunction convertFormula(formula, data) {\n    formula = formula.replace(\"_xlfn.\", \"\");\n    formula = formula.replace(/#REF!/g, \"#REF\");\n    // SUBOTOTAL function, eg. =SUBTOTAL(3, {formula})\n    formula = formula.replace(subtotalRegex, (match, functionId) => {\n        const convertedFunction = SUBTOTAL_FUNCTION_CONVERSION_MAP[functionId];\n        return convertedFunction ? convertedFunction + \"(\" : match;\n    });\n    // External references, eg. ='[1]Sheet 3'!$B$4\n    formula = formula.replace(externalReferenceRegex, (match, externalRefId, sheetName, cellRef) => {\n        externalRefId = Number(externalRefId) - 1;\n        cellRef = cellRef.replace(/\\$/g, \"\");\n        const sheetIndex = data.externalBooks[externalRefId].sheetNames.findIndex((name) => name === sheetName);\n        if (sheetIndex === -1) {\n            return match;\n        }\n        const externalDataset = data.externalBooks[externalRefId].datasets.find((dataset) => dataset.sheetId === sheetIndex)?.data;\n        if (!externalDataset) {\n            return match;\n        }\n        const datasetValue = externalDataset && externalDataset[cellRef];\n        const convertedValue = Number(datasetValue) ? datasetValue : `\"${datasetValue}\"`;\n        return convertedValue || match;\n    });\n    return formula;\n}\n/**\n * Transform a shared formula for the given target.\n *\n * This will compute the offset between the original cell of the shared formula and the target cell,\n * then apply this offset to all the ranges in the formula (taking fixed references into account)\n */\nfunction adaptFormula(targetCell, sf) {\n    const refPosition = toCartesian(sf.refCellXc);\n    let newFormula = sf.formula.slice();\n    let match;\n    do {\n        match = cellRegex.exec(newFormula);\n        if (match) {\n            const formulaPosition = toCartesian(match[0].replace(\"$\", \"\"));\n            const targetPosition = toCartesian(targetCell);\n            const rangePart = {\n                colFixed: match[0].startsWith(\"$\"),\n                rowFixed: match[0].includes(\"$\", 1),\n            };\n            const offset = {\n                col: targetPosition.col - refPosition.col,\n                row: targetPosition.row - refPosition.row,\n            };\n            const offsettedPosition = {\n                col: rangePart.colFixed ? formulaPosition.col : formulaPosition.col + offset.col,\n                row: rangePart.rowFixed ? formulaPosition.row : formulaPosition.row + offset.row,\n            };\n            newFormula =\n                newFormula.slice(0, match.index) +\n                    toXC(offsettedPosition.col, offsettedPosition.row, rangePart) +\n                    newFormula.slice(match.index + match[0].length);\n        }\n    } while (match);\n    return newFormula;\n}\n\nfunction convertSheets(data, warningManager) {\n    return data.sheets.map((sheet) => {\n        convertFormulasContent(sheet, data);\n        const sheetDims = getSheetDims(sheet);\n        const sheetOptions = sheet.sheetViews[0];\n        const rowHeaderGroups = convertHeaderGroup(sheet, \"ROW\", sheetDims[1]);\n        const colHeaderGroups = convertHeaderGroup(sheet, \"COL\", sheetDims[0]);\n        return {\n            id: sheet.sheetName,\n            areGridLinesVisible: sheetOptions ? sheetOptions.showGridLines : true,\n            name: sheet.sheetName,\n            colNumber: sheetDims[0],\n            rowNumber: sheetDims[1],\n            ...convertCells(sheet, data, sheetDims, warningManager),\n            merges: sheet.merges,\n            cols: convertCols(sheet, sheetDims[0], colHeaderGroups),\n            rows: convertRows(sheet, sheetDims[1], rowHeaderGroups),\n            conditionalFormats: convertConditionalFormats(sheet.cfs, data.dxfs, warningManager),\n            figures: convertFigures(sheet),\n            isVisible: sheet.isVisible,\n            panes: sheetOptions\n                ? { xSplit: sheetOptions.pane.xSplit, ySplit: sheetOptions.pane.ySplit }\n                : { xSplit: 0, ySplit: 0 },\n            tables: [],\n            headerGroups: { COL: colHeaderGroups, ROW: rowHeaderGroups },\n            color: convertColor(sheet.sheetProperties?.tabColor),\n        };\n    });\n}\nfunction convertCols(sheet, numberOfCols, headerGroups) {\n    const cols = {};\n    // Excel begins indexes at 1\n    for (let i = 1; i < numberOfCols + 1; i++) {\n        const col = sheet.cols.find((col) => col.min <= i && i <= col.max);\n        let colSize;\n        if (col && col.width)\n            colSize = col.width;\n        else if (sheet.sheetFormat?.defaultColWidth)\n            colSize = sheet.sheetFormat.defaultColWidth;\n        else\n            colSize = EXCEL_DEFAULT_COL_WIDTH;\n        // In xlsx there is no difference between hidden columns and columns inside a folded group.\n        // But in o-spreadsheet folded columns are not considered hidden.\n        const colIndex = i - 1;\n        const isColFolded = headerGroups.some((group) => group.isFolded && group.start <= colIndex && colIndex <= group.end);\n        cols[colIndex] = {\n            size: convertWidthFromExcel(colSize),\n            isHidden: !isColFolded && col?.hidden,\n        };\n    }\n    return cols;\n}\nfunction convertRows(sheet, numberOfRows, headerGroups) {\n    const rows = {};\n    // Excel begins indexes at 1\n    for (let i = 1; i < numberOfRows + 1; i++) {\n        const row = sheet.rows.find((row) => row.index === i);\n        let rowSize;\n        if (row && row.height)\n            rowSize = row.height;\n        else if (sheet.sheetFormat?.defaultRowHeight)\n            rowSize = sheet.sheetFormat.defaultRowHeight;\n        else\n            rowSize = EXCEL_DEFAULT_ROW_HEIGHT;\n        // In xlsx there is no difference between hidden rows and rows inside a folded group.\n        // But in o-spreadsheet folded rows are not considered hidden.\n        const rowIndex = i - 1;\n        const isRowFolded = headerGroups.some((group) => group.isFolded && group.start <= rowIndex && rowIndex <= group.end);\n        rows[rowIndex] = {\n            size: convertHeightFromExcel(rowSize),\n            isHidden: !isRowFolded && row?.hidden,\n        };\n    }\n    return rows;\n}\n/** Remove newlines (\\n) in shared strings, We do not support them */\nfunction convertSharedStrings(xlsxSharedStrings) {\n    return xlsxSharedStrings.map((str) => str.replace(/\\n/g, \"\"));\n}\nfunction convertCells(sheet, data, sheetDims, warningManager) {\n    const cells = {};\n    const styles = {};\n    const formats = {};\n    const borders = {};\n    const sharedStrings = convertSharedStrings(data.sharedStrings);\n    const hyperlinkMap = sheet.hyperlinks.reduce((map, link) => {\n        map[link.xc] = link;\n        return map;\n    }, {});\n    for (let row of sheet.rows) {\n        for (let cell of row.cells) {\n            cells[cell.xc] = {\n                content: getCellValue(cell, hyperlinkMap, sharedStrings, warningManager),\n            };\n            if (cell.styleIndex) {\n                // + 1 : our indexes for normalized values begin at 1 and not 0\n                styles[cell.xc] = cell.styleIndex + 1;\n                formats[cell.xc] = data.styles[cell.styleIndex].numFmtId + 1;\n                borders[cell.xc] = data.styles[cell.styleIndex].borderId + 1;\n            }\n        }\n    }\n    // Apply row style\n    for (let row of sheet.rows.filter((row) => row.styleIndex)) {\n        for (let colIndex = 1; colIndex <= sheetDims[0]; colIndex++) {\n            const xc = toXC(colIndex - 1, row.index - 1); // Excel indexes start at 1\n            let cell = cells[xc];\n            if (!cell) {\n                cell = {};\n                cells[xc] = cell;\n            }\n            styles[xc] ??= row.styleIndex + 1;\n            borders[xc] ??= data.styles[row.styleIndex].borderId + 1;\n            formats[xc] ??= data.styles[row.styleIndex].numFmtId + 1;\n        }\n    }\n    // Apply col style\n    for (let col of sheet.cols.filter((col) => col.styleIndex)) {\n        for (let colIndex = col.min; colIndex <= Math.min(col.max, sheetDims[0]); colIndex++) {\n            for (let rowIndex = 1; rowIndex <= sheetDims[1]; rowIndex++) {\n                const xc = toXC(colIndex - 1, rowIndex - 1); // Excel indexes start at 1\n                let cell = cells[xc];\n                if (!cell) {\n                    cell = {};\n                    cells[xc] = cell;\n                }\n                styles[xc] ??= col.styleIndex + 1;\n                borders[xc] ??= data.styles[col.styleIndex].borderId + 1;\n                formats[xc] ??= data.styles[col.styleIndex].numFmtId + 1;\n            }\n        }\n    }\n    return { cells, styles, formats, borders };\n}\nfunction getCellValue(cell, hyperLinksMap, sharedStrings, warningManager) {\n    let cellValue;\n    switch (cell.type) {\n        case \"sharedString\":\n            const ssIndex = parseInt(cell.value, 10);\n            cellValue = sharedStrings[ssIndex];\n            break;\n        case \"boolean\":\n            cellValue = Number(cell.value) ? \"TRUE\" : \"FALSE\";\n            break;\n        case \"date\": // I'm not sure where this is used rather than a number with a format\n        case \"error\": // I don't think Excel really uses this\n        case \"inlineStr\":\n        case \"number\":\n        case \"str\":\n            cellValue = cell.value;\n            break;\n    }\n    if (cellValue && hyperLinksMap[cell.xc]) {\n        cellValue = convertHyperlink(hyperLinksMap[cell.xc], cellValue, warningManager);\n    }\n    if (cell.formula) {\n        cellValue = cell.formula.content;\n    }\n    return cellValue;\n}\nfunction convertHyperlink(link, cellValue, warningManager) {\n    const label = link.display || cellValue;\n    if (!link.relTarget && !link.location) {\n        warningManager.generateNotSupportedWarning(WarningTypes.BadlyFormattedHyperlink);\n    }\n    const url = link.relTarget\n        ? link.relTarget\n        : buildSheetLink(splitReference(link.location).sheetName);\n    return markdownLink(label, url);\n}\nfunction getSheetDims(sheet) {\n    const dims = [0, 0];\n    for (let row of sheet.rows) {\n        dims[0] = Math.max(dims[0], largeMax(row.cells.map((cell) => toCartesian(cell.xc).col)));\n        dims[1] = Math.max(dims[1], row.index);\n    }\n    dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);\n    dims[1] = Math.max(dims[1], EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS);\n    return dims;\n}\n/**\n * Get the header groups from the XLS file.\n *\n * See ASCII art in HeaderGroupingPlugin.exportForExcel() for details on how the groups are defined in the xlsx.\n */\nfunction convertHeaderGroup(sheet, dim, numberOfHeaders) {\n    const outlineProperties = sheet?.sheetProperties?.outlinePr;\n    const headerGroups = [];\n    let currentLayer = 0;\n    for (let i = 0; i < numberOfHeaders; i++) {\n        const header = getHeader(sheet, dim, i);\n        const headerLayer = header?.outlineLevel || 0;\n        if (headerLayer > currentLayer) {\n            // Whether the flag indicating if the group is collapsed is on the header before or after the group. Default is after.\n            const collapseFlagAfter = (dim === \"ROW\" ? outlineProperties?.summaryBelow : outlineProperties?.summaryRight) ?? true;\n            const group = computeHeaderGroup(sheet, dim, i, collapseFlagAfter);\n            if (group) {\n                headerGroups.push(group);\n            }\n        }\n        currentLayer = headerLayer;\n    }\n    return headerGroups;\n}\nfunction computeHeaderGroup(sheet, dim, startIndex, collapseFlagAfter) {\n    const startHeader = getHeader(sheet, dim, startIndex);\n    const startLayer = startHeader?.outlineLevel;\n    if (!startLayer || !startLayer) {\n        return undefined;\n    }\n    let currentLayer = startLayer;\n    let currentIndex = startIndex;\n    let currentHeader = startHeader;\n    while (currentHeader && currentLayer >= startLayer) {\n        currentIndex++;\n        currentHeader = getHeader(sheet, dim, currentIndex);\n        currentLayer = currentHeader?.outlineLevel || 0;\n    }\n    const start = startIndex;\n    const end = currentIndex - 1;\n    const collapseFlagHeader = collapseFlagAfter\n        ? getHeader(sheet, dim, end + 1)\n        : getHeader(sheet, dim, start - 1);\n    const isFolded = collapseFlagHeader?.collapsed || false;\n    return { start: start - 1, end: end - 1, isFolded }; // -1 because indices start at 1 in excel and 0 in o-spreadsheet\n}\nfunction getHeader(sheet, dim, index) {\n    return \"COL\" === dim\n        ? sheet.cols.find((col) => col.min <= index && index <= col.max)\n        : sheet.rows.find((row) => row.index === index);\n}\n\nconst TABLE_STYLE_CATEGORIES = {\n    light: _t(\"Light\"),\n    medium: _t(\"Medium\"),\n    dark: _t(\"Dark\"),\n    custom: _t(\"Custom\"),\n};\nconst DEFAULT_TABLE_CONFIG = {\n    hasFilters: true,\n    totalRow: false,\n    firstColumn: false,\n    lastColumn: false,\n    numberOfHeaders: 1,\n    bandedRows: true,\n    bandedColumns: false,\n    automaticAutofill: true,\n    styleId: \"TableStyleMedium2\",\n};\nfunction generateTableColorSet(name, highlightColor) {\n    return {\n        coloredText: darkenColor(highlightColor, 0.3),\n        light: lightenColor(highlightColor, 0.8),\n        medium: lightenColor(highlightColor, 0.6),\n        dark: darkenColor(highlightColor, 0.3),\n        mediumBorder: lightenColor(highlightColor, 0.45),\n        highlight: highlightColor,\n        name,\n    };\n}\nconst COLOR_SETS = {\n    black: {\n        name: _t(\"Black\"),\n        coloredText: \"#000000\",\n        light: \"#D9D9D9\",\n        medium: \"#A6A6A6\",\n        dark: \"#404040\",\n        mediumBorder: \"#000000\",\n        highlight: \"#000000\",\n    },\n    lightBlue: generateTableColorSet(_t(\"Light blue\"), \"#346B90\"),\n    red: generateTableColorSet(_t(\"Red\"), \"#C53628\"),\n    lightGreen: generateTableColorSet(_t(\"Light green\"), \"#748747\"),\n    purple: generateTableColorSet(_t(\"Purple\"), \"#6C4E65\"),\n    gray: {\n        name: _t(\"Gray\"),\n        coloredText: \"#666666\",\n        light: \"#EEEEEE\",\n        medium: \"#DDDDDD\",\n        dark: \"#767676\",\n        mediumBorder: \"#D0D0D0\",\n        highlight: \"#A9A9A9\",\n    },\n    orange: generateTableColorSet(_t(\"Orange\"), \"#C37034\"),\n};\nconst DARK_COLOR_SETS = {\n    black: COLOR_SETS.black,\n    orangeBlue: { ...COLOR_SETS.lightBlue, highlight: COLOR_SETS.orange.highlight },\n    purpleGreen: { ...COLOR_SETS.lightGreen, highlight: COLOR_SETS.purple.highlight },\n    redBlue: { ...COLOR_SETS.lightBlue, highlight: COLOR_SETS.red.highlight },\n};\nconst lightColoredText = (colorSet) => ({\n    category: \"light\",\n    templateName: \"lightColoredText\",\n    primaryColor: colorSet.highlight,\n    wholeTable: {\n        style: { textColor: colorSet.coloredText },\n        border: {\n            top: { color: colorSet.highlight, style: \"thin\" },\n            bottom: { color: colorSet.highlight, style: \"thin\" },\n        },\n    },\n    headerRow: { border: { bottom: { color: colorSet.highlight, style: \"thin\" } } },\n    totalRow: { border: { top: { color: colorSet.highlight, style: \"thin\" } } },\n    firstRowStripe: { style: { fillColor: colorSet.light } },\n});\nconst lightWithHeader = (colorSet) => ({\n    category: \"light\",\n    templateName: \"lightWithHeader\",\n    primaryColor: colorSet.highlight,\n    wholeTable: {\n        border: {\n            top: { color: colorSet.highlight, style: \"thin\" },\n            bottom: { color: colorSet.highlight, style: \"thin\" },\n            left: { color: colorSet.highlight, style: \"thin\" },\n            right: { color: colorSet.highlight, style: \"thin\" },\n        },\n    },\n    headerRow: {\n        style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" },\n        border: { bottom: { color: colorSet.highlight, style: \"thin\" } },\n    },\n    totalRow: { border: { top: { color: colorSet.highlight, style: \"medium\" } } }, // @compatibility: should be double line\n    firstRowStripe: { border: { bottom: { color: colorSet.highlight, style: \"thin\" } } },\n    secondRowStripe: { border: { bottom: { color: colorSet.highlight, style: \"thin\" } } },\n});\nconst lightAllBorders = (colorSet) => ({\n    category: \"light\",\n    templateName: \"lightAllBorders\",\n    primaryColor: colorSet.highlight,\n    wholeTable: {\n        border: {\n            top: { color: colorSet.highlight, style: \"thin\" },\n            bottom: { color: colorSet.highlight, style: \"thin\" },\n            left: { color: colorSet.highlight, style: \"thin\" },\n            right: { color: colorSet.highlight, style: \"thin\" },\n            horizontal: { color: colorSet.highlight, style: \"thin\" },\n            vertical: { color: colorSet.highlight, style: \"thin\" },\n        },\n    },\n    headerRow: { border: { bottom: { color: colorSet.highlight, style: \"medium\" } } },\n    totalRow: { border: { top: { color: colorSet.highlight, style: \"medium\" } } }, // @compatibility: should be double line\n    firstRowStripe: { style: { fillColor: colorSet.light } },\n    firstColumnStripe: { style: { fillColor: colorSet.light } },\n});\nconst mediumBandedBorders = (colorSet) => ({\n    category: \"medium\",\n    templateName: \"mediumBandedBorders\",\n    primaryColor: colorSet.highlight,\n    wholeTable: {\n        border: {\n            top: { color: colorSet.mediumBorder, style: \"thin\" },\n            bottom: { color: colorSet.mediumBorder, style: \"thin\" },\n            left: { color: colorSet.mediumBorder, style: \"thin\" },\n            right: { color: colorSet.mediumBorder, style: \"thin\" },\n            horizontal: { color: colorSet.mediumBorder, style: \"thin\" },\n        },\n    },\n    headerRow: {\n        style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" },\n    },\n    totalRow: { border: { top: { color: colorSet.highlight, style: \"medium\" } } }, // @compatibility: should be double line\n    firstRowStripe: { style: { fillColor: colorSet.light } },\n    firstColumnStripe: { style: { fillColor: colorSet.light } },\n});\nconst mediumWhiteBorders = (colorSet) => ({\n    category: \"medium\",\n    templateName: \"mediumWhiteBorders\",\n    primaryColor: colorSet.highlight,\n    wholeTable: {\n        border: {\n            horizontal: { color: \"#FFFFFF\", style: \"thin\" },\n            vertical: { color: \"#FFFFFF\", style: \"thin\" },\n        },\n        style: { fillColor: colorSet.light },\n    },\n    headerRow: {\n        border: { bottom: { color: \"#FFFFFF\", style: \"thick\" } },\n        style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" },\n    },\n    totalRow: {\n        border: { top: { color: \"#FFFFFF\", style: \"thick\" } },\n        style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" },\n    },\n    firstColumn: { style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" } },\n    lastColumn: { style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" } },\n    firstRowStripe: { style: { fillColor: colorSet.medium } },\n    firstColumnStripe: { style: { fillColor: colorSet.medium } },\n});\nconst mediumMinimalBorders = (colorSet) => ({\n    category: \"medium\",\n    templateName: \"mediumMinimalBorders\",\n    primaryColor: colorSet.highlight,\n    wholeTable: {\n        border: {\n            top: { color: \"#000000\", style: \"medium\" },\n            bottom: { color: \"#000000\", style: \"medium\" },\n        },\n    },\n    totalRow: { border: { top: { color: \"#000000\", style: \"medium\" } } }, // @compatibility: should be double line\n    headerRow: {\n        style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" },\n        border: { bottom: { color: \"#000000\", style: \"medium\" } },\n    },\n    firstColumn: { style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" } },\n    lastColumn: { style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" } },\n    firstRowStripe: { style: { fillColor: COLOR_SETS.black.light } },\n    firstColumnStripe: { style: { fillColor: COLOR_SETS.black.light } },\n});\nconst mediumAllBorders = (colorSet) => ({\n    category: \"medium\",\n    templateName: \"mediumAllBorders\",\n    primaryColor: colorSet.highlight,\n    wholeTable: {\n        border: {\n            top: { color: colorSet.mediumBorder, style: \"thin\" },\n            bottom: { color: colorSet.mediumBorder, style: \"thin\" },\n            left: { color: colorSet.mediumBorder, style: \"thin\" },\n            right: { color: colorSet.mediumBorder, style: \"thin\" },\n            horizontal: { color: colorSet.mediumBorder, style: \"thin\" },\n            vertical: { color: colorSet.mediumBorder, style: \"thin\" },\n        },\n        style: { fillColor: colorSet.light },\n    },\n    totalRow: { border: { top: { color: colorSet.highlight, style: \"medium\" } } }, // @compatibility: should be double line\n    firstRowStripe: { style: { fillColor: colorSet.medium } },\n    firstColumnStripe: { style: { fillColor: colorSet.medium } },\n});\nconst dark = (colorSet) => ({\n    category: \"dark\",\n    templateName: \"dark\",\n    primaryColor: colorSet.highlight,\n    wholeTable: { style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" } },\n    totalRow: {\n        style: { fillColor: colorSet.dark, textColor: \"#FFFFFF\" },\n        border: { top: { color: \"#FFFFFF\", style: \"thick\" } },\n    },\n    headerRow: {\n        style: { fillColor: \"#000000\" },\n        border: { bottom: { color: \"#FFFFFF\", style: \"thick\" } },\n    },\n    firstColumn: {\n        style: { fillColor: colorSet.dark },\n        border: { right: { color: \"#FFFFFF\", style: \"thick\" } },\n    },\n    lastColumn: {\n        style: { fillColor: colorSet.dark },\n        border: { left: { color: \"#FFFFFF\", style: \"thick\" } },\n    },\n    firstRowStripe: { style: { fillColor: colorSet.dark } },\n    firstColumnStripe: { style: { fillColor: colorSet.dark } },\n});\nconst darkNoBorders = (colorSet) => ({\n    category: \"dark\",\n    templateName: \"darkNoBorders\",\n    primaryColor: colorSet.highlight,\n    wholeTable: { style: { fillColor: colorSet.light } },\n    totalRow: { border: { top: { color: \"#000000\", style: \"medium\" } } }, // @compatibility: should be double line\n    headerRow: { style: { fillColor: colorSet.highlight, textColor: \"#FFFFFF\" } },\n    firstRowStripe: { style: { fillColor: colorSet.medium } },\n    firstColumnStripe: { style: { fillColor: colorSet.medium } },\n});\nconst darkTemplateInBlack = dark(COLOR_SETS.black);\ndarkTemplateInBlack.wholeTable.style.fillColor = \"#737373\";\nconst mediumMinimalBordersInBlack = mediumMinimalBorders(COLOR_SETS.black);\nmediumMinimalBordersInBlack.wholeTable.border = {\n    ...mediumMinimalBordersInBlack.wholeTable.border,\n    left: { color: \"#000000\", style: \"thin\" },\n    right: { color: \"#000000\", style: \"thin\" },\n    horizontal: { color: \"#000000\", style: \"thin\" },\n    vertical: { color: \"#000000\", style: \"thin\" },\n};\nfunction buildPreset(name, template, colorSet) {\n    return { ...template(colorSet), displayName: `${colorSet.name}, ${name}` };\n}\nconst TABLE_PRESETS = {\n    None: { category: \"light\", templateName: \"none\", primaryColor: \"\", displayName: \"none\" },\n    TableStyleLight1: buildPreset(\"TableStyleLight1\", lightColoredText, COLOR_SETS.black),\n    TableStyleLight2: buildPreset(\"TableStyleLight2\", lightColoredText, COLOR_SETS.lightBlue),\n    TableStyleLight3: buildPreset(\"TableStyleLight3\", lightColoredText, COLOR_SETS.red),\n    TableStyleLight4: buildPreset(\"TableStyleLight4\", lightColoredText, COLOR_SETS.lightGreen),\n    TableStyleLight5: buildPreset(\"TableStyleLight5\", lightColoredText, COLOR_SETS.purple),\n    TableStyleLight6: buildPreset(\"TableStyleLight6\", lightColoredText, COLOR_SETS.gray),\n    TableStyleLight7: buildPreset(\"TableStyleLight7\", lightColoredText, COLOR_SETS.orange),\n    TableStyleLight8: buildPreset(\"TableStyleLight8\", lightWithHeader, COLOR_SETS.black),\n    TableStyleLight9: buildPreset(\"TableStyleLight9\", lightWithHeader, COLOR_SETS.lightBlue),\n    TableStyleLight10: buildPreset(\"TableStyleLight10\", lightWithHeader, COLOR_SETS.red),\n    TableStyleLight11: buildPreset(\"TableStyleLight11\", lightWithHeader, COLOR_SETS.lightGreen),\n    TableStyleLight12: buildPreset(\"TableStyleLight12\", lightWithHeader, COLOR_SETS.purple),\n    TableStyleLight13: buildPreset(\"TableStyleLight13\", lightWithHeader, COLOR_SETS.gray),\n    TableStyleLight14: buildPreset(\"TableStyleLight14\", lightWithHeader, COLOR_SETS.orange),\n    TableStyleLight15: buildPreset(\"TableStyleLight15\", lightAllBorders, COLOR_SETS.black),\n    TableStyleLight16: buildPreset(\"TableStyleLight16\", lightAllBorders, COLOR_SETS.lightBlue),\n    TableStyleLight17: buildPreset(\"TableStyleLight17\", lightAllBorders, COLOR_SETS.red),\n    TableStyleLight18: buildPreset(\"TableStyleLight18\", lightAllBorders, COLOR_SETS.lightGreen),\n    TableStyleLight19: buildPreset(\"TableStyleLight19\", lightAllBorders, COLOR_SETS.purple),\n    TableStyleLight20: buildPreset(\"TableStyleLight20\", lightAllBorders, COLOR_SETS.gray),\n    TableStyleLight21: buildPreset(\"TableStyleLight21\", lightAllBorders, COLOR_SETS.orange),\n    TableStyleMedium1: buildPreset(\"TableStyleMedium1\", mediumBandedBorders, COLOR_SETS.black),\n    TableStyleMedium2: buildPreset(\"TableStyleMedium2\", mediumBandedBorders, COLOR_SETS.lightBlue),\n    TableStyleMedium3: buildPreset(\"TableStyleMedium3\", mediumBandedBorders, COLOR_SETS.red),\n    TableStyleMedium4: buildPreset(\"TableStyleMedium4\", mediumBandedBorders, COLOR_SETS.lightGreen),\n    TableStyleMedium5: buildPreset(\"TableStyleMedium5\", mediumBandedBorders, COLOR_SETS.purple),\n    TableStyleMedium6: buildPreset(\"TableStyleMedium6\", mediumBandedBorders, COLOR_SETS.gray),\n    TableStyleMedium7: buildPreset(\"TableStyleMedium7\", mediumBandedBorders, COLOR_SETS.orange),\n    TableStyleMedium8: buildPreset(\"TableStyleMedium8\", mediumWhiteBorders, COLOR_SETS.black),\n    TableStyleMedium9: buildPreset(\"TableStyleMedium9\", mediumWhiteBorders, COLOR_SETS.lightBlue),\n    TableStyleMedium10: buildPreset(\"TableStyleMedium10\", mediumWhiteBorders, COLOR_SETS.red),\n    TableStyleMedium11: buildPreset(\"TableStyleMedium11\", mediumWhiteBorders, COLOR_SETS.lightGreen),\n    TableStyleMedium12: buildPreset(\"TableStyleMedium12\", mediumWhiteBorders, COLOR_SETS.purple),\n    TableStyleMedium13: buildPreset(\"TableStyleMedium13\", mediumWhiteBorders, COLOR_SETS.gray),\n    TableStyleMedium14: buildPreset(\"TableStyleMedium14\", mediumWhiteBorders, COLOR_SETS.orange),\n    TableStyleMedium15: { ...mediumMinimalBordersInBlack, displayName: \"Black, TableStyleMedium15\" },\n    TableStyleMedium16: buildPreset(\"TableStyleMedium16\", mediumMinimalBorders, COLOR_SETS.lightBlue),\n    TableStyleMedium17: buildPreset(\"TableStyleMedium17\", mediumMinimalBorders, COLOR_SETS.red),\n    TableStyleMedium18: buildPreset(\"TableStyleMedium18\", mediumMinimalBorders, COLOR_SETS.lightGreen),\n    TableStyleMedium19: buildPreset(\"TableStyleMedium19\", mediumMinimalBorders, COLOR_SETS.purple),\n    TableStyleMedium20: buildPreset(\"TableStyleMedium20\", mediumMinimalBorders, COLOR_SETS.gray),\n    TableStyleMedium21: buildPreset(\"TableStyleMedium21\", mediumMinimalBorders, COLOR_SETS.orange),\n    TableStyleMedium22: buildPreset(\"TableStyleMedium22\", mediumAllBorders, COLOR_SETS.black),\n    TableStyleMedium23: buildPreset(\"TableStyleMedium23\", mediumAllBorders, COLOR_SETS.lightBlue),\n    TableStyleMedium24: buildPreset(\"TableStyleMedium24\", mediumAllBorders, COLOR_SETS.red),\n    TableStyleMedium25: buildPreset(\"TableStyleMedium25\", mediumAllBorders, COLOR_SETS.lightGreen),\n    TableStyleMedium26: buildPreset(\"TableStyleMedium26\", mediumAllBorders, COLOR_SETS.purple),\n    TableStyleMedium27: buildPreset(\"TableStyleMedium27\", mediumAllBorders, COLOR_SETS.gray),\n    TableStyleMedium28: buildPreset(\"TableStyleMedium28\", mediumAllBorders, COLOR_SETS.orange),\n    TableStyleDark1: { ...darkTemplateInBlack, displayName: \"Black, TableStyleDark1\" },\n    TableStyleDark2: buildPreset(\"TableStyleDark2\", dark, COLOR_SETS.lightBlue),\n    TableStyleDark3: buildPreset(\"TableStyleDark3\", dark, COLOR_SETS.red),\n    TableStyleDark4: buildPreset(\"TableStyleDark4\", dark, COLOR_SETS.lightGreen),\n    TableStyleDark5: buildPreset(\"TableStyleDark5\", dark, COLOR_SETS.purple),\n    TableStyleDark6: buildPreset(\"TableStyleDark6\", dark, COLOR_SETS.gray),\n    TableStyleDark7: buildPreset(\"TableStyleDark7\", dark, COLOR_SETS.orange),\n    TableStyleDark8: buildPreset(\"TableStyleDark8\", darkNoBorders, DARK_COLOR_SETS.black),\n    TableStyleDark9: buildPreset(\"TableStyleDark9\", darkNoBorders, DARK_COLOR_SETS.redBlue),\n    TableStyleDark10: buildPreset(\"TableStyleDark10\", darkNoBorders, DARK_COLOR_SETS.purpleGreen),\n    TableStyleDark11: buildPreset(\"TableStyleDark11\", darkNoBorders, DARK_COLOR_SETS.orangeBlue),\n};\nconst TABLE_STYLES_TEMPLATES = {\n    none: () => ({ category: \"none\", templateName: \"none\", primaryColor: \"\", name: \"none\" }),\n    lightColoredText: lightColoredText,\n    lightAllBorders: lightAllBorders,\n    mediumAllBorders: mediumAllBorders,\n    lightWithHeader: lightWithHeader,\n    mediumBandedBorders: mediumBandedBorders,\n    mediumMinimalBorders: mediumMinimalBorders,\n    darkNoBorders: darkNoBorders,\n    mediumWhiteBorders: mediumWhiteBorders,\n    dark: dark,\n};\nfunction buildTableStyle(name, templateName, primaryColor) {\n    const colorSet = generateTableColorSet(\"\", primaryColor);\n    return {\n        ...TABLE_STYLES_TEMPLATES[templateName](colorSet),\n        category: \"custom\",\n        displayName: name,\n    };\n}\n\n/**\n * Convert the imported XLSX tables and pivots convert the table-specific formula references into standard references.\n *\n * Change the converted data in-place.\n */\nfunction convertTables(convertedData, xlsxData) {\n    for (const xlsxSheet of xlsxData.sheets) {\n        const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName);\n        if (!sheet)\n            continue;\n        if (!sheet.tables)\n            sheet.tables = [];\n        for (const table of xlsxSheet.tables) {\n            sheet.tables.push({ range: table.ref, config: convertTableConfig(table) });\n        }\n        for (const pivotTable of xlsxSheet.pivotTables) {\n            sheet.tables.push({\n                range: pivotTable.location.ref,\n                config: convertPivotTableConfig(pivotTable),\n            });\n        }\n    }\n    convertTableFormulaReferences(convertedData.sheets, xlsxData.sheets);\n}\nfunction convertTableConfig(table) {\n    const styleId = table.style?.name || \"\";\n    return {\n        hasFilters: table.autoFilter !== undefined,\n        numberOfHeaders: table.headerRowCount,\n        totalRow: table.totalsRowCount > 0,\n        firstColumn: table.style?.showFirstColumn || false,\n        lastColumn: table.style?.showLastColumn || false,\n        bandedRows: table.style?.showRowStripes || false,\n        bandedColumns: table.style?.showColumnStripes || false,\n        styleId: TABLE_PRESETS[styleId] ? styleId : DEFAULT_TABLE_CONFIG.styleId,\n    };\n}\nfunction convertPivotTableConfig(pivotTable) {\n    return {\n        hasFilters: false,\n        numberOfHeaders: pivotTable.location.firstDataRow,\n        totalRow: pivotTable.rowGrandTotals,\n        firstColumn: true,\n        lastColumn: pivotTable.style?.showLastColumn || false,\n        bandedRows: pivotTable.style?.showRowStripes || false,\n        bandedColumns: pivotTable.style?.showColStripes || false,\n        styleId: DEFAULT_TABLE_CONFIG.styleId,\n    };\n}\n/**\n * In all the sheets, replace the table-only references in the formula cells with standard references.\n */\nfunction convertTableFormulaReferences(convertedSheets, xlsxSheets) {\n    for (let sheet of convertedSheets) {\n        const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;\n        for (let table of tables) {\n            const tabRef = table.name + \"[\";\n            for (let position of positions(toZone(table.ref))) {\n                const xc = toXC(position.col, position.row);\n                const cell = sheet.cells[xc];\n                if (cell && cell.content && cell.content.startsWith(\"=\")) {\n                    let refIndex;\n                    while ((refIndex = cell.content.indexOf(tabRef)) !== -1) {\n                        let reference = cell.content.slice(refIndex + tabRef.length);\n                        // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]\n                        let endIndex = reference.indexOf(\"]\");\n                        if (reference.startsWith(`[`)) {\n                            endIndex = reference.indexOf(\"]\", endIndex + 1);\n                            endIndex = reference.indexOf(\"]\", endIndex + 1);\n                        }\n                        reference = reference.slice(0, endIndex);\n                        const convertedRef = convertTableReference(reference, table, xc);\n                        cell.content =\n                            cell.content.slice(0, refIndex) +\n                                convertedRef +\n                                cell.content.slice(tabRef.length + refIndex + endIndex + 1);\n                    }\n                }\n            }\n        }\n    }\n}\n/**\n * Convert table-specific references in formulas into standard references.\n *\n * A reference in a table can have the form (only the part between brackets should be given to this function):\n *  - tableName[colName] : reference to the whole column \"colName\"\n *  - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName\n *\n * The available keywords are :\n * - #All : all the column (including totals)\n * - #Data : only the column data (no headers/totals)\n * - #Headers : only the header of the column\n * - #Totals : only the totals of the column\n * - #This Row : only the element in the same row as the cell\n */\nfunction convertTableReference(expr, table, cellXc) {\n    const refElements = expr.split(\",\");\n    const tableZone = toZone(table.ref);\n    const refZone = { ...tableZone };\n    let isReferencedZoneValid = true;\n    // Single column reference\n    if (refElements.length === 1) {\n        const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);\n        refZone.left = refZone.right = colRelativeIndex + tableZone.left;\n        if (table.headerRowCount) {\n            refZone.top += table.headerRowCount;\n        }\n        if (table.totalsRowCount) {\n            refZone.bottom -= 1;\n        }\n    }\n    // Other references\n    else {\n        switch (refElements[0].slice(1, refElements[0].length - 1)) {\n            case \"#All\":\n                refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;\n                refZone.bottom = tableZone.bottom;\n                break;\n            case \"#Data\":\n                refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;\n                refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;\n                break;\n            case \"#This Row\":\n                refZone.top = refZone.bottom = toCartesian(cellXc).row;\n                break;\n            case \"#Headers\":\n                refZone.top = refZone.bottom = tableZone.top;\n                if (!table.headerRowCount) {\n                    isReferencedZoneValid = false;\n                }\n                break;\n            case \"#Totals\":\n                refZone.top = refZone.bottom = tableZone.bottom;\n                if (!table.totalsRowCount) {\n                    isReferencedZoneValid = false;\n                }\n                break;\n        }\n        const colRef = refElements[1].slice(1, refElements[1].length - 1);\n        const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);\n        refZone.left = refZone.right = colRelativeIndex + tableZone.left;\n    }\n    if (!isReferencedZoneValid) {\n        return CellErrorType.InvalidReference;\n    }\n    return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);\n}\n\n// -------------------------------------\n//            XML HELPERS\n// -------------------------------------\nfunction createXMLFile(doc, path, contentType) {\n    return {\n        content: new XMLSerializer().serializeToString(doc),\n        path,\n        contentType,\n    };\n}\nfunction xmlEscape(str) {\n    return (String(str)\n        .replace(/\\&/g, \"&amp;\")\n        .replace(/\\</g, \"&lt;\")\n        .replace(/\\>/g, \"&gt;\")\n        .replace(/\\\"/g, \"&quot;\")\n        .replace(/\\'/g, \"&apos;\")\n        // Delete all ASCII control characters except for TAB (\\x09), LF (\\x0A) and CR (\\x0D)\n        // They are not valid at all in XML 1.0 (even escaped)\n        .replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\"));\n}\nfunction formatAttributes(attrs) {\n    return new XMLString(attrs.map(([key, val]) => `${key}=\"${xmlEscape(val)}\"`).join(\" \"));\n}\nfunction parseXML(xmlString, mimeType = \"text/xml\") {\n    const document = new DOMParser().parseFromString(xmlString.toString(), mimeType);\n    const parserError = document.querySelector(\"parsererror\");\n    if (parserError) {\n        const errorString = parserError.innerHTML;\n        const lineNumber = parseInt(errorString.split(\":\")[0], 10);\n        const xmlStringArray = xmlString.toString().trim().split(\"\\n\");\n        const xmlPreview = xmlStringArray\n            .slice(Math.max(lineNumber - 3, 0), Math.min(lineNumber + 2, xmlStringArray.length))\n            .join(\"\\n\");\n        throw new Error(`XML string could not be parsed: ${errorString}\\n${xmlPreview}`);\n    }\n    return document;\n}\nfunction convertBorderDescr(descr) {\n    if (!descr) {\n        return undefined;\n    }\n    return {\n        style: descr.style,\n        color: { rgb: descr.color },\n    };\n}\nfunction getDefaultXLSXStructure(data) {\n    const xlsxBorders = Object.values(data.borders).map((border) => {\n        return {\n            left: convertBorderDescr(border.left),\n            right: convertBorderDescr(border.right),\n            bottom: convertBorderDescr(border.bottom),\n            top: convertBorderDescr(border.top),\n        };\n    });\n    const borders = [{}, ...xlsxBorders];\n    return {\n        relsFiles: [],\n        sharedStrings: [],\n        // default Values that will always be part of the style sheet\n        styles: [\n            {\n                fontId: 0,\n                fillId: 0,\n                numFmtId: 0,\n                borderId: 0,\n                alignment: {},\n            },\n        ],\n        fonts: [\n            {\n                size: DEFAULT_FONT_SIZE,\n                family: 2,\n                color: { rgb: \"000000\" },\n                name: \"Arial\",\n            },\n        ],\n        fills: [{ reservedAttribute: \"none\" }, { reservedAttribute: \"gray125\" }],\n        borders,\n        numFmts: [],\n        dxfs: [],\n    };\n}\nfunction createOverride(partName, contentType) {\n    return escapeXml /*xml*/ `\n    <Override ContentType=\"${contentType}\" PartName=\"${partName}\" />\n  `;\n}\nfunction createDefaultXMLElement(extension, contentType) {\n    return escapeXml /*xml*/ `\n    <Default Extension=\"${extension}\" ContentType=\"${contentType}\" />\n  `;\n}\nfunction joinXmlNodes(xmlNodes) {\n    return new XMLString(xmlNodes.join(\"\\n\"));\n}\n/**\n * Escape interpolated values except if the value is already\n * a properly escaped XML string.\n *\n * ```\n * escapeXml`<t>${\"This will be escaped\"}</t>`\n * ```\n */\nfunction escapeXml(strings, ...expressions) {\n    let str = [strings[0]];\n    for (let i = 0; i < expressions.length; i++) {\n        const value = expressions[i] instanceof XMLString ? expressions[i] : xmlEscape(expressions[i]);\n        str.push(value + strings[i + 1]);\n    }\n    return new XMLString(concat(str));\n}\n/**\n * Removes the escaped namespace of all the xml tags in the string.\n *\n * Eg. : \"NAMESPACEnsNAMESPACEtest a\" => \"test a\"\n */\nfunction removeTagEscapedNamespaces(tag) {\n    return tag.replace(/NAMESPACE.*NAMESPACE(.*)/, \"$1\");\n}\n/**\n * Encase the namespaces in the element's tags with NAMESPACE string\n *\n * e.g. <x:foo> becomes <NAMESPACExNAMESPACEFoo>\n *\n * That's useful because namespaces aren't supported by the HTML specification, so it's arbitrary whether a HTML parser/querySelector\n * implementation will support namespaces in the tags or not.\n */\nfunction escapeTagNamespaces(str) {\n    return str.replaceAll(/(<\\/?)([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, \"$1\" + \"NAMESPACE\" + \"$2\" + \"NAMESPACE\" + \"$3\");\n}\nfunction escapeQueryNameSpaces(query) {\n    return query.replaceAll(/([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, \"NAMESPACE\" + \"$1\" + \"NAMESPACE\" + \"$2\");\n}\n\nclass AttributeValue {\n    value;\n    constructor(value) {\n        this.value = value;\n    }\n    asString() {\n        return fixXlsxUnicode(String(this.value));\n    }\n    asBool() {\n        if (this.value === \"true\")\n            return true; // for files exported from Libre Office\n        if (this.value === \"false\")\n            return false;\n        return Boolean(Number(this.value));\n    }\n    asNum() {\n        return Number(this.value);\n    }\n}\nclass XlsxBaseExtractor {\n    rootFile;\n    xlsxFileStructure;\n    warningManager;\n    relationships;\n    // The xml file we are currently parsing. We should have one Extractor class by XLSXImportFile, but\n    // the XLSXImportFile contains both the main .xml file, and the .rels file\n    currentFile = undefined;\n    /**\n     * /!\\ Important : There should be no namespaces in the tags of the XML files.\n     *\n     * This class use native querySelector and querySelectorAll, that's used for HTML (not XML). These aren't supposed to\n     * handled namespaces, as they are not supported by the HTML specification. Some implementations (most browsers) do\n     * actually support namespaces, but some don't (e.g. jsdom).\n     *\n     * The namespace should be escaped as with NAMESPACE string (eg. <t:foo> => <NAMESPACEtNAMESPACEfoo>).\n     */\n    constructor(rootFile, xlsxStructure, warningManager) {\n        this.rootFile = rootFile;\n        this.currentFile = rootFile.file.fileName;\n        this.xlsxFileStructure = xlsxStructure;\n        this.warningManager = warningManager;\n        this.relationships = {};\n        if (rootFile.rels) {\n            this.extractRelationships(rootFile.rels).map((rel) => {\n                this.relationships[rel.id] = rel;\n            });\n        }\n    }\n    /**\n     * Extract all the relationships inside a .xml.rels file\n     */\n    extractRelationships(relFile) {\n        return this.mapOnElements({ parent: relFile.xml, query: \"Relationship\" }, (relationshipElement) => {\n            return {\n                id: this.extractAttr(relationshipElement, \"Id\", { required: true }).asString(),\n                target: this.extractAttr(relationshipElement, \"Target\", { required: true }).asString(),\n                type: this.extractAttr(relationshipElement, \"Type\", { required: true }).asString(),\n            };\n        });\n    }\n    /**\n     * Get the list of all the XLSX files in the XLSX file structure\n     */\n    getListOfXMLFiles() {\n        const XMLFiles = Object.entries(this.xlsxFileStructure)\n            .filter(([key]) => key !== \"images\")\n            .map(([_, value]) => value)\n            .flat()\n            .filter(isDefined);\n        return XMLFiles;\n    }\n    /**\n     * Return an array containing the return value of the given function applied to all the XML elements\n     * found using the MapOnElementArgs.\n     *\n     * The arguments contains :\n     *  - query : a QuerySelector string to find the elements to apply the function to\n     *  - parent : an XML element or XML document in which to find the queried elements\n     *  - children : if true, the function is applied on the direct children of the queried element\n     *\n     * This method will also handle the errors thrown in the argument function.\n     */\n    mapOnElements(args, fct) {\n        const ret = [];\n        const oldWorkingDocument = this.currentFile;\n        let elements;\n        if (args.children) {\n            const children = this.querySelector(args.parent, args.query)?.children;\n            elements = children ? children : [];\n        }\n        else {\n            elements = this.querySelectorAll(args.parent, args.query);\n        }\n        if (elements) {\n            for (let element of elements) {\n                try {\n                    ret.push(fct(element));\n                }\n                catch (e) {\n                    this.catchErrorOnElement(e, element);\n                }\n            }\n        }\n        this.currentFile = oldWorkingDocument;\n        return ret;\n    }\n    /**\n     * Log an error caught when parsing an element in the warningManager.\n     */\n    catchErrorOnElement(error, onElement) {\n        const errorMsg = onElement\n            ? `Error when parsing an element <${onElement.tagName}> of file ${this.currentFile}, skip this element. \\n${error.stack}`\n            : `Error when parsing file ${this.currentFile}.`;\n        this.warningManager.addParsingWarning([errorMsg, error.message].join(\"\\n\"));\n    }\n    /**\n     * Extract an attribute from an Element.\n     *\n     * If the attribute is required but was not found, will add a warning in the warningManager if it was given a default\n     * value, and throw an error if no default value was given.\n     *\n     * Can only return undefined value for non-required attributes without default value.\n     */\n    extractAttr(e, attName, optionalArgs) {\n        const attribute = e.attributes[attName];\n        if (!attribute)\n            this.handleMissingValue(e, `attribute \"${attName}\"`, optionalArgs);\n        const value = attribute?.value ? attribute.value : optionalArgs?.default;\n        return (value === undefined ? undefined : new AttributeValue(value));\n    }\n    /**\n     * Extract the text content of an Element.\n     *\n     * If the text content is required but was not found, will add a warning in the warningManager if it was given a default\n     * value, and throw an error if no default value was given.\n     *\n     * Can only return undefined value for non-required text content without default value.\n     */\n    extractTextContent(element, optionalArgs) {\n        if (optionalArgs?.default !== undefined && typeof optionalArgs.default !== \"string\") {\n            throw new Error(\"extractTextContent default value should be a string\");\n        }\n        const shouldPreserveSpaces = element?.attributes[\"xml:space\"]?.value === \"preserve\";\n        let textContent = element?.textContent;\n        if (!element || textContent === null) {\n            this.handleMissingValue(element, `text content`, optionalArgs);\n        }\n        if (textContent) {\n            textContent = shouldPreserveSpaces ? textContent : textContent.trim();\n        }\n        return (textContent ? fixXlsxUnicode(textContent) : optionalArgs?.default);\n    }\n    /**\n     * Extract an attribute of a child of the given element.\n     *\n     * The reference of a child can be a string (tag of the child) or an number (index in the list of children of the element)\n     *\n     * If the attribute is required but either the attribute or the referenced child element was not found, it will\n     * will add a warning in the warningManager if it was given a default value, and throw an error if no default value was given.\n     *\n     * Can only return undefined value for non-required attributes without default value.\n     */\n    extractChildAttr(e, childRef, attName, optionalArgs) {\n        let child;\n        if (typeof childRef === \"number\") {\n            child = e.children[childRef];\n        }\n        else {\n            child = this.querySelector(e, childRef);\n        }\n        if (!child) {\n            this.handleMissingValue(e, typeof childRef === \"number\" ? `child at index ${childRef}` : `child <${childRef}>`, optionalArgs);\n        }\n        const value = child\n            ? this.extractAttr(child, attName, optionalArgs)?.asString()\n            : optionalArgs?.default;\n        return (value !== undefined ? new AttributeValue(value) : undefined);\n    }\n    /**\n     * Extract the text content of a child of the given element.\n     *\n     * If the text content is required but either the text content or the referenced child element was not found, it will\n     * will add a warning in the warningManager if it was given a default value, and throw an error if no default value was given.\n     *\n     * Can only return undefined value for non-required text content without default value.\n     */\n    extractChildTextContent(e, childRef, optionalArgs) {\n        if (optionalArgs?.default !== undefined && typeof optionalArgs.default !== \"string\") {\n            throw new Error(\"extractTextContent default value should be a string\");\n        }\n        let child = this.querySelector(e, childRef);\n        if (!child) {\n            this.handleMissingValue(e, `child <${childRef}>`, optionalArgs);\n        }\n        return (child ? this.extractTextContent(child, optionalArgs) : optionalArgs?.default);\n    }\n    /**\n     * Should be called if a extractAttr/extractTextContent doesn't find the element it needs to extract.\n     *\n     * If the extractable was required, this function will add a warning in the warningManager if there was a default value,\n     * and throw an error if no default value was given.\n     */\n    handleMissingValue(parentElement, missingElementName, optionalArgs) {\n        if (optionalArgs?.required) {\n            if (optionalArgs?.default) {\n                this.warningManager.addParsingWarning(`Missing required ${missingElementName} in element <${parentElement.tagName}> of ${this.currentFile}, replacing it by the default value ${optionalArgs.default}`);\n            }\n            else {\n                throw new Error(`Missing required ${missingElementName} in element <${parentElement.tagName}> of ${this.currentFile}, and no default value was set`);\n            }\n        }\n    }\n    /**\n     * Extract a color, extracting it from the theme if needed.\n     *\n     * Will throw an error if the element references a theme, but no theme was provided or the theme it doesn't contain the color.\n     */\n    extractColor(colorElement, theme, defaultColor) {\n        if (!colorElement) {\n            return defaultColor ? { rgb: defaultColor } : undefined;\n        }\n        const themeIndex = this.extractAttr(colorElement, \"theme\")?.asString();\n        let rgb;\n        if (themeIndex !== undefined) {\n            if (!theme || !theme.clrScheme) {\n                throw new Error(\"Color referencing a theme but no theme was provided\");\n            }\n            rgb = this.getThemeColor(themeIndex, theme.clrScheme);\n        }\n        else {\n            rgb = this.extractAttr(colorElement, \"rgb\")?.asString();\n            rgb = rgb === DEFAULT_SYSTEM_COLOR ? undefined : rgb;\n        }\n        const color = {\n            rgb: rgb || defaultColor,\n            auto: this.extractAttr(colorElement, \"auto\")?.asBool(),\n            indexed: this.extractAttr(colorElement, \"indexed\")?.asNum(),\n            tint: this.extractAttr(colorElement, \"tint\")?.asNum(),\n        };\n        return color;\n    }\n    /**\n     * Returns the xml file targeted by a relationship.\n     */\n    getTargetXmlFile(relationship) {\n        if (!relationship)\n            throw new Error(\"Undefined target file\");\n        const target = this.processRelationshipTargetName(relationship.target);\n        // Use \"endsWith\" because targets are relative paths, and we know the files by their absolute path.\n        const f = this.getListOfXMLFiles().find((f) => f.file.fileName.endsWith(target));\n        if (!f || !f.file)\n            throw new Error(\"Cannot find target file\");\n        return f;\n    }\n    /**\n     * Returns the image parameters targeted by a relationship.\n     */\n    getTargetImageFile(relationship) {\n        if (!relationship)\n            throw new Error(\"Undefined target file\");\n        const target = this.processRelationshipTargetName(relationship.target);\n        // Use \"endsWith\" because targets are relative paths, and we know the files by their absolute path.\n        const f = this.xlsxFileStructure.images.find((f) => f.fileName.endsWith(target));\n        if (!f)\n            throw new Error(\"Cannot find target file\");\n        return f;\n    }\n    querySelector(element, query) {\n        const escapedQuery = escapeQueryNameSpaces(query);\n        return element.querySelector(escapedQuery);\n    }\n    querySelectorAll(element, query) {\n        const escapedQuery = escapeQueryNameSpaces(query);\n        return element.querySelectorAll(escapedQuery);\n    }\n    /**\n     * Get a color from its id in the Theme's colorScheme.\n     *\n     * Note that Excel don't use the colors from the theme but from its own internal theme, so the displayed\n     * colors will be different in the import than in excel.\n     * .\n     */\n    getThemeColor(colorId, clrScheme) {\n        switch (colorId) {\n            case \"0\": // 0 : sysColor window text\n                return \"FFFFFF\";\n            case \"1\": // 1 : sysColor window background\n                return \"000000\";\n            // Don't ask me why these 2 are inverted, I cannot find any documentation for it but everyone does it\n            case \"2\":\n                return clrScheme[\"3\"].value;\n            case \"3\":\n                return clrScheme[\"2\"].value;\n            default:\n                return clrScheme[colorId].value;\n        }\n    }\n    /** Remove signs of relative path. */\n    processRelationshipTargetName(targetName) {\n        return targetName.replace(/\\.+\\//, \"\");\n    }\n}\n\n/**\n * XLSX Extractor class that can be used for either sharedString XML files or theme XML files.\n *\n * Since they both are quite simple, it make sense to make a single class to manage them all, to avoid unnecessary file\n * cluttering.\n */\nclass XlsxMiscExtractor extends XlsxBaseExtractor {\n    getTheme() {\n        const clrScheme = this.mapOnElements({ query: \"a:clrScheme\", parent: this.rootFile.file.xml, children: true }, (element) => {\n            return {\n                name: element.tagName,\n                value: this.extractChildAttr(element, 0, \"val\", {\n                    required: true,\n                    default: AUTO_COLOR,\n                }).asString(),\n                lastClr: this.extractChildAttr(element, 0, \"lastClr\", {\n                    default: AUTO_COLOR,\n                }).asString(),\n            };\n        });\n        return { clrScheme };\n    }\n    /**\n     * Get the array of shared strings of the XLSX.\n     *\n     * Worth noting that running a prettier on the xml can mess up some strings, since there is an option in the\n     * xmls to keep the spacing and not trim the string.\n     */\n    getSharedStrings() {\n        return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"si\" }, (ssElement) => {\n            // Shared string can either be a simple text, or a rich text (text with formatting, possibly in multiple parts)\n            if (ssElement.children[0].tagName === \"t\") {\n                return this.extractTextContent(ssElement) || \"\";\n            }\n            // We don't support rich text formatting, we'll only extract the text\n            else {\n                return this.mapOnElements({ parent: ssElement, query: \"t\" }, (textElement) => {\n                    return this.extractTextContent(textElement) || \"\";\n                }).join(\"\");\n            }\n        });\n    }\n}\n\nclass XlsxCfExtractor extends XlsxBaseExtractor {\n    theme;\n    constructor(sheetFile, xlsxStructure, warningManager, theme) {\n        super(sheetFile, xlsxStructure, warningManager);\n        this.theme = theme;\n    }\n    extractConditionalFormattings() {\n        const cfs = this.mapOnElements({ parent: this.rootFile.file.xml, query: \"worksheet > conditionalFormatting\" }, (cfElement) => {\n            return {\n                // sqref = ranges on which the cf applies, separated by spaces\n                sqref: this.extractAttr(cfElement, \"sqref\", { required: true }).asString().split(\" \"),\n                pivot: this.extractAttr(cfElement, \"pivot\")?.asBool(),\n                cfRules: this.extractCFRules(cfElement, this.theme),\n            };\n        });\n        // XLSX extension to OpenXml\n        cfs.push(...this.mapOnElements({ parent: this.rootFile.file.xml, query: \"extLst x14:conditionalFormatting\" }, (cfElement) => {\n            return {\n                sqref: this.extractChildTextContent(cfElement, \"xm:sqref\", { required: true }).split(\" \"),\n                pivot: this.extractAttr(cfElement, \"xm:pivot\")?.asBool(),\n                cfRules: this.extractCFRules(cfElement, this.theme),\n            };\n        }));\n        return cfs;\n    }\n    extractCFRules(cfElement, theme) {\n        return this.mapOnElements({ parent: cfElement, query: \"cfRule, x14:cfRule\" }, (cfRuleElement) => {\n            const cfType = this.extractAttr(cfRuleElement, \"type\", {\n                required: true,\n            }).asString();\n            if (cfType === \"dataBar\") {\n                // Databars are an extension to OpenXml and have a different format (XLSX \u00a72.6.30). Do'nt bother\n                // extracting them as we don't support them.\n                throw new Error(\"Databars conditional formats are not supported.\");\n            }\n            return {\n                type: cfType,\n                priority: this.extractAttr(cfRuleElement, \"priority\", { required: true }).asNum(),\n                colorScale: this.extractCfColorScale(cfRuleElement, theme),\n                formula: this.extractCfFormula(cfRuleElement),\n                iconSet: this.extractCfIconSet(cfRuleElement),\n                dxfId: this.extractAttr(cfRuleElement, \"dxfId\")?.asNum(),\n                stopIfTrue: this.extractAttr(cfRuleElement, \"stopIfTrue\")?.asBool(),\n                aboveAverage: this.extractAttr(cfRuleElement, \"aboveAverage\")?.asBool(),\n                percent: this.extractAttr(cfRuleElement, \"percent\")?.asBool(),\n                bottom: this.extractAttr(cfRuleElement, \"bottom\")?.asBool(),\n                operator: this.extractAttr(cfRuleElement, \"operator\")?.asString(),\n                text: this.extractAttr(cfRuleElement, \"text\")?.asString(),\n                timePeriod: this.extractAttr(cfRuleElement, \"timePeriod\")?.asString(),\n                rank: this.extractAttr(cfRuleElement, \"rank\")?.asNum(),\n                stdDev: this.extractAttr(cfRuleElement, \"stdDev\")?.asNum(),\n                equalAverage: this.extractAttr(cfRuleElement, \"equalAverage\")?.asBool(),\n            };\n        });\n    }\n    extractCfFormula(cfRulesElement) {\n        return this.mapOnElements({ parent: cfRulesElement, query: \"formula\" }, (cfFormulaElements) => {\n            return this.extractTextContent(cfFormulaElements, { required: true });\n        });\n    }\n    extractCfColorScale(cfRulesElement, theme) {\n        const colorScaleElement = this.querySelector(cfRulesElement, \"colorScale\");\n        if (!colorScaleElement)\n            return undefined;\n        return {\n            colors: this.mapOnElements({ parent: colorScaleElement, query: \"color\" }, (colorElement) => {\n                return this.extractColor(colorElement, theme, \"ffffff\");\n            }),\n            cfvos: this.extractCFVos(colorScaleElement),\n        };\n    }\n    extractCfIconSet(cfRulesElement) {\n        const iconSetElement = this.querySelector(cfRulesElement, \"iconSet, x14:iconSet\");\n        if (!iconSetElement)\n            return undefined;\n        return {\n            iconSet: this.extractAttr(iconSetElement, \"iconSet\", {\n                default: \"3TrafficLights1\",\n            }).asString(),\n            showValue: this.extractAttr(iconSetElement, \"showValue\", { default: true }).asBool(),\n            percent: this.extractAttr(iconSetElement, \"percent\", { default: true }).asBool(),\n            reverse: this.extractAttr(iconSetElement, \"reverse\")?.asBool(),\n            custom: this.extractAttr(iconSetElement, \"custom\")?.asBool(),\n            cfvos: this.extractCFVos(iconSetElement),\n            cfIcons: this.extractCfIcons(iconSetElement),\n        };\n    }\n    extractCfIcons(iconSetElement) {\n        const icons = this.mapOnElements({ parent: iconSetElement, query: \"cfIcon, x14:cfIcon\" }, (cfIconElement) => {\n            return {\n                iconSet: this.extractAttr(cfIconElement, \"iconSet\", {\n                    required: true,\n                }).asString(),\n                iconId: this.extractAttr(cfIconElement, \"iconId\", { required: true }).asNum(),\n            };\n        });\n        return icons.length === 0 ? undefined : icons;\n    }\n    extractCFVos(parent) {\n        return this.mapOnElements({ parent, query: \"cfvo, x14:cfvo\" }, (cfVoElement) => {\n            return {\n                type: this.extractAttr(cfVoElement, \"type\", {\n                    required: true,\n                }).asString(),\n                gte: this.extractAttr(cfVoElement, \"gte\", { default: true })?.asBool(),\n                value: cfVoElement.attributes[\"val\"]\n                    ? this.extractAttr(cfVoElement, \"val\")?.asString()\n                    : this.extractChildTextContent(cfVoElement, \"f, xm:f\"),\n            };\n        });\n    }\n}\n\nclass XlsxChartExtractor extends XlsxBaseExtractor {\n    extractChart() {\n        return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"c:chartSpace\" }, (rootChartElement) => {\n            const chartType = this.getChartType(rootChartElement);\n            if (!CHART_TYPE_CONVERSION_MAP[chartType]) {\n                throw new Error(`Unsupported chart type ${chartType}`);\n            }\n            if (CHART_TYPE_CONVERSION_MAP[chartType] === \"combo\") {\n                return this.extractComboChart(rootChartElement);\n            }\n            // Title can be separated into multiple xml elements (for styling and such), we only import the text\n            const chartTitle = this.mapOnElements({ parent: rootChartElement, query: \"c:chart > c:title a:t\" }, (textElement) => {\n                return textElement.textContent || \"\";\n            }).join(\"\");\n            const barChartGrouping = this.extractChildAttr(rootChartElement, \"c:grouping\", \"val\", {\n                default: \"clustered\",\n            }).asString();\n            return {\n                title: { text: chartTitle },\n                type: CHART_TYPE_CONVERSION_MAP[chartType],\n                dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),\n                labelRange: this.extractChildTextContent(rootChartElement, `c:ser ${chartType === \"scatterChart\" ? \"c:numRef\" : \"c:cat\"} c:f`),\n                backgroundColor: this.extractChildAttr(rootChartElement, \"c:chartSpace > c:spPr a:srgbClr\", \"val\", {\n                    default: \"ffffff\",\n                }).asString(),\n                legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(rootChartElement, \"c:legendPos\", \"val\", {\n                    default: \"b\",\n                }).asString()],\n                stacked: barChartGrouping === \"stacked\",\n                fontColor: \"000000\",\n            };\n        })[0];\n    }\n    extractComboChart(chartElement) {\n        // Title can be separated into multiple xml elements (for styling and such), we only import the text\n        const chartTitle = this.mapOnElements({ parent: chartElement, query: \"c:title a:t\" }, (textElement) => {\n            return textElement.textContent || \"\";\n        }).join(\"\");\n        const barChartGrouping = this.extractChildAttr(chartElement, \"c:grouping\", \"val\", {\n            default: \"clustered\",\n        }).asString();\n        return {\n            title: { text: chartTitle },\n            type: \"combo\",\n            dataSets: [\n                ...this.extractChartDatasets(this.querySelectorAll(chartElement, `c:barChart`), \"comboChart\"),\n                ...this.extractChartDatasets(this.querySelectorAll(chartElement, `c:lineChart`), \"comboChart\"),\n            ],\n            labelRange: this.extractChildTextContent(chartElement, \"c:ser c:cat c:f\"),\n            backgroundColor: this.extractChildAttr(chartElement, \"c:chartSpace > c:spPr a:srgbClr\", \"val\", {\n                default: \"ffffff\",\n            }).asString(),\n            legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, \"c:legendPos\", \"val\", {\n                default: \"b\",\n            }).asString()],\n            stacked: barChartGrouping === \"stacked\",\n            fontColor: \"000000\",\n        };\n    }\n    extractChartDatasets(chartElements, chartType) {\n        return Array.from(chartElements)\n            .map((element) => {\n            if (chartType === \"scatterChart\") {\n                return this.extractScatterChartDatasets(element);\n            }\n            return this.mapOnElements({ parent: element, query: \"c:ser\" }, (chartDataElement) => {\n                let label = {};\n                const reference = this.extractChildTextContent(chartDataElement, \"c:tx c:f\");\n                if (reference) {\n                    label = { reference };\n                }\n                else {\n                    const text = this.extractChildTextContent(chartDataElement, \"c:tx c:v\");\n                    if (text) {\n                        label = { text };\n                    }\n                }\n                const color = this.extractChildAttr(chartDataElement, \"c:spPr a:solidFill a:srgbClr\", \"val\");\n                return {\n                    label,\n                    range: this.extractChildTextContent(chartDataElement, \"c:val c:f\", {\n                        required: true,\n                    }),\n                    backgroundColor: color ? `${toHex(color.asString())}` : undefined,\n                };\n            });\n        })\n            .flat();\n    }\n    extractScatterChartDatasets(chartElement) {\n        return this.mapOnElements({ parent: chartElement, query: \"c:ser\" }, (chartDataElement) => {\n            let label = {};\n            const reference = this.extractChildTextContent(chartDataElement, \"c:tx c:f\");\n            if (reference) {\n                label = { reference };\n            }\n            else {\n                const text = this.extractChildTextContent(chartDataElement, \"c:tx c:v\");\n                if (text) {\n                    label = { text };\n                }\n            }\n            return {\n                label,\n                range: this.extractChildTextContent(chartDataElement, \"c:yVal c:f\", { required: true }),\n            };\n        });\n    }\n    /**\n     * The chart type in the XML isn't explicitly defined, but there is an XML element that define the\n     * chart, and this element tag name tells us which type of chart it is. We just need to find this XML element.\n     */\n    getChartType(chartElement) {\n        const plotAreaElement = this.querySelector(chartElement, \"c:plotArea\");\n        if (!plotAreaElement) {\n            throw new Error(\"Missing plot area in the chart definition.\");\n        }\n        let globalTag = undefined;\n        for (let child of plotAreaElement.children) {\n            const tag = removeTagEscapedNamespaces(child.tagName);\n            if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {\n                if (!globalTag) {\n                    globalTag = tag;\n                }\n                else if (globalTag !== tag) {\n                    globalTag = \"comboChart\";\n                }\n            }\n        }\n        if (globalTag) {\n            return globalTag;\n        }\n        throw new Error(\"Unknown chart type\");\n    }\n}\n\nconst ONE_CELL_ANCHOR = \"oneCellAnchor\";\nconst TWO_CELL_ANCHOR = \"twoCellAnchor\";\nclass XlsxFigureExtractor extends XlsxBaseExtractor {\n    extractFigures() {\n        return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"xdr:wsDr\", children: true }, (figureElement) => {\n            const anchorType = removeTagEscapedNamespaces(figureElement.tagName);\n            const anchors = this.extractFigureAnchorsByType(figureElement, anchorType);\n            const chartElement = this.querySelector(figureElement, \"c:chart\");\n            const imageElement = this.querySelector(figureElement, \"a:blip\");\n            if (!chartElement && !imageElement) {\n                throw new Error(\"Only chart and image figures are currently supported.\");\n            }\n            return {\n                anchors,\n                data: chartElement ? this.extractChart(chartElement) : this.extractImage(figureElement),\n                figureSize: anchorType === ONE_CELL_ANCHOR\n                    ? this.extractFigureSizeFromSizeTag(figureElement, \"xdr:ext\")\n                    : undefined,\n            };\n        });\n    }\n    extractFigureAnchorsByType(figureElement, anchorType) {\n        switch (anchorType) {\n            case ONE_CELL_ANCHOR:\n                return [this.extractFigureAnchor(\"xdr:from\", figureElement)];\n            case TWO_CELL_ANCHOR:\n                return [\n                    this.extractFigureAnchor(\"xdr:from\", figureElement),\n                    this.extractFigureAnchor(\"xdr:to\", figureElement),\n                ];\n            default:\n                throw new Error(`${anchorType} is not supported for xlsx drawings. `);\n        }\n    }\n    extractFigureSizeFromSizeTag(figureElement, sizeTag) {\n        const sizeElement = this.querySelector(figureElement, sizeTag);\n        if (!sizeElement) {\n            throw new Error(`Missing size element '${sizeTag}'`);\n        }\n        return {\n            cx: this.extractAttr(sizeElement, \"cx\", { required: true }).asNum(),\n            cy: this.extractAttr(sizeElement, \"cy\", { required: true }).asNum(),\n        };\n    }\n    extractFigureAnchor(anchorTag, figureElement) {\n        const anchor = this.querySelector(figureElement, anchorTag);\n        if (!anchor) {\n            throw new Error(`Missing anchor element ${anchorTag}`);\n        }\n        return {\n            col: Number(this.extractChildTextContent(anchor, \"xdr:col\", { required: true })),\n            colOffset: Number(this.extractChildTextContent(anchor, \"xdr:colOff\", { required: true })),\n            row: Number(this.extractChildTextContent(anchor, \"xdr:row\", { required: true })),\n            rowOffset: Number(this.extractChildTextContent(anchor, \"xdr:rowOff\", { required: true })),\n        };\n    }\n    extractChart(chartElement) {\n        const chartId = this.extractAttr(chartElement, \"r:id\", { required: true }).asString();\n        const chartFile = this.getTargetXmlFile(this.relationships[chartId]);\n        const chartDefinition = new XlsxChartExtractor(chartFile, this.xlsxFileStructure, this.warningManager).extractChart();\n        if (!chartDefinition) {\n            throw new Error(\"Unable to extract chart definition\");\n        }\n        return chartDefinition;\n    }\n    extractImage(figureElement) {\n        const imageElement = this.querySelector(figureElement, \"a:blip\");\n        const imageId = this.extractAttr(imageElement, \"r:embed\", { required: true }).asString();\n        const image = this.getTargetImageFile(this.relationships[imageId]);\n        if (!image) {\n            throw new Error(\"Unable to extract image\");\n        }\n        const extension = image.fileName.split(\".\").at(-1);\n        const anchorType = removeTagEscapedNamespaces(figureElement.tagName);\n        const sizeElement = anchorType === TWO_CELL_ANCHOR ? this.querySelector(figureElement, \"a:xfrm\") : figureElement;\n        const sizeTag = anchorType === TWO_CELL_ANCHOR ? \"a:ext\" : \"xdr:ext\";\n        const size = this.extractFigureSizeFromSizeTag(sizeElement, sizeTag);\n        return {\n            imageSrc: image.imageSrc,\n            mimetype: extension ? IMAGE_EXTENSION_TO_MIMETYPE_MAPPING[extension] : undefined,\n            size,\n        };\n    }\n}\n\n/**\n * We don't really support pivot tables, we'll just extract them as Tables.\n */\nclass XlsxPivotExtractor extends XlsxBaseExtractor {\n    getPivotTable() {\n        return this.mapOnElements(\n        // Use :root instead of \"pivotTableDefinition\" because others pivotTableDefinition elements are present inside the root\n        // pivotTableDefinition elements.\n        { query: \":root\", parent: this.rootFile.file.xml }, (pivotElement) => {\n            return {\n                name: this.extractAttr(pivotElement, \"name\", { required: true }).asString(),\n                rowGrandTotals: this.extractAttr(pivotElement, \"rowGrandTotals\", {\n                    default: true,\n                }).asBool(),\n                location: this.extractPivotLocation(pivotElement),\n                style: this.extractPivotStyleInfo(pivotElement),\n            };\n        })[0];\n    }\n    extractPivotLocation(pivotElement) {\n        return this.mapOnElements({ query: \"location\", parent: pivotElement }, (pivotStyleElement) => {\n            return {\n                ref: this.extractAttr(pivotStyleElement, \"ref\", { required: true }).asString(),\n                firstHeaderRow: this.extractAttr(pivotStyleElement, \"firstHeaderRow\", {\n                    required: true,\n                }).asNum(),\n                firstDataRow: this.extractAttr(pivotStyleElement, \"firstDataRow\", {\n                    required: true,\n                }).asNum(),\n                firstDataCol: this.extractAttr(pivotStyleElement, \"firstDataCol\", {\n                    required: true,\n                }).asNum(),\n            };\n        })[0];\n    }\n    extractPivotStyleInfo(pivotElement) {\n        return this.mapOnElements({ query: \"pivotTableStyleInfo\", parent: pivotElement }, (pivotStyleElement) => {\n            return {\n                name: this.extractAttr(pivotStyleElement, \"name\", { required: true }).asString(),\n                showRowHeaders: this.extractAttr(pivotStyleElement, \"showRowHeaders\", {\n                    required: true,\n                }).asBool(),\n                showColHeaders: this.extractAttr(pivotStyleElement, \"showColHeaders\", {\n                    required: true,\n                }).asBool(),\n                showRowStripes: this.extractAttr(pivotStyleElement, \"showRowStripes\", {\n                    required: true,\n                }).asBool(),\n                showColStripes: this.extractAttr(pivotStyleElement, \"showColStripes\", {\n                    required: true,\n                }).asBool(),\n                showLastColumn: this.extractAttr(pivotStyleElement, \"showLastColumn\")?.asBool(),\n            };\n        })[0];\n    }\n}\n\nclass XlsxTableExtractor extends XlsxBaseExtractor {\n    getTable() {\n        return this.mapOnElements({ query: \"table\", parent: this.rootFile.file.xml }, (tableElement) => {\n            return {\n                displayName: this.extractAttr(tableElement, \"displayName\", {\n                    required: true,\n                }).asString(),\n                name: this.extractAttr(tableElement, \"name\")?.asString(),\n                id: this.extractAttr(tableElement, \"id\", { required: true }).asString(),\n                ref: this.extractAttr(tableElement, \"ref\", { required: true }).asString(),\n                headerRowCount: this.extractAttr(tableElement, \"headerRowCount\", {\n                    default: 1,\n                }).asNum(),\n                totalsRowCount: this.extractAttr(tableElement, \"totalsRowCount\", {\n                    default: 0,\n                }).asNum(),\n                cols: this.extractTableCols(tableElement),\n                style: this.extractTableStyleInfo(tableElement),\n                autoFilter: this.extractTableAutoFilter(tableElement),\n            };\n        })[0];\n    }\n    extractTableCols(tableElement) {\n        return this.mapOnElements({ query: \"tableColumn\", parent: tableElement }, (tableColElement) => {\n            return {\n                id: this.extractAttr(tableColElement, \"id\", { required: true }).asString(),\n                name: this.extractAttr(tableColElement, \"name\", { required: true }).asString(),\n                colFormula: this.extractChildTextContent(tableColElement, \"calculatedColumnFormula\"),\n            };\n        });\n    }\n    extractTableStyleInfo(tableElement) {\n        return this.mapOnElements({ query: \"tableStyleInfo\", parent: tableElement }, (tableStyleElement) => {\n            return {\n                name: this.extractAttr(tableStyleElement, \"name\")?.asString(),\n                showFirstColumn: this.extractAttr(tableStyleElement, \"showFirstColumn\")?.asBool(),\n                showLastColumn: this.extractAttr(tableStyleElement, \"showLastColumn\")?.asBool(),\n                showRowStripes: this.extractAttr(tableStyleElement, \"showRowStripes\")?.asBool(),\n                showColumnStripes: this.extractAttr(tableStyleElement, \"showColumnStripes\")?.asBool(),\n            };\n        })[0];\n    }\n    extractTableAutoFilter(tableElement) {\n        return this.mapOnElements({ query: \"autoFilter\", parent: tableElement }, (autoFilterElement) => {\n            return {\n                columns: this.extractFilterColumns(autoFilterElement),\n                zone: this.extractAttr(autoFilterElement, \"ref\", { required: true }).asString(),\n            };\n        })[0];\n    }\n    extractFilterColumns(autoFilterElement) {\n        return this.mapOnElements({ query: \"tableColumn\", parent: autoFilterElement }, (filterColumnElement) => {\n            return {\n                colId: this.extractAttr(autoFilterElement, \"colId\", { required: true }).asNum(),\n                hiddenButton: this.extractAttr(autoFilterElement, \"hiddenButton\", {\n                    default: false,\n                }).asBool(),\n                filters: this.extractSimpleFilter(filterColumnElement),\n            };\n        });\n    }\n    extractSimpleFilter(filterColumnElement) {\n        return this.mapOnElements({ query: \"filter\", parent: filterColumnElement }, (filterColumnElement) => {\n            return {\n                val: this.extractAttr(filterColumnElement, \"val\", { required: true }).asString(),\n            };\n        });\n    }\n}\n\nclass XlsxSheetExtractor extends XlsxBaseExtractor {\n    theme;\n    constructor(sheetFile, xlsxStructure, warningManager, theme) {\n        super(sheetFile, xlsxStructure, warningManager);\n        this.theme = theme;\n    }\n    getSheet() {\n        return this.mapOnElements({ query: \"worksheet\", parent: this.rootFile.file.xml }, (sheetElement) => {\n            const sheetWorkbookInfo = this.getSheetWorkbookInfo();\n            return {\n                sheetName: this.extractSheetName(),\n                sheetViews: this.extractSheetViews(sheetElement),\n                sheetFormat: this.extractSheetFormat(sheetElement),\n                sheetProperties: this.extractSheetProperties(sheetElement),\n                cols: this.extractCols(sheetElement),\n                rows: this.extractRows(sheetElement),\n                sharedFormulas: this.extractSharedFormulas(sheetElement),\n                merges: this.extractMerges(sheetElement),\n                cfs: this.extractConditionalFormats(),\n                figures: this.extractFigures(sheetElement),\n                hyperlinks: this.extractHyperLinks(sheetElement),\n                tables: this.extractTables(sheetElement),\n                pivotTables: this.extractPivotTables(),\n                isVisible: sheetWorkbookInfo.state === \"visible\" ? true : false,\n            };\n        })[0];\n    }\n    extractSheetViews(worksheet) {\n        return this.mapOnElements({ parent: worksheet, query: \"sheetView\" }, (sheetViewElement) => {\n            const paneElement = this.querySelector(sheetViewElement, \"pane\");\n            return {\n                tabSelected: this.extractAttr(sheetViewElement, \"tabSelected\", {\n                    default: false,\n                }).asBool(),\n                showFormulas: this.extractAttr(sheetViewElement, \"showFormulas\", {\n                    default: false,\n                }).asBool(),\n                showGridLines: this.extractAttr(sheetViewElement, \"showGridLines\", {\n                    default: true,\n                }).asBool(),\n                showRowColHeaders: this.extractAttr(sheetViewElement, \"showRowColHeaders\", {\n                    default: true,\n                }).asBool(),\n                pane: {\n                    xSplit: paneElement\n                        ? this.extractAttr(paneElement, \"xSplit\", { default: 0 }).asNum()\n                        : 0,\n                    ySplit: paneElement\n                        ? this.extractAttr(paneElement, \"ySplit\", { default: 0 }).asNum()\n                        : 0,\n                },\n            };\n        });\n    }\n    extractSheetName() {\n        const relativePath = getRelativePath(this.xlsxFileStructure.workbook.file.fileName, this.rootFile.file.fileName);\n        const workbookRels = this.extractRelationships(this.xlsxFileStructure.workbook.rels);\n        const relId = workbookRels.find((rel) => rel.target === relativePath).id;\n        // Having a namespace in the attributes names mess with the querySelector, and the behavior is not the same\n        // for every XML parser. So we'll search manually instead of using a querySelector to search for an attribute value.\n        for (let sheetElement of this.querySelectorAll(this.xlsxFileStructure.workbook.file.xml, \"sheet\")) {\n            if (sheetElement.attributes[\"r:id\"].value === relId) {\n                return sheetElement.attributes[\"name\"].value;\n            }\n        }\n        throw new Error(\"Missing sheet name\");\n    }\n    getSheetWorkbookInfo() {\n        const relativePath = getRelativePath(this.xlsxFileStructure.workbook.file.fileName, this.rootFile.file.fileName);\n        const workbookRels = this.extractRelationships(this.xlsxFileStructure.workbook.rels);\n        const relId = workbookRels.find((rel) => rel.target === relativePath).id;\n        const workbookSheets = this.mapOnElements({ parent: this.xlsxFileStructure.workbook.file.xml, query: \"sheet\" }, (sheetElement) => {\n            return {\n                relationshipId: this.extractAttr(sheetElement, \"r:id\", { required: true }).asString(),\n                sheetId: this.extractAttr(sheetElement, \"sheetId\", { required: true }).asString(),\n                sheetName: this.extractAttr(sheetElement, \"name\", { required: true }).asString(),\n                state: this.extractAttr(sheetElement, \"state\", {\n                    default: \"visible\",\n                }).asString(),\n            };\n        });\n        const info = workbookSheets.find((info) => info.relationshipId === relId);\n        if (!info) {\n            throw new Error(\"Cannot find corresponding workbook sheet\");\n        }\n        return info;\n    }\n    extractConditionalFormats() {\n        return new XlsxCfExtractor(this.rootFile, this.xlsxFileStructure, this.warningManager, this.theme).extractConditionalFormattings();\n    }\n    extractFigures(worksheet) {\n        const figures = this.mapOnElements({ parent: worksheet, query: \"drawing\" }, (drawingElement) => {\n            const drawingId = this.extractAttr(drawingElement, \"r:id\", { required: true })?.asString();\n            const drawingFile = this.getTargetXmlFile(this.relationships[drawingId]);\n            const figures = new XlsxFigureExtractor(drawingFile, this.xlsxFileStructure, this.warningManager).extractFigures();\n            return figures;\n        })[0];\n        return figures || [];\n    }\n    extractTables(worksheet) {\n        return this.mapOnElements({ query: \"tablePart\", parent: worksheet }, (tablePartElement) => {\n            const tableId = this.extractAttr(tablePartElement, \"r:id\", { required: true })?.asString();\n            const tableFile = this.getTargetXmlFile(this.relationships[tableId]);\n            const tableExtractor = new XlsxTableExtractor(tableFile, this.xlsxFileStructure, this.warningManager);\n            return tableExtractor.getTable();\n        });\n    }\n    extractPivotTables() {\n        try {\n            return Object.values(this.relationships)\n                .filter((relationship) => relationship.type.endsWith(\"pivotTable\"))\n                .map((pivotRelationship) => {\n                const pivotFile = this.getTargetXmlFile(pivotRelationship);\n                const pivot = new XlsxPivotExtractor(pivotFile, this.xlsxFileStructure, this.warningManager).getPivotTable();\n                return pivot;\n            });\n        }\n        catch (e) {\n            this.catchErrorOnElement(e);\n            return [];\n        }\n    }\n    extractMerges(worksheet) {\n        return this.mapOnElements({ parent: worksheet, query: \"mergeCell\" }, (mergeElement) => {\n            return this.extractAttr(mergeElement, \"ref\", { required: true }).asString();\n        });\n    }\n    extractSheetFormat(worksheet) {\n        const formatElement = this.querySelector(worksheet, \"sheetFormatPr\");\n        if (!formatElement)\n            return undefined;\n        return {\n            defaultColWidth: this.extractAttr(formatElement, \"defaultColWidth\", {\n                default: EXCEL_DEFAULT_COL_WIDTH.toString(),\n            }).asNum(),\n            defaultRowHeight: this.extractAttr(formatElement, \"defaultRowHeight\", {\n                default: EXCEL_DEFAULT_ROW_HEIGHT.toString(),\n            }).asNum(),\n        };\n    }\n    extractSheetProperties(worksheet) {\n        const propertiesElement = this.querySelector(worksheet, \"sheetPr\");\n        if (!propertiesElement)\n            return undefined;\n        return {\n            outlinePr: this.extractSheetOutlineProperties(propertiesElement),\n            tabColor: this.extractColor(this.querySelector(propertiesElement, \"tabColor\"), this.theme),\n        };\n    }\n    extractSheetOutlineProperties(sheetProperties) {\n        const properties = this.querySelector(sheetProperties, \"outlinePr\");\n        if (!properties)\n            return undefined;\n        return {\n            summaryBelow: this.extractAttr(properties, \"summaryBelow\", { default: true }).asBool(),\n            summaryRight: this.extractAttr(properties, \"summaryRight\", { default: true }).asBool(),\n        };\n    }\n    extractCols(worksheet) {\n        return this.mapOnElements({ parent: worksheet, query: \"cols col\" }, (colElement) => {\n            return {\n                width: this.extractAttr(colElement, \"width\")?.asNum(),\n                customWidth: this.extractAttr(colElement, \"customWidth\")?.asBool(),\n                bestFit: this.extractAttr(colElement, \"bestFit\")?.asBool(),\n                hidden: this.extractAttr(colElement, \"hidden\")?.asBool(),\n                min: this.extractAttr(colElement, \"min\", { required: true })?.asNum(),\n                max: this.extractAttr(colElement, \"max\", { required: true })?.asNum(),\n                styleIndex: this.extractAttr(colElement, \"style\")?.asNum(),\n                outlineLevel: this.extractAttr(colElement, \"outlineLevel\")?.asNum(),\n                collapsed: this.extractAttr(colElement, \"collapsed\")?.asBool(),\n            };\n        });\n    }\n    extractRows(worksheet) {\n        return this.mapOnElements({ parent: worksheet, query: \"sheetData row\" }, (rowElement) => {\n            return {\n                index: this.extractAttr(rowElement, \"r\", { required: true })?.asNum(),\n                cells: this.extractCells(rowElement),\n                height: this.extractAttr(rowElement, \"ht\")?.asNum(),\n                customHeight: this.extractAttr(rowElement, \"customHeight\")?.asBool(),\n                hidden: this.extractAttr(rowElement, \"hidden\")?.asBool(),\n                styleIndex: this.extractAttr(rowElement, \"s\")?.asNum(),\n                outlineLevel: this.extractAttr(rowElement, \"outlineLevel\")?.asNum(),\n                collapsed: this.extractAttr(rowElement, \"collapsed\")?.asBool(),\n            };\n        });\n    }\n    extractCells(row) {\n        return this.mapOnElements({ parent: row, query: \"c\" }, (cellElement) => {\n            return {\n                xc: this.extractAttr(cellElement, \"r\", { required: true })?.asString(),\n                styleIndex: this.extractAttr(cellElement, \"s\")?.asNum(),\n                type: CELL_TYPE_CONVERSION_MAP[this.extractAttr(cellElement, \"t\", { default: \"n\" })?.asString()],\n                value: this.extractChildTextContent(cellElement, \"v\"),\n                formula: this.extractCellFormula(cellElement),\n            };\n        });\n    }\n    extractCellFormula(cellElement) {\n        const formulaElement = this.querySelector(cellElement, \"f\");\n        if (!formulaElement)\n            return undefined;\n        return {\n            content: this.extractTextContent(formulaElement),\n            sharedIndex: this.extractAttr(formulaElement, \"si\")?.asNum(),\n            ref: this.extractAttr(formulaElement, \"ref\")?.asString(),\n        };\n    }\n    extractHyperLinks(worksheet) {\n        return this.mapOnElements({ parent: worksheet, query: \"hyperlink\" }, (linkElement) => {\n            const relId = this.extractAttr(linkElement, \"r:id\")?.asString();\n            return {\n                xc: this.extractAttr(linkElement, \"ref\", { required: true })?.asString(),\n                location: this.extractAttr(linkElement, \"location\")?.asString(),\n                display: this.extractAttr(linkElement, \"display\")?.asString(),\n                relTarget: relId ? this.relationships[relId].target : undefined,\n            };\n        });\n    }\n    extractSharedFormulas(worksheet) {\n        const sfElements = this.querySelectorAll(worksheet, `f[si][ref]`);\n        const sfMap = {};\n        for (let sfElement of sfElements) {\n            const index = this.extractAttr(sfElement, \"si\", { required: true }).asNum();\n            const formula = this.extractTextContent(sfElement, { required: true });\n            sfMap[index] = formula;\n        }\n        const sfs = [];\n        for (let i = 0; i < Object.keys(sfMap).length; i++) {\n            if (!sfMap[i]) {\n                this.warningManager.addParsingWarning(`Missing shared formula ${i}, replacing it by empty formula`);\n                sfs.push(\"\");\n            }\n            else {\n                sfs.push(sfMap[i]);\n            }\n        }\n        return sfs;\n    }\n}\n\nclass XlsxStyleExtractor extends XlsxBaseExtractor {\n    theme;\n    constructor(xlsxStructure, warningManager, theme) {\n        super(xlsxStructure.styles, xlsxStructure, warningManager);\n        this.theme = theme;\n    }\n    getNumFormats() {\n        return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"numFmt\" }, (numFmtElement) => {\n            return this.extractNumFormats(numFmtElement);\n        });\n    }\n    extractNumFormats(numFmtElement) {\n        return {\n            id: this.extractAttr(numFmtElement, \"numFmtId\", {\n                required: true,\n            }).asNum(),\n            format: this.extractAttr(numFmtElement, \"formatCode\", {\n                required: true,\n                default: \"\",\n            }).asString(),\n        };\n    }\n    getFonts() {\n        return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"font\" }, (font) => {\n            return this.extractFont(font);\n        });\n    }\n    extractFont(fontElement) {\n        const name = this.extractChildAttr(fontElement, \"name\", \"val\", {\n            default: \"Arial\",\n        }).asString();\n        const size = this.extractChildAttr(fontElement, \"sz\", \"val\", {\n            default: DEFAULT_FONT_SIZE.toString(),\n        }).asNum();\n        const color = this.extractColor(this.querySelector(fontElement, `color`), this.theme);\n        // The behavior for these is kinda strange. The text is italic if there is either a \"italic\" tag with no \"val\"\n        // attribute, or a tag with a \"val\" attribute = \"1\" (boolean).\n        const italicElement = this.querySelector(fontElement, `i`) || undefined;\n        const italic = italicElement && italicElement.attributes[\"val\"]?.value !== \"0\";\n        const boldElement = this.querySelector(fontElement, `b`) || undefined;\n        const bold = boldElement && boldElement.attributes[\"val\"]?.value !== \"0\";\n        const strikeElement = this.querySelector(fontElement, `strike`) || undefined;\n        const strike = strikeElement && strikeElement.attributes[\"val\"]?.value !== \"0\";\n        const underlineElement = this.querySelector(fontElement, `u`) || undefined;\n        const underline = underlineElement && underlineElement.attributes[\"val\"]?.value !== \"none\";\n        return { name, size, color, italic, bold, underline, strike };\n    }\n    getFills() {\n        return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"fill\" }, (fillElement) => {\n            return this.extractFill(fillElement);\n        });\n    }\n    extractFill(fillElement) {\n        // Fills are either patterns of gradients\n        const fillChild = fillElement.children[0];\n        if (fillChild.tagName === \"patternFill\") {\n            return {\n                patternType: fillChild.attributes[\"patternType\"]?.value,\n                bgColor: this.extractColor(this.querySelector(fillChild, \"bgColor\"), this.theme),\n                fgColor: this.extractColor(this.querySelector(fillChild, \"fgColor\"), this.theme),\n            };\n        }\n        else {\n            // We don't support gradients. Take the second gradient color as fill color\n            return {\n                patternType: \"solid\",\n                fgColor: this.extractColor(this.querySelectorAll(fillChild, \"color\")[1], this.theme),\n            };\n        }\n    }\n    getBorders() {\n        return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"border\" }, (borderElement) => {\n            return this.extractBorder(borderElement);\n        });\n    }\n    extractBorder(borderElement) {\n        const border = {\n            left: this.extractSingleBorder(borderElement, \"left\", this.theme),\n            right: this.extractSingleBorder(borderElement, \"right\", this.theme),\n            top: this.extractSingleBorder(borderElement, \"top\", this.theme),\n            bottom: this.extractSingleBorder(borderElement, \"bottom\", this.theme),\n            diagonal: this.extractSingleBorder(borderElement, \"diagonal\", this.theme),\n        };\n        if (border.diagonal) {\n            border.diagonalUp = this.extractAttr(borderElement, \"diagonalUp\")?.asBool();\n            border.diagonalDown = this.extractAttr(borderElement, \"diagonalDown\")?.asBool();\n        }\n        return border;\n    }\n    extractSingleBorder(borderElement, direction, theme) {\n        const directionElement = this.querySelector(borderElement, direction);\n        if (!directionElement || !directionElement.attributes[\"style\"])\n            return undefined;\n        return {\n            style: this.extractAttr(directionElement, \"style\", {\n                required: true,\n                default: \"thin\",\n            }).asString(),\n            color: this.extractColor(directionElement.children[0], theme, \"000000\"),\n        };\n    }\n    extractAlignment(alignmentElement) {\n        return {\n            horizontal: this.extractAttr(alignmentElement, \"horizontal\", {\n                default: \"general\",\n            }).asString(),\n            vertical: this.extractAttr(alignmentElement, \"vertical\", {\n                default: \"bottom\",\n            }).asString(),\n            textRotation: this.extractAttr(alignmentElement, \"textRotation\")?.asNum(),\n            wrapText: this.extractAttr(alignmentElement, \"wrapText\")?.asBool(),\n            indent: this.extractAttr(alignmentElement, \"indent\")?.asNum(),\n            relativeIndent: this.extractAttr(alignmentElement, \"relativeIndent\")?.asNum(),\n            justifyLastLine: this.extractAttr(alignmentElement, \"justifyLastLine\")?.asBool(),\n            shrinkToFit: this.extractAttr(alignmentElement, \"shrinkToFit\")?.asBool(),\n            readingOrder: this.extractAttr(alignmentElement, \"readingOrder\")?.asNum(),\n        };\n    }\n    getDxfs() {\n        return this.mapOnElements({ query: \"dxf\", parent: this.rootFile.file.xml }, (dxfElement) => {\n            const fontElement = this.querySelector(dxfElement, \"font\");\n            const fillElement = this.querySelector(dxfElement, \"fill\");\n            const borderElement = this.querySelector(dxfElement, \"border\");\n            const numFmtElement = this.querySelector(dxfElement, \"numFmt\");\n            const alignmentElement = this.querySelector(dxfElement, \"alignment\");\n            return {\n                font: fontElement ? this.extractFont(fontElement) : undefined,\n                fill: fillElement ? this.extractFill(fillElement) : undefined,\n                numFmt: numFmtElement ? this.extractNumFormats(numFmtElement) : undefined,\n                alignment: alignmentElement ? this.extractAlignment(alignmentElement) : undefined,\n                border: borderElement ? this.extractBorder(borderElement) : undefined,\n            };\n        });\n    }\n    getStyles() {\n        return this.mapOnElements({ query: \"cellXfs xf\", parent: this.rootFile.file.xml }, (styleElement) => {\n            const alignmentElement = this.querySelector(styleElement, \"alignment\");\n            return {\n                fontId: this.extractAttr(styleElement, \"fontId\", {\n                    required: true,\n                    default: 0,\n                }).asNum(),\n                fillId: this.extractAttr(styleElement, \"fillId\", {\n                    required: true,\n                    default: 0,\n                }).asNum(),\n                borderId: this.extractAttr(styleElement, \"borderId\", {\n                    required: true,\n                    default: 0,\n                }).asNum(),\n                numFmtId: this.extractAttr(styleElement, \"numFmtId\", {\n                    required: true,\n                    default: 0,\n                }).asNum(),\n                alignment: alignmentElement ? this.extractAlignment(alignmentElement) : undefined,\n            };\n        });\n    }\n}\n\nclass XlsxExternalBookExtractor extends XlsxBaseExtractor {\n    getExternalBook() {\n        return this.mapOnElements({ parent: this.rootFile.file.xml, query: \"externalBook\" }, (bookElement) => {\n            return {\n                rId: this.extractAttr(bookElement, \"r:id\", { required: true }).asString(),\n                sheetNames: this.mapOnElements({ parent: bookElement, query: \"sheetName\" }, (sheetNameElement) => {\n                    return this.extractAttr(sheetNameElement, \"val\", { required: true }).asString();\n                }),\n                datasets: this.extractExternalSheetData(bookElement),\n            };\n        })[0];\n    }\n    extractExternalSheetData(externalBookElement) {\n        return this.mapOnElements({ parent: externalBookElement, query: \"sheetData\" }, (sheetDataElement) => {\n            const cellsData = this.mapOnElements({ parent: sheetDataElement, query: \"cell\" }, (cellElement) => {\n                return {\n                    xc: this.extractAttr(cellElement, \"r\", { required: true }).asString(),\n                    value: this.extractChildTextContent(cellElement, \"v\", { required: true }),\n                };\n            });\n            const dataMap = {};\n            for (let cell of cellsData) {\n                dataMap[cell.xc] = cell.value;\n            }\n            return {\n                sheetId: this.extractAttr(sheetDataElement, \"sheetId\", { required: true }).asNum(),\n                data: dataMap,\n            };\n        });\n    }\n}\n\n/**\n * Return all the xmls converted to XLSXImportFile corresponding to the given content type.\n */\nfunction getXLSXFilesOfType(contentType, xmls) {\n    const paths = getPathsOfContent(contentType, xmls);\n    return getXlsxFile(paths, xmls);\n}\n/**\n * From an array of file path, return the equivalents XLSXFiles. An XLSX File is composed of an XML,\n * and optionally of a relationships XML.\n */\nfunction getXlsxFile(files, xmls) {\n    const ret = [];\n    for (let file of files) {\n        const rels = getRelationFile(file, xmls);\n        ret.push({\n            file: { fileName: file, xml: xmls[file] },\n            rels: rels ? { fileName: rels, xml: xmls[rels] } : undefined,\n        });\n    }\n    return ret;\n}\n/**\n * Return all the path of the files in a XLSX directory that have content of the given type.\n */\nfunction getPathsOfContent(contentType, xmls) {\n    const xml = xmls[CONTENT_TYPES_FILE];\n    const sheetItems = xml.querySelectorAll(`Override[ContentType=\"${contentType}\"]`);\n    const paths = [];\n    for (let item of sheetItems) {\n        const file = item?.attributes[\"PartName\"].value;\n        paths.push(file.substring(1)); // Remove the heading \"/\"\n    }\n    return paths;\n}\n/**\n * Get the corresponding relationship file for a given xml file in a XLSX directory.\n */\nfunction getRelationFile(file, xmls) {\n    if (file === CONTENT_TYPES_FILE) {\n        return \"_rels/.rels\";\n    }\n    let relsFile = \"\";\n    const pathParts = file.split(\"/\");\n    for (let i = 0; i < pathParts.length - 1; i++) {\n        relsFile += pathParts[i] + \"/\";\n    }\n    relsFile += \"_rels/\";\n    relsFile += pathParts[pathParts.length - 1] + \".rels\";\n    if (!xmls[relsFile]) {\n        relsFile = undefined;\n    }\n    return relsFile;\n}\n\nconst EXCEL_IMPORT_VERSION = 21;\nclass XlsxReader {\n    warningManager;\n    xmls;\n    images;\n    constructor(files) {\n        this.warningManager = new XLSXImportWarningManager();\n        this.xmls = {};\n        this.images = [];\n        for (let key of Object.keys(files)) {\n            // Random files can be in xlsx (like a bin file for printer settings)\n            if (key.endsWith(\".xml\") || key.endsWith(\".rels\")) {\n                const contentString = escapeTagNamespaces(files[key]);\n                this.xmls[key] = parseXML(new XMLString(contentString));\n            }\n            else if (key.includes(\"media/image\")) {\n                this.images.push({\n                    fileName: key,\n                    imageSrc: files[key][\"imageSrc\"],\n                });\n            }\n        }\n    }\n    convertXlsx() {\n        const xlsxData = this.getXlsxData();\n        const convertedData = this.convertImportedData(xlsxData);\n        return convertedData;\n    }\n    // ---------------------------------------------------------------------------\n    // Parsing XMLs\n    // ---------------------------------------------------------------------------\n    getXlsxData() {\n        const xlsxFileStructure = this.buildXlsxFileStructure();\n        const theme = xlsxFileStructure.theme\n            ? new XlsxMiscExtractor(xlsxFileStructure.theme, xlsxFileStructure, this.warningManager).getTheme()\n            : undefined;\n        const sharedStrings = xlsxFileStructure.sharedStrings\n            ? new XlsxMiscExtractor(xlsxFileStructure.sharedStrings, xlsxFileStructure, this.warningManager).getSharedStrings()\n            : [];\n        // Sort sheets by file name : the sheets will always be named sheet1.xml, sheet2.xml, ... in order\n        const sheets = xlsxFileStructure.sheets\n            .sort((a, b) => a.file.fileName.localeCompare(b.file.fileName, undefined, { numeric: true }))\n            .map((sheetFile) => {\n            return new XlsxSheetExtractor(sheetFile, xlsxFileStructure, this.warningManager, theme).getSheet();\n        });\n        const externalBooks = xlsxFileStructure.externalLinks.map((externalLinkFile) => {\n            return new XlsxExternalBookExtractor(externalLinkFile, xlsxFileStructure, this.warningManager).getExternalBook();\n        });\n        const styleExtractor = new XlsxStyleExtractor(xlsxFileStructure, this.warningManager, theme);\n        return {\n            fonts: styleExtractor.getFonts(),\n            fills: styleExtractor.getFills(),\n            borders: styleExtractor.getBorders(),\n            dxfs: styleExtractor.getDxfs(),\n            numFmts: styleExtractor.getNumFormats(),\n            styles: styleExtractor.getStyles(),\n            sheets: sheets,\n            sharedStrings,\n            externalBooks,\n        };\n    }\n    buildXlsxFileStructure() {\n        const xlsxFileStructure = {\n            sheets: getXLSXFilesOfType(CONTENT_TYPES.sheet, this.xmls),\n            workbook: getXLSXFilesOfType(CONTENT_TYPES.workbook, this.xmls)[0],\n            styles: getXLSXFilesOfType(CONTENT_TYPES.styles, this.xmls)[0],\n            sharedStrings: getXLSXFilesOfType(CONTENT_TYPES.sharedStrings, this.xmls)[0],\n            theme: getXLSXFilesOfType(CONTENT_TYPES.themes, this.xmls)[0],\n            charts: getXLSXFilesOfType(CONTENT_TYPES.chart, this.xmls),\n            figures: getXLSXFilesOfType(CONTENT_TYPES.drawing, this.xmls),\n            tables: getXLSXFilesOfType(CONTENT_TYPES.table, this.xmls),\n            pivots: getXLSXFilesOfType(CONTENT_TYPES.pivot, this.xmls),\n            externalLinks: getXLSXFilesOfType(CONTENT_TYPES.externalLink, this.xmls),\n            images: this.images,\n        };\n        if (!xlsxFileStructure.workbook.rels) {\n            throw Error(_t(\"Cannot find workbook relations file\"));\n        }\n        return xlsxFileStructure;\n    }\n    // ---------------------------------------------------------------------------\n    // Conversion\n    // ---------------------------------------------------------------------------\n    convertImportedData(data) {\n        const convertedData = {\n            version: EXCEL_IMPORT_VERSION,\n            sheets: convertSheets(data, this.warningManager),\n            styles: convertStyles(data, this.warningManager),\n            formats: convertFormats(data, this.warningManager),\n            borders: convertBorders(data, this.warningManager),\n            revisionId: DEFAULT_REVISION_ID,\n        };\n        convertTables(convertedData, data);\n        // Remove falsy attributes in styles. Not mandatory, but make objects more readable when debugging\n        Object.keys(data.styles).map((key) => {\n            data.styles[key] = removeFalsyAttributes(data.styles[key]);\n        });\n        return convertedData;\n    }\n}\n\n/**\n * parses a formula (as a string) into the same formula,\n * but with the references to other cells extracted\n *\n * =sum(a3:b1) + c3 --> =sum(|0|) + |1|\n *\n * @param formula\n */\nfunction normalizeV9(formula) {\n    const tokens = rangeTokenize(formula);\n    let dependencies = [];\n    let noRefFormula = \"\".concat(...tokens.map((token) => {\n        if (token.type === \"REFERENCE\" && cellReference.test(token.value)) {\n            const value = token.value.trim();\n            if (!dependencies.includes(value)) {\n                dependencies.push(value);\n            }\n            return `${FORMULA_REF_IDENTIFIER}${dependencies.indexOf(value)}${FORMULA_REF_IDENTIFIER}`;\n        }\n        else {\n            return token.value;\n        }\n    }));\n    return { text: noRefFormula, dependencies };\n}\n\n// FIXME: Remove this map when Firefox supports getWeekInfo\n// https:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo\nconst WEEK_START = {\n    am_ET: 7,\n    ar_001: 6,\n    ar_SY: 6,\n    az_AZ: 1,\n    eu_ES: 1,\n    be_BY: 1,\n    bn_IN: 1,\n    bs_BA: 1,\n    bg_BG: 1,\n    ca_ES: 1,\n    zh_CN: 7,\n    zh_HK: 7,\n    zh_TW: 7,\n    hr_HR: 1,\n    cs_CZ: 1,\n    da_DK: 1,\n    nl_BE: 1,\n    nl_NL: 1,\n    en_AU: 7,\n    en_CA: 7,\n    en_GB: 1,\n    en_IN: 7,\n    en_NZ: 7,\n    et_EE: 1,\n    fi_FI: 1,\n    fr_BE: 1,\n    fr_CA: 7,\n    fr_CH: 1,\n    fr_FR: 1,\n    gl_ES: 1,\n    ka_GE: 1,\n    de_DE: 1,\n    de_CH: 1,\n    el_GR: 1,\n    gu_IN: 7,\n    he_IL: 7,\n    hi_IN: 7,\n    hu_HU: 1,\n    id_ID: 7,\n    it_IT: 1,\n    ja_JP: 7,\n    kab_DZ: 6,\n    km_KH: 7,\n    ko_KP: 1,\n    ko_KR: 7,\n    lo_LA: 7,\n    lv_LV: 1,\n    lt_LT: 1,\n    lb_LU: 1,\n    mk_MK: 1,\n    ml_IN: 1,\n    mn_MN: 7,\n    ms_MY: 1,\n    nb_NO: 1,\n    fa_IR: 6,\n    pl_PL: 1,\n    pt_AO: 1,\n    pt_BR: 7,\n    pt_PT: 1,\n    ro_RO: 1,\n    ru_RU: 1,\n    sr_RS: 7,\n    \"sr@latin\": 7,\n    sk_SK: 1,\n    sl_SI: 1,\n    es_AR: 7,\n    es_BO: 1,\n    es_CL: 1,\n    es_CO: 7,\n    es_CR: 1,\n    es_DO: 1,\n    es_EC: 1,\n    es_GT: 7,\n    es_MX: 7,\n    es_PA: 7,\n    es_PE: 7,\n    es_PY: 7,\n    es_UY: 1,\n    es_VE: 7,\n    sw: 1,\n    sv_SE: 1,\n    th_TH: 7,\n    tl_PH: 1,\n    tr_TR: 1,\n    uk_UA: 1,\n    vi_VN: 1,\n    sq_AL: 1,\n    te_IN: 7,\n    en_US: 7,\n    my_MM: 7,\n    es_ES: 1,\n    es_419: 1,\n};\n\nconst migrationStepRegistry = new Registry();\nmigrationStepRegistry\n    .add(\"migration_1\", {\n    // add the `activeSheet` field on data\n    versionFrom: \"1\",\n    migrate(data) {\n        if (data.sheets && data.sheets[0]) {\n            data.activeSheet = data.sheets[0].name;\n        }\n        return data;\n    },\n})\n    .add(\"migration_2\", {\n    // add an id field in each sheet\n    versionFrom: \"2\",\n    migrate(data) {\n        if (data.sheets && data.sheets.length) {\n            for (let sheet of data.sheets) {\n                sheet.id = sheet.id || sheet.name;\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_3\", {\n    // activeSheet is now an id, not the name of a sheet\n    versionFrom: \"3\",\n    migrate(data) {\n        if (data.sheets && data.activeSheet) {\n            const activeSheet = data.sheets.find((s) => s.name === data.activeSheet);\n            data.activeSheet = activeSheet.id;\n        }\n        return data;\n    },\n})\n    .add(\"migration_4\", {\n    // add figures object in each sheets\n    versionFrom: \"4\",\n    migrate(data) {\n        for (let sheet of data.sheets || []) {\n            sheet.figures = sheet.figures || [];\n        }\n        return data;\n    },\n})\n    .add(\"migration_5\", {\n    // normalize the content of the cell if it is a formula to avoid parsing all the formula that vary only by the cells they use\n    versionFrom: \"5\",\n    migrate(data) {\n        for (let sheet of data.sheets || []) {\n            for (let xc in sheet.cells || []) {\n                const cell = sheet.cells[xc];\n                if (cell.content && cell.content.startsWith(\"=\")) {\n                    cell.formula = normalizeV9(cell.content);\n                }\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_6\", {\n    // transform chart data structure\n    versionFrom: \"6\",\n    migrate(data) {\n        for (let sheet of data.sheets || []) {\n            for (let f in sheet.figures || []) {\n                const { dataSets, ...newData } = sheet.figures[f].data;\n                const newDataSets = [];\n                for (let ds of dataSets) {\n                    if (ds.labelCell) {\n                        const dataRange = toZone(ds.dataRange);\n                        const newRange = ds.labelCell + \":\" + toXC(dataRange.right, dataRange.bottom);\n                        newDataSets.push(newRange);\n                    }\n                    else {\n                        newDataSets.push(ds.dataRange);\n                    }\n                }\n                newData.dataSetsHaveTitle = Boolean(dataSets[0].labelCell);\n                newData.dataSets = newDataSets;\n                sheet.figures[f].data = newData;\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_7\", {\n    // remove single quotes in sheet names\n    versionFrom: \"7\",\n    migrate(data) {\n        const namesTaken = [];\n        for (let sheet of data.sheets || []) {\n            if (!sheet.name) {\n                continue;\n            }\n            const oldName = sheet.name;\n            const escapedName = sanitizeSheetName(oldName, \"_\");\n            let i = 1;\n            let newName = escapedName;\n            while (namesTaken.includes(newName)) {\n                newName = `${escapedName}${i}`;\n                i++;\n            }\n            sheet.name = newName;\n            namesTaken.push(newName);\n            const replaceName = (str) => {\n                if (str === undefined) {\n                    return str;\n                }\n                // replaceAll is only available in next Typescript version\n                let newString = str.replace(oldName, newName);\n                let currentString = str;\n                while (currentString !== newString) {\n                    currentString = newString;\n                    newString = currentString.replace(oldName, newName);\n                }\n                return currentString;\n            };\n            //cells\n            for (let xc in sheet.cells) {\n                const cell = sheet.cells[xc];\n                if (cell.formula) {\n                    cell.formula.dependencies = cell.formula.dependencies.map(replaceName);\n                }\n            }\n            //charts\n            for (let figure of sheet.figures || []) {\n                if (figure.type === \"chart\") {\n                    const dataSets = figure.data.dataSets.map(replaceName);\n                    const labelRange = replaceName(figure.data.labelRange);\n                    figure.data = { ...figure.data, dataSets, labelRange };\n                }\n            }\n            //ConditionalFormats\n            for (let cf of sheet.conditionalFormats || []) {\n                cf.ranges = cf.ranges.map(replaceName);\n                for (const thresholdName of [\n                    \"minimum\",\n                    \"maximum\",\n                    \"midpoint\",\n                    \"upperInflectionPoint\",\n                    \"lowerInflectionPoint\",\n                ]) {\n                    if (cf.rule[thresholdName]?.type === \"formula\") {\n                        cf.rule[thresholdName].value = replaceName(cf.rule[thresholdName].value);\n                    }\n                }\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_8\", {\n    // transform chart data structure with design attributes\n    versionFrom: \"8\",\n    migrate(data) {\n        for (const sheet of data.sheets || []) {\n            for (const chart of sheet.figures || []) {\n                chart.data.background = BACKGROUND_CHART_COLOR;\n                chart.data.verticalAxisPosition = \"left\";\n                chart.data.legendPosition = \"top\";\n                chart.data.stacked = false;\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_9\", {\n    // de-normalize formula to reduce exported json size (~30%)\n    versionFrom: \"9\",\n    migrate(data) {\n        for (let sheet of data.sheets || []) {\n            for (let xc in sheet.cells || []) {\n                const cell = sheet.cells[xc];\n                if (cell.formula) {\n                    let { text, dependencies } = cell.formula;\n                    for (let [index, d] of Object.entries(dependencies)) {\n                        const stringPosition = `\\\\${FORMULA_REF_IDENTIFIER}${index}\\\\${FORMULA_REF_IDENTIFIER}`;\n                        text = text.replace(new RegExp(stringPosition, \"g\"), d);\n                    }\n                    cell.content = text;\n                    delete cell.formula;\n                }\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_10\", {\n    // normalize the formats of the cells\n    versionFrom: \"10\",\n    migrate(data) {\n        const formats = {};\n        for (let sheet of data.sheets || []) {\n            for (let xc in sheet.cells || []) {\n                const cell = sheet.cells[xc];\n                if (cell.format) {\n                    cell.format = getItemId(cell.format, formats);\n                }\n            }\n        }\n        data.formats = formats;\n        return data;\n    },\n})\n    .add(\"migration_11\", {\n    // Add isVisible to sheets\n    versionFrom: \"11\",\n    migrate(data) {\n        for (let sheet of data.sheets || []) {\n            sheet.isVisible = true;\n        }\n        return data;\n    },\n})\n    .add(\"migration_12\", {\n    // Fix data filter duplication\n    versionFrom: \"12\",\n    migrate(data) {\n        return fixOverlappingFilters(data);\n    },\n})\n    .add(\"migration_12_5\", {\n    // Change Border description structure\n    versionFrom: \"12.5\",\n    migrate(data) {\n        for (const borderId in data.borders) {\n            const border = data.borders[borderId];\n            for (const position in border) {\n                if (Array.isArray(border[position])) {\n                    border[position] = {\n                        style: border[position][0],\n                        color: border[position][1],\n                    };\n                }\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_13\", {\n    // Add locale to spreadsheet settings\n    versionFrom: \"13\",\n    migrate(data) {\n        if (!data.settings) {\n            data.settings = {};\n        }\n        if (!data.settings.locale) {\n            data.settings.locale = DEFAULT_LOCALE;\n        }\n        return data;\n    },\n})\n    .add(\"migration_14\", {\n    // Fix datafilter duplication (post saas-17.1)\n    versionFrom: \"14\",\n    migrate(data) {\n        return fixOverlappingFilters(data);\n    },\n})\n    .add(\"migration_14_5\", {\n    // Rename filterTable to tables\n    versionFrom: \"14.5\",\n    migrate(data) {\n        for (const sheetData of data.sheets || []) {\n            sheetData.tables = sheetData.tables || sheetData.filterTables || [];\n            delete sheetData.filterTables;\n        }\n        return data;\n    },\n})\n    .add(\"migration_15\", {\n    // Add pivots\n    versionFrom: \"15\",\n    migrate(data) {\n        if (!data.pivots) {\n            data.pivots = {};\n        }\n        if (!data.pivotNextId) {\n            data.pivotNextId = getMaxObjectId(data.pivots) + 1;\n        }\n        return data;\n    },\n})\n    .add(\"migration_16\", {\n    // transform chart data structure (2)\n    versionFrom: \"16\",\n    migrate(data) {\n        for (const sheet of data.sheets || []) {\n            for (const f in sheet.figures || []) {\n                const figure = sheet.figures[f];\n                if (\"title\" in figure.data && typeof figure.data.title === \"string\") {\n                    figure.data.title = { text: figure.data.title };\n                }\n                const figureType = figure.data.type;\n                if (![\"line\", \"bar\", \"pie\", \"scatter\", \"waterfall\", \"combo\"].includes(figureType)) {\n                    continue;\n                }\n                const { dataSets, ...newData } = sheet.figures[f].data;\n                const newDataSets = dataSets.map((dataRange) => ({ dataRange }));\n                newData.dataSets = newDataSets;\n                sheet.figures[f].data = newData;\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_17\", {\n    // Empty migration to allow external modules to add their own migration steps\n    // before this version\n    versionFrom: \"17\",\n    migrate(data) {\n        return data;\n    },\n})\n    .add(\"migration_18\", {\n    // Change measures and dimensions `name` to `fieldName`\n    // Add id to measures\n    versionFrom: \"18\",\n    migrate(data) {\n        for (const pivot of Object.values(data.pivots || {})) {\n            pivot.measures = pivot.measures.map((measure) => ({\n                id: measure.name, //Do not set name + aggregator, to support old formulas\n                fieldName: measure.name,\n                aggregator: measure.aggregator,\n            }));\n            pivot.columns = pivot.columns.map((column) => ({\n                fieldName: column.name,\n                order: column.order,\n                granularity: column.granularity,\n            }));\n            pivot.rows = pivot.rows.map((row) => ({\n                fieldName: row.name,\n                order: row.order,\n                granularity: row.granularity,\n            }));\n        }\n        return data;\n    },\n})\n    .add(\"migration_19\", {\n    // \"Add weekStart to locale\",\n    versionFrom: \"19\",\n    migrate(data) {\n        const locale = data.settings?.locale;\n        if (locale) {\n            const code = locale.code;\n            locale.weekStart = WEEK_START[code] || 1; // Default to Monday;\n        }\n        return data;\n    },\n})\n    .add(\"migration_20\", {\n    // group style and format into zones,\n    versionFrom: \"20\",\n    migrate(data) {\n        for (const sheet of data.sheets || []) {\n            sheet.styles = {};\n            sheet.formats = {};\n            sheet.borders = {};\n            for (const xc in sheet.cells) {\n                sheet.styles[xc] = sheet.cells[xc].style;\n                sheet.formats[xc] = sheet.cells[xc].format;\n                sheet.borders[xc] = sheet.cells[xc].border;\n                delete sheet.cells[xc].style;\n                delete sheet.cells[xc].format;\n                delete sheet.cells[xc].border;\n            }\n        }\n        return data;\n    },\n})\n    .add(\"migration_21\", {\n    // \"Add operator in gauge inflection points\",\n    versionFrom: \"21\",\n    migrate(data) {\n        for (const sheet of data.sheets || []) {\n            for (const figure of sheet.figures || []) {\n                if (figure.tag !== \"chart\" || figure.data.type !== \"gauge\") {\n                    continue;\n                }\n                const gaugeData = figure.data;\n                if (gaugeData?.sectionRule?.lowerInflectionPoint) {\n                    gaugeData.sectionRule.lowerInflectionPoint.operator = \"<=\";\n                }\n                if (gaugeData?.sectionRule?.upperInflectionPoint) {\n                    gaugeData.sectionRule.upperInflectionPoint.operator = \"<=\";\n                }\n            }\n        }\n        return data;\n    },\n});\nfunction fixOverlappingFilters(data) {\n    for (let sheet of data.sheets || []) {\n        let knownDataFilterZones = [];\n        for (let filterTable of sheet.filterTables || []) {\n            const zone = toZone(filterTable.range);\n            // See commit message of https://github.com/odoo/o-spreadsheet/pull/3632 of more details\n            const intersectZoneIndex = knownDataFilterZones.findIndex((knownZone) => overlap(knownZone, zone));\n            if (intersectZoneIndex !== -1) {\n                knownDataFilterZones[intersectZoneIndex] = zone;\n            }\n            else {\n                knownDataFilterZones.push(zone);\n            }\n        }\n        sheet.filterTables = knownDataFilterZones.map((zone) => ({\n            range: zoneToXc(zone),\n        }));\n    }\n    return data;\n}\n\n/**\n * This is the current state version number. It should be incremented each time\n * a breaking change is made in the way the state is handled, and an upgrade\n * function should be defined\n */\nconst CURRENT_VERSION = 22;\nconst INITIAL_SHEET_ID = \"Sheet1\";\n/**\n * This function tries to load anything that could look like a valid\n * workbookData object. It applies any migrations, if needed, and return a\n * current, complete workbookData object.\n *\n * It also ensures that there is at least one sheet.\n */\nfunction load(data, verboseImport) {\n    if (!data) {\n        return createEmptyWorkbookData();\n    }\n    console.debug(\"### Loading data ###\");\n    const start = performance.now();\n    if (data[\"[Content_Types].xml\"]) {\n        const reader = new XlsxReader(data);\n        data = reader.convertXlsx();\n        if (verboseImport) {\n            for (let parsingError of reader.warningManager.warnings.sort()) {\n                console.warn(parsingError);\n            }\n        }\n    }\n    // apply migrations, if needed\n    if (\"version\" in data) {\n        if (data.version < CURRENT_VERSION) {\n            console.debug(\"Migrating data from version\", data.version);\n            data = migrate(data);\n        }\n    }\n    data = repairData(data);\n    console.debug(\"Data loaded in\", performance.now() - start, \"ms\");\n    console.debug(\"###\");\n    return data;\n}\n// -----------------------------------------------------------------------------\n// Migrations\n// -----------------------------------------------------------------------------\nfunction compareVersions(v1, v2) {\n    const version1 = v1.split(\".\").map(Number);\n    const version2 = v2.split(\".\").map(Number);\n    for (let i = 0; i < Math.max(version1.length, version2.length); i++) {\n        const part1 = version1[i] || 0;\n        const part2 = version2[i] || 0;\n        if (part1 > part2) {\n            return 1;\n        }\n        if (part1 < part2) {\n            return -1;\n        }\n    }\n    return 0;\n}\nfunction migrate(data) {\n    const start = performance.now();\n    const steps = migrationStepRegistry\n        .getAll()\n        .sort((a, b) => compareVersions(a.versionFrom, b.versionFrom));\n    const index = steps.findIndex((step) => step.versionFrom === data.version.toString());\n    for (let i = index; i < steps.length; i++) {\n        data = steps[i].migrate(data);\n    }\n    console.debug(\"Data migrated in\", performance.now() - start, \"ms\");\n    return data;\n}\n/**\n * This function is used to repair faulty data independently of the migration.\n */\nfunction repairData(data) {\n    data = forceUnicityOfFigure(data);\n    data = setDefaults(data);\n    return data;\n}\n/**\n * Force the unicity of figure ids accross sheets\n */\nfunction forceUnicityOfFigure(data) {\n    if (data.uniqueFigureIds) {\n        return data;\n    }\n    const figureIds = new Set();\n    const uuidGenerator = new UuidGenerator();\n    for (const sheet of data.sheets || []) {\n        for (const figure of sheet.figures || []) {\n            if (figureIds.has(figure.id)) {\n                figure.id += uuidGenerator.uuidv4();\n            }\n            figureIds.add(figure.id);\n        }\n    }\n    data.uniqueFigureIds = true;\n    return data;\n}\n/**\n * sanity check: try to fix missing fields/corrupted state by providing\n * sensible default values\n */\nfunction setDefaults(partialData) {\n    const data = Object.assign(createEmptyWorkbookData(), partialData, {\n        version: CURRENT_VERSION,\n    });\n    data.sheets = data.sheets\n        ? data.sheets.map((s, i) => Object.assign(createEmptySheet(`Sheet${i + 1}`, `Sheet${i + 1}`), s))\n        : [];\n    if (data.sheets.length === 0) {\n        data.sheets.push(createEmptySheet(INITIAL_SHEET_ID, \"Sheet1\"));\n    }\n    if (!isValidLocale(data.settings.locale)) {\n        data.settings.locale = DEFAULT_LOCALE;\n    }\n    return data;\n}\n/**\n * The goal of this function is to repair corrupted/wrong initial messages caused by\n * a bug.\n * The bug should obviously be fixed, but it's too late for existing spreadsheet.\n */\nfunction repairInitialMessages(data, initialMessages) {\n    initialMessages = fixTranslatedSheetIds(data, initialMessages);\n    initialMessages = dropCommands(initialMessages, \"SORT_CELLS\");\n    initialMessages = dropCommands(initialMessages, \"SET_DECIMAL\");\n    initialMessages = fixChartDefinitions(data, initialMessages);\n    return initialMessages;\n}\n/**\n * When the workbook data is originally empty, a new one is generated on-the-fly.\n * A bug caused the sheet id to be non-deterministic. The sheet id was propagated in\n * commands.\n * This function repairs initial commands with a wrong sheetId.\n */\nfunction fixTranslatedSheetIds(data, initialMessages) {\n    // the fix is only needed when the workbook is generated on-the-fly\n    if (Object.keys(data).length !== 0) {\n        return initialMessages;\n    }\n    const sheetIds = [];\n    const messages = [];\n    const fixSheetId = (cmd) => {\n        if (cmd.type === \"CREATE_SHEET\") {\n            sheetIds.push(cmd.sheetId);\n        }\n        else if (\"sheetId\" in cmd && !sheetIds.includes(cmd.sheetId)) {\n            return { ...cmd, sheetId: INITIAL_SHEET_ID };\n        }\n        return cmd;\n    };\n    for (const message of initialMessages) {\n        if (message.type === \"REMOTE_REVISION\") {\n            messages.push({\n                ...message,\n                commands: message.commands.map(fixSheetId),\n            });\n        }\n        else {\n            messages.push(message);\n        }\n    }\n    return messages;\n}\nfunction dropCommands(initialMessages, commandType) {\n    const messages = [];\n    for (const message of initialMessages) {\n        if (message.type === \"REMOTE_REVISION\") {\n            messages.push({\n                ...message,\n                commands: message.commands.filter((command) => command.type !== commandType),\n            });\n        }\n        else {\n            messages.push(message);\n        }\n    }\n    return messages;\n}\nfunction fixChartDefinitions(data, initialMessages) {\n    const messages = [];\n    const map = {};\n    for (const sheet of data.sheets || []) {\n        sheet.figures?.forEach((figure) => {\n            if (figure.tag === \"chart\") {\n                // chart definition\n                map[figure.id] = figure.data;\n            }\n        });\n    }\n    for (const message of initialMessages) {\n        if (message.type === \"REMOTE_REVISION\") {\n            const commands = [];\n            for (const cmd of message.commands) {\n                let command = cmd;\n                switch (cmd.type) {\n                    case \"CREATE_CHART\":\n                        map[cmd.id] = cmd.definition;\n                        break;\n                    case \"UPDATE_CHART\":\n                        if (!map[cmd.id]) {\n                            /** the chart does not exist on the map, it might have been created after a duplicate sheet.\n                             * We don't have access to the definition, so we skip the command.\n                             */\n                            console.log(`Fix chart definition: chart with id ${cmd.id} not found.`);\n                            continue;\n                        }\n                        const definition = map[cmd.id];\n                        const newDefinition = { ...definition, ...cmd.definition };\n                        command = { ...cmd, definition: newDefinition };\n                        map[cmd.id] = newDefinition;\n                        break;\n                }\n                commands.push(command);\n            }\n            messages.push({\n                ...message,\n                commands,\n            });\n        }\n        else {\n            messages.push(message);\n        }\n    }\n    return messages;\n}\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\nfunction createEmptySheet(sheetId, name) {\n    return {\n        id: sheetId,\n        name,\n        colNumber: 26,\n        rowNumber: 100,\n        cells: {},\n        styles: {},\n        formats: {},\n        borders: {},\n        cols: {},\n        rows: {},\n        merges: [],\n        conditionalFormats: [],\n        figures: [],\n        tables: [],\n        isVisible: true,\n    };\n}\nfunction createEmptyWorkbookData(sheetName = \"Sheet1\") {\n    const data = {\n        version: CURRENT_VERSION,\n        sheets: [createEmptySheet(INITIAL_SHEET_ID, sheetName)],\n        styles: {},\n        formats: {},\n        borders: {},\n        revisionId: DEFAULT_REVISION_ID,\n        uniqueFigureIds: true,\n        settings: { locale: DEFAULT_LOCALE },\n        pivots: {},\n        pivotNextId: 1,\n        customTableStyles: {},\n    };\n    return data;\n}\nfunction createEmptyExcelSheet(sheetId, name) {\n    return {\n        ...createEmptySheet(sheetId, name),\n        charts: [],\n        images: [],\n    };\n}\nfunction createEmptyExcelWorkbookData() {\n    return {\n        ...createEmptyWorkbookData(),\n        sheets: [createEmptyExcelSheet(INITIAL_SHEET_ID, \"Sheet1\")],\n    };\n}\n\nconst PasteInteractiveContent = {\n    wrongPasteSelection: _t(\"This operation is not allowed with multiple selections.\"),\n    willRemoveExistingMerge: _t(\"This operation is not possible due to a merge. Please remove the merges first than try again.\"),\n    wrongFigurePasteOption: _t(\"Cannot do a special paste of a figure.\"),\n    frozenPaneOverlap: _t(\"This operation is not allowed due to an overlapping frozen pane.\"),\n};\nfunction handlePasteResult(env, result) {\n    if (!result.isSuccessful) {\n        if (result.reasons.includes(\"WrongPasteSelection\" /* CommandResult.WrongPasteSelection */)) {\n            env.raiseError(PasteInteractiveContent.wrongPasteSelection);\n        }\n        else if (result.reasons.includes(\"WillRemoveExistingMerge\" /* CommandResult.WillRemoveExistingMerge */)) {\n            env.raiseError(PasteInteractiveContent.willRemoveExistingMerge);\n        }\n        else if (result.reasons.includes(\"WrongFigurePasteOption\" /* CommandResult.WrongFigurePasteOption */)) {\n            env.raiseError(PasteInteractiveContent.wrongFigurePasteOption);\n        }\n        else if (result.reasons.includes(\"FrozenPaneOverlap\" /* CommandResult.FrozenPaneOverlap */)) {\n            env.raiseError(PasteInteractiveContent.frozenPaneOverlap);\n        }\n    }\n}\nfunction interactivePaste(env, target, pasteOption) {\n    const result = env.model.dispatch(\"PASTE\", { target, pasteOption });\n    handlePasteResult(env, result);\n}\nfunction interactivePasteFromOS(env, target, clipboardContent, pasteOption) {\n    let result;\n    // We do not trust the clipboard content to be accurate and comprehensive.\n    // Therefore, to ensure reliability, we handle unexpected errors that may\n    // arise from content that would not be suitable for the current version.\n    try {\n        result = env.model.dispatch(\"PASTE_FROM_OS_CLIPBOARD\", {\n            target,\n            clipboardContent,\n            pasteOption,\n        });\n    }\n    catch (error) {\n        const parsedSpreadsheetContent = clipboardContent.data;\n        if (parsedSpreadsheetContent?.version !== CURRENT_VERSION) {\n            env.raiseError(_t(\"An unexpected error occurred while pasting content.\\\n          This is probably due to a spreadsheet version mismatch.\"));\n        }\n        result = env.model.dispatch(\"PASTE_FROM_OS_CLIPBOARD\", {\n            target,\n            clipboardContent: {\n                text: clipboardContent.text,\n            },\n            pasteOption,\n        });\n    }\n    handlePasteResult(env, result);\n}\n\nconst CfTerms = {\n    Errors: {\n        [\"InvalidRange\" /* CommandResult.InvalidRange */]: _t(\"The range is invalid\"),\n        [\"FirstArgMissing\" /* CommandResult.FirstArgMissing */]: _t(\"The argument is missing. Please provide a value\"),\n        [\"SecondArgMissing\" /* CommandResult.SecondArgMissing */]: _t(\"The second argument is missing. Please provide a value\"),\n        [\"MinNaN\" /* CommandResult.MinNaN */]: _t(\"The minpoint must be a number\"),\n        [\"MidNaN\" /* CommandResult.MidNaN */]: _t(\"The midpoint must be a number\"),\n        [\"MaxNaN\" /* CommandResult.MaxNaN */]: _t(\"The maxpoint must be a number\"),\n        [\"ValueUpperInflectionNaN\" /* CommandResult.ValueUpperInflectionNaN */]: _t(\"The first value must be a number\"),\n        [\"ValueLowerInflectionNaN\" /* CommandResult.ValueLowerInflectionNaN */]: _t(\"The second value must be a number\"),\n        [\"MinBiggerThanMax\" /* CommandResult.MinBiggerThanMax */]: _t(\"Minimum must be smaller then Maximum\"),\n        [\"MinBiggerThanMid\" /* CommandResult.MinBiggerThanMid */]: _t(\"Minimum must be smaller then Midpoint\"),\n        [\"MidBiggerThanMax\" /* CommandResult.MidBiggerThanMax */]: _t(\"Midpoint must be smaller then Maximum\"),\n        [\"LowerBiggerThanUpper\" /* CommandResult.LowerBiggerThanUpper */]: _t(\"Lower inflection point must be smaller than upper inflection point\"),\n        [\"MinInvalidFormula\" /* CommandResult.MinInvalidFormula */]: _t(\"Invalid Minpoint formula\"),\n        [\"MaxInvalidFormula\" /* CommandResult.MaxInvalidFormula */]: _t(\"Invalid Maxpoint formula\"),\n        [\"MidInvalidFormula\" /* CommandResult.MidInvalidFormula */]: _t(\"Invalid Midpoint formula\"),\n        [\"ValueUpperInvalidFormula\" /* CommandResult.ValueUpperInvalidFormula */]: _t(\"Invalid upper inflection point formula\"),\n        [\"ValueLowerInvalidFormula\" /* CommandResult.ValueLowerInvalidFormula */]: _t(\"Invalid lower inflection point formula\"),\n        [\"EmptyRange\" /* CommandResult.EmptyRange */]: _t(\"A range needs to be defined\"),\n        [\"ValueCellIsInvalidFormula\" /* CommandResult.ValueCellIsInvalidFormula */]: _t(\"At least one of the provided values is an invalid formula\"),\n        Unexpected: _t(\"The rule is invalid for an unknown reason\"),\n    },\n    ColorScale: _t(\"Color scale\"),\n    IconSet: _t(\"Icon set\"),\n    DataBar: _t(\"Data bar\"),\n};\nconst CellIsOperators = {\n    IsEmpty: _t(\"Is empty\"),\n    IsNotEmpty: _t(\"Is not empty\"),\n    ContainsText: _t(\"Contains\"),\n    NotContains: _t(\"Does not contain\"),\n    BeginsWith: _t(\"Starts with\"),\n    EndsWith: _t(\"Ends with\"),\n    Equal: _t(\"Is equal to\"),\n    NotEqual: _t(\"Is not equal to\"),\n    GreaterThan: _t(\"Is greater than\"),\n    GreaterThanOrEqual: _t(\"Is greater than or equal to\"),\n    LessThan: _t(\"Is less than\"),\n    LessThanOrEqual: _t(\"Is less than or equal to\"),\n    Between: _t(\"Is between\"),\n    NotBetween: _t(\"Is not between\"),\n};\nconst ChartTerms = {\n    Series: _t(\"Series\"),\n    BackgroundColor: _t(\"Background color\"),\n    StackedBarChart: _t(\"Stacked bar chart\"),\n    StackedLineChart: _t(\"Stacked line chart\"),\n    StackedAreaChart: _t(\"Stacked area chart\"),\n    StackedColumnChart: _t(\"Stacked column chart\"),\n    CumulativeData: _t(\"Cumulative data\"),\n    TreatLabelsAsText: _t(\"Treat labels as text\"),\n    AggregatedChart: _t(\"Aggregate\"),\n    Errors: {\n        Unexpected: _t(\"The chart definition is invalid for an unknown reason\"),\n        // BASIC CHART ERRORS (LINE | BAR | PIE)\n        [\"InvalidDataSet\" /* CommandResult.InvalidDataSet */]: _t(\"The dataset is invalid\"),\n        [\"InvalidLabelRange\" /* CommandResult.InvalidLabelRange */]: _t(\"Labels are invalid\"),\n        // SCORECARD CHART ERRORS\n        [\"InvalidScorecardKeyValue\" /* CommandResult.InvalidScorecardKeyValue */]: _t(\"The key value is invalid\"),\n        [\"InvalidScorecardBaseline\" /* CommandResult.InvalidScorecardBaseline */]: _t(\"The baseline value is invalid\"),\n        // GAUGE CHART ERRORS\n        [\"InvalidGaugeDataRange\" /* CommandResult.InvalidGaugeDataRange */]: _t(\"The data range is invalid\"),\n        [\"EmptyGaugeRangeMin\" /* CommandResult.EmptyGaugeRangeMin */]: _t(\"A minimum range limit value is needed\"),\n        [\"GaugeRangeMinNaN\" /* CommandResult.GaugeRangeMinNaN */]: _t(\"The minimum range limit value must be a number\"),\n        [\"EmptyGaugeRangeMax\" /* CommandResult.EmptyGaugeRangeMax */]: _t(\"A maximum range limit value is needed\"),\n        [\"GaugeRangeMaxNaN\" /* CommandResult.GaugeRangeMaxNaN */]: _t(\"The maximum range limit value must be a number\"),\n        [\"GaugeRangeMinBiggerThanRangeMax\" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */]: _t(\"Minimum range limit must be smaller than maximum range limit\"),\n        [\"GaugeLowerInflectionPointNaN\" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t(\"The lower inflection point value must be a number\"),\n        [\"GaugeUpperInflectionPointNaN\" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t(\"The upper inflection point value must be a number\"),\n    },\n};\nconst CustomCurrencyTerms = {\n    Custom: _t(\"Custom\"),\n};\nconst MergeErrorMessage = _t(\"Merged cells are preventing this operation. Unmerge those cells and try again.\");\nconst SplitToColumnsTerms = {\n    Errors: {\n        Unexpected: _t(\"Cannot split the selection for an unknown reason\"),\n        [\"NoSplitSeparatorInSelection\" /* CommandResult.NoSplitSeparatorInSelection */]: _t(\"There is no match for the selected separator in the selection\"),\n        [\"MoreThanOneColumnSelected\" /* CommandResult.MoreThanOneColumnSelected */]: _t(\"Only a selection from a single column can be split\"),\n        [\"SplitWillOverwriteContent\" /* CommandResult.SplitWillOverwriteContent */]: _t(\"Splitting will overwrite existing content\"),\n    },\n};\nconst RemoveDuplicateTerms = {\n    Errors: {\n        Unexpected: _t(\"Cannot remove duplicates for an unknown reason\"),\n        [\"MoreThanOneRangeSelected\" /* CommandResult.MoreThanOneRangeSelected */]: _t(\"Please select only one range of cells\"),\n        [\"EmptyTarget\" /* CommandResult.EmptyTarget */]: _t(\"Please select a range of cells containing values.\"),\n        [\"NoColumnsProvided\" /* CommandResult.NoColumnsProvided */]: _t(\"Please select at latest one column to analyze.\"),\n        //TODO: Remove it when accept to copy and paste merge cells\n        [\"WillRemoveExistingMerge\" /* CommandResult.WillRemoveExistingMerge */]: PasteInteractiveContent.willRemoveExistingMerge,\n    },\n};\nconst DVTerms = {\n    DateIs: {\n        today: _t(\"today\"),\n        yesterday: _t(\"yesterday\"),\n        tomorrow: _t(\"tomorrow\"),\n        lastWeek: _t(\"in the past week\"),\n        lastMonth: _t(\"in the past month\"),\n        lastYear: _t(\"in the past year\"),\n    },\n    DateIsBefore: {\n        today: _t(\"today\"),\n        yesterday: _t(\"yesterday\"),\n        tomorrow: _t(\"tomorrow\"),\n        lastWeek: _t(\"one week ago\"),\n        lastMonth: _t(\"one month ago\"),\n        lastYear: _t(\"one year ago\"),\n    },\n    CriterionError: {\n        notEmptyValue: _t(\"The value must not be empty\"),\n        numberValue: _t(\"The value must be a number\"),\n        dateValue: _t(\"The value must be a date\"),\n        validRange: _t(\"The value must be a valid range\"),\n    },\n};\nconst TableTerms = {\n    Errors: {\n        Unexpected: _t(\"The table zone is invalid for an unknown reason\"),\n        [\"TableOverlap\" /* CommandResult.TableOverlap */]: _t(\"You cannot create overlapping tables.\"),\n        [\"NonContinuousTargets\" /* CommandResult.NonContinuousTargets */]: _t(\"A table can only be created on a continuous selection.\"),\n        [\"InvalidRange\" /* CommandResult.InvalidRange */]: _t(\"The range is invalid\"),\n        [\"TargetOutOfSheet\" /* CommandResult.TargetOutOfSheet */]: _t(\"The range is out of the sheet\"),\n    },\n    Checkboxes: {\n        hasFilters: _t(\"Filter button\"),\n        headerRow: _t(\"Header row(s)\"),\n        bandedRows: _t(\"Banded rows\"),\n        firstColumn: _t(\"First column\"),\n        lastColumn: _t(\"Last column\"),\n        bandedColumns: _t(\"Banded columns\"),\n        automaticAutofill: _t(\"Automatically autofill formulas\"),\n        totalRow: _t(\"Total row\"),\n        isDynamic: _t(\"Auto-adjust to formula result\"),\n    },\n    Tooltips: {\n        filterWithoutHeader: _t(\"Cannot have filters without a header row\"),\n        isDynamic: _t(\"For tables based on array formulas only\"),\n    },\n};\nconst measureDisplayTerms = {\n    labels: {\n        no_calculations: _t(\"No calculations\"),\n        \"%_of_grand_total\": _t(\"% of grand total\"),\n        \"%_of_col_total\": _t(\"% of column total\"),\n        \"%_of_row_total\": _t(\"% of row total\"),\n        \"%_of\": _t(\"% of\"),\n        \"%_of_parent_row_total\": _t(\"% of parent row total\"),\n        \"%_of_parent_col_total\": _t(\"% of parent column total\"),\n        \"%_of_parent_total\": _t(\"% of parent total\"),\n        difference_from: _t(\"Difference from\"),\n        \"%_difference_from\": _t(\"% difference from\"),\n        running_total: _t(\"Running total\"),\n        \"%_running_total\": _t(\"% Running total\"),\n        rank_asc: _t(\"Rank smallest to largest\"),\n        rank_desc: _t(\"Rank largest to smallest\"),\n        index: _t(\"Index\"),\n    },\n    descriptions: {\n        \"%_of_grand_total\": () => _t(\"Displayed as % of grand total\"),\n        \"%_of_col_total\": () => _t(\"Displayed as % of column total\"),\n        \"%_of_row_total\": () => _t(\"Displayed as % of row total\"),\n        \"%_of\": (field) => _t('Displayed as % of \"%s\"', field),\n        \"%_of_parent_row_total\": (field) => _t('Displayed as % of parent row total of \"%s\"', field),\n        \"%_of_parent_col_total\": () => _t(\"Displayed as % of parent column total\"),\n        \"%_of_parent_total\": (field) => _t('Displayed as % of parent \"%s\" total', field),\n        difference_from: (field) => _t('Displayed as difference from \"%s\"', field),\n        \"%_difference_from\": (field) => _t('Displayed as % difference from \"%s\"', field),\n        running_total: (field) => _t('Displayed as running total based on \"%s\"', field),\n        \"%_running_total\": (field) => _t('Displayed as % running total based on \"%s\"', field),\n        rank_asc: (field) => _t('Displayed as rank from smallest to largest based on \"%s\"', field),\n        rank_desc: (field) => _t('Displayed as rank largest to smallest based on \"%s\"', field),\n        index: () => _t(\"Displayed as index\"),\n    },\n    documentation: {\n        no_calculations: _t(\"Displays the value that is entered in the field.\"),\n        \"%_of_grand_total\": _t(\"Displays values as a percentage of the grand total of all the values or data points in the report.\"),\n        \"%_of_col_total\": _t(\"Displays all the values in each column or series as a percentage of the total for the column or series.\"),\n        \"%_of_row_total\": _t(\"Displays the value in each row or category as a percentage of the total for the row or category.\"),\n        \"%_of\": _t(\"Displays values as a percentage of the value of the Base item in the Base field.\"),\n        \"%_of_parent_row_total\": _t(\"Calculates values as follows:\\n(value for the item) / (value for the parent item on rows)\"),\n        \"%_of_parent_col_total\": _t(\"Calculates values as follows:\\n(value for the item) / (value for the parent item on columns)\"),\n        \"%_of_parent_total\": _t(\"Calculates values as follows:\\n(value for the item) / (value for the parent item of the selected Base field)\"),\n        difference_from: _t(\"Displays values as the difference from the value of the Base item in the Base field.\"),\n        \"%_difference_from\": _t(\"Displays values as the percentage difference from the value of the Base item in the Base field.\"),\n        running_total: _t(\"Displays the value for successive items in the Base field as a running total.\"),\n        \"%_running_total\": _t(\"Calculates the value as a percentage for successive items in the Base field that are displayed as a running total.\"),\n        rank_asc: _t(\"Displays the rank of selected values in a specific field, listing the smallest item in the field as 1, and each larger value with a higher rank value.\"),\n        rank_desc: _t(\"Displays the rank of selected values in a specific field, listing the largest item in the field as 1, and each smaller value with a higher rank value.\"),\n        index: _t(\"Calculates values as follows:\\n((value in cell) x (Grand Total of Grand Totals)) / ((Grand Row Total) x (Grand Column Total))\"),\n    },\n};\n\n/**\n * This file contains helpers that are common to different runtime charts (mainly\n * line, bar and pie charts)\n */\n/**\n * Get the data from a dataSet\n */\nfunction getData(getters, ds) {\n    if (ds.dataRange) {\n        const labelCellZone = ds.labelCell ? [ds.labelCell.zone] : [];\n        const dataZone = recomputeZones([ds.dataRange.zone], labelCellZone)[0];\n        if (dataZone === undefined) {\n            return [];\n        }\n        const dataRange = getters.getRangeFromZone(ds.dataRange.sheetId, dataZone);\n        return getters.getRangeValues(dataRange).map((value) => (value === \"\" ? undefined : value));\n    }\n    return [];\n}\nfunction filterEmptyDataPoints(labels, datasets) {\n    const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));\n    const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {\n        const label = labels[dataPointIndex];\n        const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);\n        return label || values.some((value) => value === 0 || Boolean(value));\n    });\n    return {\n        labels: dataPointsIndexes.map((i) => labels[i] || \"\"),\n        dataSetsValues: datasets.map((dataset) => ({\n            ...dataset,\n            data: dataPointsIndexes.map((i) => dataset.data[i]),\n        })),\n    };\n}\n/**\n * Aggregates data based on labels\n */\nfunction aggregateDataForLabels(labels, datasets) {\n    const parseNumber = (value) => (typeof value === \"number\" ? value : 0);\n    const labelSet = new Set(labels);\n    const labelMap = {};\n    labelSet.forEach((label) => {\n        labelMap[label] = new Array(datasets.length).fill(0);\n    });\n    for (const indexOfLabel of range(0, labels.length)) {\n        const label = labels[indexOfLabel];\n        for (const indexOfDataset of range(0, datasets.length)) {\n            labelMap[label][indexOfDataset] += parseNumber(datasets[indexOfDataset].data[indexOfLabel]);\n        }\n    }\n    return {\n        labels: Array.from(labelSet),\n        dataSetsValues: datasets.map((dataset, indexOfDataset) => ({\n            ...dataset,\n            data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),\n        })),\n    };\n}\nfunction truncateLabel(label) {\n    if (!label) {\n        return \"\";\n    }\n    if (label.length > MAX_CHAR_LABEL) {\n        return label.substring(0, MAX_CHAR_LABEL) + \"\u2026\";\n    }\n    return label;\n}\n/**\n * Get a default chart js configuration\n */\nfunction getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, truncateLabels = true, horizontalChart, }) {\n    const chartTitle = chart.title.text ? chart.title : { ...chart.title, content: \"\" };\n    const options = {\n        // https://www.chartjs.org/docs/latest/general/responsive.html\n        responsive: true, // will resize when its container is resized\n        maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout\n        layout: {\n            padding: {\n                left: DEFAULT_CHART_PADDING,\n                right: DEFAULT_CHART_PADDING,\n                top: chartTitle.text ? DEFAULT_CHART_PADDING / 2 : DEFAULT_CHART_PADDING + 5,\n                bottom: DEFAULT_CHART_PADDING,\n            },\n        },\n        elements: {\n            line: {\n                fill: false, // do not fill the area under line charts\n            },\n            point: {\n                hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby\n            },\n        },\n        animation: false,\n        plugins: {\n            title: {\n                display: !!chartTitle.text,\n                text: _t(chartTitle.text),\n                color: chartTitle?.color ?? fontColor,\n                align: chartTitle.align === \"center\" ? \"center\" : chartTitle.align === \"right\" ? \"end\" : \"start\",\n                font: {\n                    size: DEFAULT_CHART_FONT_SIZE,\n                    weight: chartTitle.bold ? \"bold\" : \"normal\",\n                    style: chartTitle.italic ? \"italic\" : \"normal\",\n                },\n            },\n            legend: {\n                // Disable default legend onClick (show/hide dataset), to allow us to set a global onClick on the chart container.\n                // If we want to re-enable this in the future, we need to override the default onClick to stop the event propagation\n                onClick: () => { },\n            },\n            tooltip: {\n                callbacks: {\n                    label: function (tooltipItem) {\n                        const xLabel = tooltipItem.dataset?.label || tooltipItem.label;\n                        // tooltipItem.parsed can be an object or a number for pie charts\n                        let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;\n                        if (yLabel === undefined || yLabel === null) {\n                            yLabel = tooltipItem.parsed;\n                        }\n                        const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? \"#,##\" : format;\n                        const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });\n                        return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;\n                    },\n                },\n            },\n        },\n    };\n    return {\n        type: chart.type,\n        options,\n        data: {\n            labels: truncateLabels ? labels.map(truncateLabel) : labels,\n            datasets: [],\n        },\n        platform: undefined, // This key is optional and will be set by chart.js\n        plugins: [],\n    };\n}\nfunction getChartLabelFormat(getters, range) {\n    if (!range)\n        return undefined;\n    const { sheetId, zone: { left, top, bottom }, } = range;\n    for (let row = top; row <= bottom; row++) {\n        const format = getters.getEvaluatedCell({ sheetId, col: left, row }).format;\n        if (format) {\n            return format;\n        }\n    }\n    return undefined;\n}\nfunction getChartLabelValues(getters, dataSets, labelRange) {\n    let labels = { values: [], formattedValues: [] };\n    if (labelRange) {\n        const { left } = labelRange.zone;\n        if (!labelRange.invalidXc &&\n            !labelRange.invalidSheetName &&\n            !getters.isColHidden(labelRange.sheetId, left)) {\n            labels = {\n                formattedValues: getters.getRangeFormattedValues(labelRange),\n                values: getters.getRangeValues(labelRange).map((val) => String(val ?? \"\")),\n            };\n        }\n        else if (dataSets[0]) {\n            const ranges = getData(getters, dataSets[0]);\n            labels = {\n                formattedValues: range(0, ranges.length).map((r) => r.toString()),\n                values: labels.formattedValues,\n            };\n        }\n    }\n    else if (dataSets.length === 1) {\n        for (let i = 0; i < getData(getters, dataSets[0]).length; i++) {\n            labels.formattedValues.push(\"\");\n            labels.values.push(\"\");\n        }\n    }\n    else {\n        if (dataSets[0]) {\n            const ranges = getData(getters, dataSets[0]);\n            labels = {\n                formattedValues: range(0, ranges.length).map((r) => r.toString()),\n                values: labels.formattedValues,\n            };\n        }\n    }\n    return labels;\n}\n/**\n * Get the format to apply to the the dataset values. This format is defined as the first format\n * found in the dataset ranges that isn't a date format.\n */\nfunction getChartDatasetFormat(getters, dataSets) {\n    for (const ds of dataSets) {\n        const formatsInDataset = getters.getRangeFormats(ds.dataRange);\n        const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));\n        if (format)\n            return format;\n    }\n    return undefined;\n}\nfunction getChartDatasetValues(getters, dataSets) {\n    const datasetValues = [];\n    for (const [dsIndex, ds] of Object.entries(dataSets)) {\n        if (getters.isColHidden(ds.dataRange.sheetId, ds.dataRange.zone.left)) {\n            continue;\n        }\n        let label;\n        if (ds.labelCell) {\n            const labelRange = ds.labelCell;\n            const cell = labelRange\n                ? getters.getEvaluatedCell({\n                    sheetId: labelRange.sheetId,\n                    col: labelRange.zone.left,\n                    row: labelRange.zone.top,\n                })\n                : undefined;\n            label =\n                cell && labelRange\n                    ? truncateLabel(cell.formattedValue)\n                    : (label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`);\n        }\n        else {\n            label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`;\n        }\n        let data = ds.dataRange ? getData(getters, ds) : [];\n        if (data.every((e) => typeof e === \"string\" && !isEvaluationError(e)) &&\n            data.some((e) => e !== \"\")) {\n            // In this case, we want a chart based on the string occurrences count\n            // This will be done by associating each string with a value of 1 and\n            // then using the classical aggregation method to sum the values.\n            data.fill(1);\n        }\n        else if (data.every((cell) => cell === undefined || cell === null || !isNumber(cell.toString(), getters.getLocale()))) {\n            continue;\n        }\n        datasetValues.push({ data, label });\n    }\n    return datasetValues;\n}\n/**\n * If the chart is a stacked area chart, we want to fill until the next dataset.\n * If the chart is a simple area chart, we want to fill until the origin (bottom axis).\n *\n * See https://www.chartjs.org/docs/latest/charts/area.html#filling-modes\n */\nfunction getFillingMode(index, stackedChart) {\n    if (!stackedChart) {\n        return \"origin\";\n    }\n    return index === 0 ? \"origin\" : \"-1\";\n}\nfunction chartToImage(runtime, figure, type) {\n    // wrap the canvas in a div with a fixed size because chart.js would\n    // fill the whole page otherwise\n    const div = document.createElement(\"div\");\n    div.style.width = `${figure.width}px`;\n    div.style.height = `${figure.height}px`;\n    const canvas = document.createElement(\"canvas\");\n    div.append(canvas);\n    canvas.setAttribute(\"width\", figure.width.toString());\n    canvas.setAttribute(\"height\", figure.height.toString());\n    // we have to add the canvas to the DOM otherwise it won't be rendered\n    document.body.append(div);\n    if (\"chartJsConfig\" in runtime) {\n        const config = deepCopy(runtime.chartJsConfig);\n        config.plugins = [backgroundColorChartJSPlugin];\n        const chart = new window.Chart(canvas, config);\n        const imgContent = chart.toBase64Image();\n        chart.destroy();\n        div.remove();\n        return imgContent;\n    }\n    else if (type === \"scorecard\") {\n        const design = getScorecardConfiguration(figure, runtime);\n        drawScoreChart(design, canvas);\n        const imgContent = canvas.toDataURL();\n        div.remove();\n        return imgContent;\n    }\n    else if (type === \"gauge\") {\n        drawGaugeChart(canvas, runtime);\n        const imgContent = canvas.toDataURL();\n        div.remove();\n        return imgContent;\n    }\n    return undefined;\n}\n/**\n * Custom chart.js plugin to set the background color of the canvas\n * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md\n */\nconst backgroundColorChartJSPlugin = {\n    id: \"customCanvasBackgroundColor\",\n    beforeDraw: (chart) => {\n        const { ctx } = chart;\n        ctx.save();\n        ctx.globalCompositeOperation = \"destination-over\";\n        ctx.fillStyle = \"#ffffff\";\n        ctx.fillRect(0, 0, chart.width, chart.height);\n        ctx.restore();\n    },\n};\n\nclass BarChart extends AbstractChart {\n    dataSets;\n    labelRange;\n    background;\n    legendPosition;\n    stacked;\n    aggregated;\n    type = \"bar\";\n    dataSetsHaveTitle;\n    dataSetDesign;\n    axesDesign;\n    horizontal;\n    showValues;\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);\n        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);\n        this.background = definition.background;\n        this.legendPosition = definition.legendPosition;\n        this.stacked = definition.stacked;\n        this.aggregated = definition.aggregated;\n        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;\n        this.dataSetDesign = definition.dataSets;\n        this.axesDesign = definition.axesDesign;\n        this.horizontal = definition.horizontal;\n        this.showValues = definition.showValues;\n    }\n    static transformDefinition(definition, executed) {\n        return transformChartDefinitionWithDataSetsWithZone(definition, executed);\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, checkDataset, checkLabelRange);\n    }\n    static getDefinitionFromContextCreation(context) {\n        return {\n            background: context.background,\n            dataSets: context.range ?? [],\n            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,\n            stacked: context.stacked ?? false,\n            aggregated: context.aggregated ?? false,\n            legendPosition: context.legendPosition ?? \"top\",\n            title: context.title || { text: \"\" },\n            type: \"bar\",\n            labelRange: context.auxiliaryRange || undefined,\n            axesDesign: context.axesDesign,\n            showValues: context.showValues,\n        };\n    }\n    getContextCreation() {\n        const range = [];\n        for (const [i, dataSet] of this.dataSets.entries()) {\n            range.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),\n            });\n        }\n        return {\n            ...this,\n            range,\n            auxiliaryRange: this.labelRange\n                ? this.getters.getRangeString(this.labelRange, this.sheetId)\n                : undefined,\n        };\n    }\n    copyForSheetId(sheetId) {\n        const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);\n        const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);\n        return new BarChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);\n        return new BarChart(definition, sheetId, this.getters);\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);\n    }\n    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {\n        const ranges = [];\n        for (const [i, dataSet] of dataSets.entries()) {\n            ranges.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),\n            });\n        }\n        return {\n            type: \"bar\",\n            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,\n            background: this.background,\n            dataSets: ranges,\n            legendPosition: this.legendPosition,\n            labelRange: labelRange\n                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)\n                : undefined,\n            title: this.title,\n            stacked: this.stacked,\n            aggregated: this.aggregated,\n            axesDesign: this.axesDesign,\n            horizontal: this.horizontal,\n            showValues: this.showValues,\n        };\n    }\n    getDefinitionForExcel() {\n        // Excel does not support aggregating labels\n        if (this.aggregated)\n            return undefined;\n        const dataSets = this.dataSets\n            .map((ds) => toExcelDataset(this.getters, ds))\n            .filter((ds) => ds.range !== \"\" && ds.range !== CellErrorType.InvalidReference);\n        const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));\n        const definition = this.getDefinition();\n        return {\n            ...definition,\n            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),\n            fontColor: toXlsxHexColor(chartFontColor(this.background)),\n            dataSets,\n            labelRange,\n            verticalAxis: getDefinedAxis(definition),\n        };\n    }\n    updateRanges(applyChange) {\n        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);\n        if (!isStale) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);\n        return new BarChart(definition, this.sheetId, this.getters);\n    }\n}\nfunction createBarChartRuntime(chart, getters) {\n    const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);\n    let labels = labelValues.formattedValues;\n    let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);\n    if (chart.dataSetsHaveTitle &&\n        dataSetsValues[0] &&\n        labels.length > dataSetsValues[0].data.length) {\n        labels.shift();\n    }\n    ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));\n    if (chart.aggregated) {\n        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));\n    }\n    const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);\n    const locale = getters.getLocale();\n    const localeFormat = { format: dataSetFormat, locale };\n    const fontColor = chartFontColor(chart.background);\n    const config = getDefaultChartJsRuntime(chart, labels, fontColor, {\n        ...localeFormat,\n        horizontalChart: chart.horizontal,\n    });\n    const legend = {\n        labels: { color: fontColor },\n    };\n    if (chart.legendPosition === \"none\") {\n        legend.display = false;\n    }\n    else {\n        legend.position = chart.legendPosition;\n    }\n    config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };\n    config.options.layout = {\n        padding: computeChartPadding({\n            displayTitle: !!chart.title.text,\n            displayLegend: chart.legendPosition === \"top\",\n        }),\n    };\n    config.options.indexAxis = chart.horizontal ? \"y\" : \"x\";\n    config.options.scales = {};\n    const labelsAxis = { ticks: { padding: 5, color: fontColor } };\n    const valuesAxis = {\n        beginAtZero: true, // the origin of the y axis is always zero\n        ticks: {\n            color: fontColor,\n            callback: formatTickValue(localeFormat),\n        },\n    };\n    const xAxis = chart.horizontal ? valuesAxis : labelsAxis;\n    const yAxis = chart.horizontal ? labelsAxis : valuesAxis;\n    const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());\n    config.options.scales.x = { ...xAxis, title: getChartAxisTitleRuntime(chart.axesDesign?.x) };\n    if (useLeftAxis) {\n        config.options.scales.y = {\n            ...yAxis,\n            position: \"left\",\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y),\n        };\n    }\n    if (useRightAxis) {\n        config.options.scales.y1 = {\n            ...yAxis,\n            position: \"right\",\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y1),\n        };\n    }\n    if (chart.stacked) {\n        // @ts-ignore chart.js type is broken\n        config.options.scales.x.stacked = true;\n        if (useLeftAxis) {\n            // @ts-ignore chart.js type is broken\n            config.options.scales.y.stacked = true;\n        }\n        if (useRightAxis) {\n            // @ts-ignore chart.js type is broken\n            config.options.scales.y1.stacked = true;\n        }\n    }\n    config.options.plugins.chartShowValuesPlugin = {\n        showValues: chart.showValues,\n        background: chart.background,\n        horizontal: chart.horizontal,\n        callback: formatTickValue(localeFormat),\n    };\n    const definition = chart.getDefinition();\n    const colors = getChartColorsGenerator(definition, dataSetsValues.length);\n    const trendDatasets = [];\n    for (const index in dataSetsValues) {\n        const { label, data } = dataSetsValues[index];\n        const color = colors.next();\n        const dataset = {\n            label,\n            data,\n            borderColor: definition.background || BACKGROUND_CHART_COLOR,\n            borderWidth: definition.stacked ? 1 : 0,\n            backgroundColor: color,\n        };\n        config.data.datasets.push(dataset);\n        if (definition.dataSets?.[index]?.label) {\n            const label = definition.dataSets[index].label;\n            dataset.label = label;\n        }\n        if (definition.dataSets?.[index]?.yAxisId && !chart.horizontal) {\n            dataset[\"yAxisID\"] = definition.dataSets[index].yAxisId;\n        }\n        const trend = definition.dataSets?.[index].trend;\n        if (!trend?.display || chart.horizontal) {\n            continue;\n        }\n        const trendDataset = getTrendDatasetForBarChart(trend, dataset);\n        if (trendDataset) {\n            trendDatasets.push(trendDataset);\n        }\n    }\n    if (trendDatasets.length) {\n        /* We add a second x axis here to draw the trend lines, with the labels length being\n         * set so that the second axis points match the classical x axis\n         */\n        const maxLength = Math.max(...trendDatasets.map((trendDataset) => trendDataset.data.length));\n        config.options.scales[TREND_LINE_XAXIS_ID] = {\n            ...xAxis,\n            labels: Array(maxLength).fill(\"\"),\n            offset: false,\n            display: false,\n        };\n        /* These datasets must be inserted after the original\n         * datasets to ensure the way we distinguish the originals and trendLine datasets after\n         */\n        trendDatasets.forEach((x) => config.data.datasets.push(x));\n        config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {\n            return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)\n                ? undefined\n                : \"\";\n        };\n    }\n    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };\n}\n\nconst UNIT_LENGTH = {\n    second: 1000,\n    minute: 1000 * 60,\n    hour: 1000 * 3600,\n    day: 1000 * 3600 * 24,\n    month: 1000 * 3600 * 24 * 30,\n    year: 1000 * 3600 * 24 * 365,\n};\nconst Milliseconds = {\n    inSeconds: function (milliseconds) {\n        return Math.floor(milliseconds / UNIT_LENGTH.second);\n    },\n    inMinutes: function (milliseconds) {\n        return Math.floor(milliseconds / UNIT_LENGTH.minute);\n    },\n    inHours: function (milliseconds) {\n        return Math.floor(milliseconds / UNIT_LENGTH.hour);\n    },\n    inDays: function (milliseconds) {\n        return Math.floor(milliseconds / UNIT_LENGTH.day);\n    },\n    inMonths: function (milliseconds) {\n        return Math.floor(milliseconds / UNIT_LENGTH.month);\n    },\n    inYears: function (milliseconds) {\n        return Math.floor(milliseconds / UNIT_LENGTH.year);\n    },\n};\n/**\n * Regex to test if a format string is a date format that can be translated into a luxon time format\n */\nconst timeFormatLuxonCompatible = /^((d|dd|m|mm|yyyy|yy|hh|h|ss|a)(-|:|\\s|\\/))*(d|dd|m|mm|yyyy|yy|hh|h|ss|a)$/i;\n/** Get the time options for the XAxis of ChartJS */\nfunction getChartTimeOptions(labels, labelFormat, locale) {\n    const luxonFormat = convertDateFormatForLuxon(labelFormat);\n    const timeUnit = getBestTimeUnitForScale(labels, luxonFormat, locale);\n    const displayFormats = {};\n    if (timeUnit) {\n        displayFormats[timeUnit] = luxonFormat;\n    }\n    return {\n        parser: luxonFormat,\n        displayFormats,\n        unit: timeUnit ?? false,\n    };\n}\n/**\n * Convert the given date format into a format that moment.js understands.\n *\n * https://github.com/moment/luxon/blob/master/docs/formatting.md#table-of-tokens\n */\nfunction convertDateFormatForLuxon(format) {\n    // \"m\" before \"h\" === month, \"m\" after \"h\" === minute\n    const indexH = format.indexOf(\"h\");\n    if (indexH >= 0) {\n        format = format.slice(0, indexH).replace(/m/g, \"M\") + format.slice(indexH);\n    }\n    else {\n        format = format.replace(/m/g, \"M\");\n    }\n    // If we have an \"a\", we should display hours as AM/PM (h), otherwise display 24 hours format (H)\n    if (!format.includes(\"a\")) {\n        format = format.replace(/h/g, \"H\");\n    }\n    return format;\n}\n/** Get the minimum time unit that the format is able to display */\nfunction getFormatMinDisplayUnit(format) {\n    if (format.includes(\"s\")) {\n        return \"second\";\n    }\n    else if (format.includes(\"m\")) {\n        return \"minute\";\n    }\n    else if (format.includes(\"h\") || format.includes(\"H\")) {\n        return \"hour\";\n    }\n    else if (format.includes(\"d\")) {\n        return \"day\";\n    }\n    else if (format.includes(\"M\")) {\n        return \"month\";\n    }\n    return \"year\";\n}\n/**\n * Returns the best time unit that should be used for the X axis of a chart in order to display all\n * the labels correctly.\n *\n * There is two conditions :\n *  - the format of the labels should be able to display the unit. For example if the format is \"DD/MM/YYYY\"\n *    it makes no sense to try to use minutes in the X axis\n *  - we want the \"best fit\" unit. For example if the labels span a period of several days, we want to use days\n *    as a unit, but if they span 200 days, we'd like to use months instead\n *\n */\nfunction getBestTimeUnitForScale(labels, format, locale) {\n    const labelDates = labels.map((label) => parseDateTime(label, locale)?.jsDate);\n    if (labelDates.some((date) => date === undefined) || labels.length < 2) {\n        return undefined;\n    }\n    const labelsTimestamps = labelDates.map((date) => date.getTime());\n    const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps);\n    const minUnit = getFormatMinDisplayUnit(format);\n    if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {\n        return \"second\";\n    }\n    else if (UNIT_LENGTH.minute >= UNIT_LENGTH[minUnit] && Milliseconds.inMinutes(period) < 180) {\n        return \"minute\";\n    }\n    else if (UNIT_LENGTH.hour >= UNIT_LENGTH[minUnit] && Milliseconds.inHours(period) < 96) {\n        return \"hour\";\n    }\n    else if (UNIT_LENGTH.day >= UNIT_LENGTH[minUnit] && Milliseconds.inDays(period) < 90) {\n        return \"day\";\n    }\n    else if (UNIT_LENGTH.month >= UNIT_LENGTH[minUnit] && Milliseconds.inMonths(period) < 36) {\n        return \"month\";\n    }\n    return \"year\";\n}\n\nfunction fixEmptyLabelsForDateCharts(labels, dataSetsValues) {\n    if (labels.length === 0 || labels.every((label) => !label)) {\n        return { labels, dataSetsValues };\n    }\n    const newLabels = [...labels];\n    const newDatasets = deepCopy(dataSetsValues);\n    for (let i = 0; i < newLabels.length; i++) {\n        if (!newLabels[i]) {\n            newLabels[i] = findNextDefinedValue(newLabels, i);\n            for (let ds of newDatasets) {\n                ds.data[i] = undefined;\n            }\n        }\n    }\n    return { labels: newLabels, dataSetsValues: newDatasets };\n}\nfunction canChartParseLabels(labelRange, getters) {\n    return canBeDateChart(labelRange, getters) || canBeLinearChart(labelRange, getters);\n}\nfunction getChartAxisType(chart, getters) {\n    if (isDateChart(chart, getters) && isLuxonTimeAdapterInstalled()) {\n        return \"time\";\n    }\n    if (isLinearChart(chart, getters)) {\n        return \"linear\";\n    }\n    return \"category\";\n}\nfunction isDateChart(chart, getters) {\n    return !chart.labelsAsText && canBeDateChart(chart.labelRange, getters);\n}\nfunction isLinearChart(chart, getters) {\n    return !chart.labelsAsText && canBeLinearChart(chart.labelRange, getters);\n}\nfunction canBeDateChart(labelRange, getters) {\n    if (!labelRange || !canBeLinearChart(labelRange, getters)) {\n        return false;\n    }\n    const labelFormat = getChartLabelFormat(getters, labelRange);\n    return Boolean(labelFormat && timeFormatLuxonCompatible.test(labelFormat));\n}\nfunction canBeLinearChart(labelRange, getters) {\n    if (!labelRange) {\n        return false;\n    }\n    const labels = getters.getRangeValues(labelRange);\n    if (labels.some((label) => isNaN(Number(label)) && label)) {\n        return false;\n    }\n    if (labels.every((label) => !label)) {\n        return false;\n    }\n    return true;\n}\nlet missingTimeAdapterAlreadyWarned = false;\nfunction isLuxonTimeAdapterInstalled() {\n    if (!window.Chart) {\n        return false;\n    }\n    // @ts-ignore\n    const adapter = new window.Chart._adapters._date({});\n    const isInstalled = adapter._id === \"luxon\";\n    if (!isInstalled && !missingTimeAdapterAlreadyWarned) {\n        missingTimeAdapterAlreadyWarned = true;\n        console.warn(\"'chartjs-adapter-luxon' time adapter is not installed. Time scale axes are disabled.\");\n    }\n    return isInstalled;\n}\nfunction getTrendDatasetForLineChart(config, dataset, axisType, locale) {\n    const filteredValues = [];\n    const filteredLabels = [];\n    const labels = [];\n    const datasetLength = dataset.data.length;\n    if (datasetLength < 2) {\n        return;\n    }\n    switch (axisType) {\n        case \"category\":\n            for (let i = 0; i < datasetLength; i++) {\n                if (typeof dataset.data[i] === \"number\") {\n                    filteredValues.push(dataset.data[i]);\n                    filteredLabels.push(i + 1);\n                }\n                labels.push(i + 1);\n            }\n            break;\n        case \"linear\":\n            for (const point of dataset.data) {\n                const label = Number(point.x);\n                if (isNaN(label)) {\n                    continue;\n                }\n                if (typeof point.y === \"number\") {\n                    filteredValues.push(point.y);\n                    filteredLabels.push(label);\n                }\n                labels.push(label);\n            }\n            break;\n        case \"time\":\n            for (const point of dataset.data) {\n                const date = toNumber({ value: point.x }, locale);\n                if (point.y !== null) {\n                    filteredValues.push(point.y);\n                    filteredLabels.push(date);\n                }\n                labels.push(date);\n            }\n            break;\n    }\n    const xmin = Math.min(...labels);\n    const xmax = Math.max(...labels);\n    if (xmax === xmin) {\n        return;\n    }\n    const numberOfStep = 5 * labels.length;\n    const step = (xmax - xmin) / numberOfStep;\n    const newLabels = range(xmin, xmax + step / 2, step);\n    const newValues = interpolateData(config, filteredValues, filteredLabels, newLabels);\n    if (!newValues.length) {\n        return;\n    }\n    return getFullTrendingLineDataSet(dataset, config, newValues);\n}\nfunction createLineOrScatterChartRuntime(chart, getters) {\n    const axisType = getChartAxisType(chart, getters);\n    const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);\n    let labels = axisType === \"linear\" ? labelValues.values : labelValues.formattedValues;\n    let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);\n    if (chart.dataSetsHaveTitle &&\n        dataSetsValues[0] &&\n        labels.length > dataSetsValues[0].data.length) {\n        labels.shift();\n    }\n    ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));\n    if (axisType === \"time\") {\n        ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues));\n    }\n    if (chart.aggregated) {\n        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));\n    }\n    const locale = getters.getLocale();\n    const truncateLabels = axisType === \"category\";\n    const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);\n    const options = { format: dataSetFormat, locale, truncateLabels };\n    const fontColor = chartFontColor(chart.background);\n    const config = getDefaultChartJsRuntime(chart, labels, fontColor, options);\n    const legend = {\n        labels: {\n            color: fontColor,\n            generateLabels(chart) {\n                // color the legend labels with the dataset color, without any transparency\n                const { data } = chart;\n                const labels = window.Chart.defaults.plugins.legend.labels.generateLabels(chart);\n                for (const [index, label] of labels.entries()) {\n                    label.fillStyle = data.datasets[index].borderColor;\n                }\n                return labels;\n            },\n        },\n    };\n    if (chart.legendPosition === \"none\") {\n        legend.display = false;\n    }\n    else {\n        legend.position = chart.legendPosition;\n    }\n    Object.assign(config.options.plugins.legend || {}, legend);\n    config.options.layout = {\n        padding: computeChartPadding({\n            displayTitle: !!chart.title.text,\n            displayLegend: chart.legendPosition === \"top\",\n        }),\n    };\n    const xAxis = {\n        ticks: {\n            padding: 5,\n            color: fontColor,\n        },\n        title: getChartAxisTitleRuntime(chart.axesDesign?.x),\n    };\n    config.options.scales = {\n        x: xAxis,\n    };\n    const yAxis = {\n        beginAtZero: true, // the origin of the y axis is always zero\n        ticks: {\n            color: fontColor,\n            callback: formatTickValue(options),\n        },\n    };\n    const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());\n    if (useLeftAxis) {\n        config.options.scales.y = {\n            ...yAxis,\n            position: \"left\",\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y),\n        };\n    }\n    if (useRightAxis) {\n        config.options.scales.y1 = {\n            ...yAxis,\n            position: \"right\",\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y1),\n        };\n    }\n    if (\"stacked\" in chart && chart.stacked) {\n        if (useLeftAxis) {\n            // @ts-ignore chart.js type is broken\n            config.options.scales.y.stacked = true;\n        }\n        if (useRightAxis) {\n            // @ts-ignore chart.js type is broken\n            config.options.scales.y1.stacked = true;\n        }\n    }\n    config.options.plugins.chartShowValuesPlugin = {\n        showValues: chart.showValues,\n        background: chart.background,\n        callback: formatTickValue(options),\n    };\n    if (chart.dataSetsHaveTitle &&\n        dataSetsValues[0] &&\n        labels.length > dataSetsValues[0].data.length) {\n        labels.shift();\n    }\n    const labelFormat = getChartLabelFormat(getters, chart.labelRange);\n    if (axisType === \"time\") {\n        const axis = {\n            type: \"time\",\n            time: getChartTimeOptions(labels, labelFormat, locale),\n        };\n        Object.assign(config.options.scales.x, axis);\n        config.options.scales.x.ticks.maxTicksLimit = 15;\n    }\n    else if (axisType === \"linear\") {\n        config.options.scales.x.type = \"linear\";\n        config.options.scales.x.ticks.callback = (value) => formatValue(value, { format: labelFormat, locale });\n        config.options.plugins.tooltip.callbacks.label = (tooltipItem) => {\n            const dataSetPoint = dataSetsValues[tooltipItem.datasetIndex].data[tooltipItem.dataIndex];\n            let label = tooltipItem.label || labelValues.values[tooltipItem.dataIndex];\n            if (isNumber(label, locale)) {\n                label = toNumber(label, locale);\n            }\n            const formattedX = formatValue(label, { locale, format: labelFormat });\n            const formattedY = formatValue(dataSetPoint, { locale, format: dataSetFormat });\n            const dataSetTitle = tooltipItem.dataset.label;\n            return formattedX\n                ? `${dataSetTitle}: (${formattedX}, ${formattedY})`\n                : `${dataSetTitle}: ${formattedY}`;\n        };\n    }\n    const areaChart = \"fillArea\" in chart ? chart.fillArea : false;\n    const stackedChart = \"stacked\" in chart ? chart.stacked : false;\n    const cumulative = \"cumulative\" in chart ? chart.cumulative : false;\n    const definition = chart.getDefinition();\n    const colors = getChartColorsGenerator(definition, dataSetsValues.length);\n    for (let [index, { label, data }] of dataSetsValues.entries()) {\n        const color = colors.next();\n        let backgroundRGBA = colorToRGBA(color);\n        if (areaChart) {\n            backgroundRGBA.a = LINE_FILL_TRANSPARENCY;\n        }\n        if (cumulative) {\n            let accumulator = 0;\n            data = data.map((value) => {\n                if (!isNaN(value)) {\n                    accumulator += parseFloat(value);\n                    return accumulator;\n                }\n                return value;\n            });\n        }\n        if ([\"linear\", \"time\"].includes(axisType)) {\n            // Replace empty string labels by undefined to make sure chartJS doesn't decide that \"\" is the same as 0\n            data = data.map((y, index) => ({ x: labels[index] || undefined, y }));\n        }\n        const backgroundColor = rgbaToHex(backgroundRGBA);\n        const dataset = {\n            label,\n            data,\n            tension: 0, // 0 -> render straight lines, which is much faster\n            borderColor: color,\n            backgroundColor,\n            pointBackgroundColor: color,\n            fill: areaChart ? getFillingMode(index, stackedChart) : false,\n        };\n        config.data.datasets.push(dataset);\n    }\n    let maxLength = 0;\n    const trendDatasets = [];\n    for (const [index, dataset] of config.data.datasets.entries()) {\n        if (definition.dataSets?.[index]?.label) {\n            const label = definition.dataSets[index].label;\n            dataset.label = label;\n        }\n        if (definition.dataSets?.[index]?.yAxisId) {\n            dataset[\"yAxisID\"] = definition.dataSets[index].yAxisId;\n        }\n        const trend = definition.dataSets?.[index].trend;\n        if (!trend?.display) {\n            continue;\n        }\n        const trendDataset = getTrendDatasetForLineChart(trend, dataset, axisType, locale);\n        if (trendDataset) {\n            maxLength = Math.max(maxLength, trendDataset.data.length);\n            trendDatasets.push(trendDataset);\n            dataSetsValues.push(trendDataset);\n        }\n    }\n    if (trendDatasets.length) {\n        /* We add a second x axis here to draw the trend lines, with the labels length being\n         * set so that the second axis points match the classical x axis\n         */\n        config.options.scales[TREND_LINE_XAXIS_ID] = {\n            ...xAxis,\n            type: \"category\",\n            labels: range(0, maxLength).map((x) => x.toString()),\n            offset: false,\n            display: false,\n        };\n        /* These datasets must be inserted after the original datasets to ensure the way we\n         * distinguish the originals and trendLine datasets after\n         */\n        trendDatasets.forEach((x) => config.data.datasets.push(x));\n    }\n    config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {\n        const displayTooltipTitle = axisType !== \"linear\" &&\n            tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);\n        return displayTooltipTitle ? undefined : \"\";\n    };\n    return {\n        chartJsConfig: config,\n        background: chart.background || BACKGROUND_CHART_COLOR,\n    };\n}\n\nclass ComboChart extends AbstractChart {\n    dataSets;\n    labelRange;\n    background;\n    legendPosition;\n    aggregated;\n    dataSetsHaveTitle;\n    dataSetDesign;\n    axesDesign;\n    type = \"combo\";\n    showValues;\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);\n        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);\n        this.background = definition.background;\n        this.legendPosition = definition.legendPosition;\n        this.aggregated = definition.aggregated;\n        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;\n        this.dataSetDesign = definition.dataSets;\n        this.axesDesign = definition.axesDesign;\n        this.showValues = definition.showValues;\n    }\n    static transformDefinition(definition, executed) {\n        return transformChartDefinitionWithDataSetsWithZone(definition, executed);\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, checkDataset, checkLabelRange);\n    }\n    getContextCreation() {\n        const range = [];\n        for (const [i, dataSet] of this.dataSets.entries()) {\n            range.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),\n            });\n        }\n        return {\n            ...this,\n            range,\n            auxiliaryRange: this.labelRange\n                ? this.getters.getRangeString(this.labelRange, this.sheetId)\n                : undefined,\n        };\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);\n    }\n    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {\n        const ranges = [];\n        for (const [i, dataSet] of dataSets.entries()) {\n            ranges.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),\n                type: this.dataSetDesign?.[i]?.type ?? (i ? \"line\" : \"bar\"),\n            });\n        }\n        return {\n            type: \"combo\",\n            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,\n            background: this.background,\n            dataSets: ranges,\n            legendPosition: this.legendPosition,\n            labelRange: labelRange\n                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)\n                : undefined,\n            title: this.title,\n            aggregated: this.aggregated,\n            axesDesign: this.axesDesign,\n            showValues: this.showValues,\n        };\n    }\n    getDefinitionForExcel() {\n        // Excel does not support aggregating labels\n        if (this.aggregated) {\n            return undefined;\n        }\n        const dataSets = this.dataSets\n            .map((ds) => toExcelDataset(this.getters, ds))\n            .filter((ds) => ds.range !== \"\" && ds.range !== CellErrorType.InvalidReference);\n        const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));\n        const definition = this.getDefinition();\n        return {\n            ...definition,\n            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),\n            fontColor: toXlsxHexColor(chartFontColor(this.background)),\n            dataSets,\n            labelRange,\n            verticalAxis: getDefinedAxis(definition),\n        };\n    }\n    updateRanges(applyChange) {\n        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);\n        if (!isStale) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);\n        return new ComboChart(definition, this.sheetId, this.getters);\n    }\n    static getDefinitionFromContextCreation(context) {\n        const dataSets = (context.range ?? []).map((ds, index) => ({\n            ...ds,\n            type: index ? \"line\" : \"bar\",\n        }));\n        return {\n            background: context.background,\n            dataSets,\n            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,\n            aggregated: context.aggregated,\n            legendPosition: context.legendPosition ?? \"top\",\n            title: context.title || { text: \"\" },\n            labelRange: context.auxiliaryRange || undefined,\n            type: \"combo\",\n            axesDesign: context.axesDesign,\n            showValues: context.showValues,\n        };\n    }\n    copyForSheetId(sheetId) {\n        const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);\n        const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);\n        return new ComboChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);\n        return new ComboChart(definition, sheetId, this.getters);\n    }\n}\nfunction createComboChartRuntime(chart, getters) {\n    const mainDataSetFormat = chart.dataSets.length\n        ? getChartDatasetFormat(getters, [chart.dataSets[0]])\n        : undefined;\n    const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets.slice(1));\n    const locale = getters.getLocale();\n    const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);\n    let labels = labelValues.formattedValues;\n    let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);\n    if (chart.dataSetsHaveTitle &&\n        dataSetsValues[0] &&\n        labels.length > dataSetsValues[0].data.length) {\n        labels.shift();\n    }\n    ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));\n    if (chart.aggregated) {\n        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));\n    }\n    const localeFormat = { format: mainDataSetFormat, locale };\n    const fontColor = chartFontColor(chart.background);\n    const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);\n    const legend = {\n        labels: { color: fontColor },\n    };\n    if (chart.legendPosition === \"none\") {\n        legend.display = false;\n    }\n    else {\n        legend.position = chart.legendPosition;\n    }\n    config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };\n    config.options.layout = {\n        padding: computeChartPadding({\n            displayTitle: !!chart.title.text,\n            displayLegend: chart.legendPosition === \"top\",\n        }),\n    };\n    config.options.scales = {\n        x: {\n            ticks: {\n                padding: 5,\n                color: fontColor,\n            },\n            title: getChartAxisTitleRuntime(chart.axesDesign?.x),\n        },\n    };\n    const leftVerticalAxis = {\n        beginAtZero: true, // the origin of the y axis is always zero\n        ticks: {\n            color: fontColor,\n            callback: formatTickValue({ format: mainDataSetFormat, locale }),\n        },\n    };\n    const rightVerticalAxis = {\n        beginAtZero: true, // the origin of the y axis is always zero\n        ticks: {\n            color: fontColor,\n            callback: formatTickValue({ format: lineDataSetsFormat, locale }),\n        },\n    };\n    const definition = chart.getDefinition();\n    const { useLeftAxis, useRightAxis } = getDefinedAxis(definition);\n    if (useLeftAxis) {\n        config.options.scales.y = {\n            ...leftVerticalAxis,\n            position: \"left\",\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y),\n        };\n    }\n    if (useRightAxis) {\n        config.options.scales.y1 = {\n            ...rightVerticalAxis,\n            position: \"right\",\n            grid: {\n                display: false,\n            },\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y1),\n        };\n    }\n    config.options.plugins.chartShowValuesPlugin = {\n        showValues: chart.showValues,\n        background: chart.background,\n        callback: formatTickValue({ format: mainDataSetFormat, locale }),\n    };\n    const colors = getChartColorsGenerator(definition, dataSetsValues.length);\n    let maxLength = 0;\n    const trendDatasets = [];\n    for (let [index, { label, data }] of dataSetsValues.entries()) {\n        const design = definition.dataSets[index];\n        const color = colors.next();\n        const type = design?.type ?? \"line\";\n        const dataset = {\n            label: design?.label ?? label,\n            data,\n            borderColor: color,\n            backgroundColor: color,\n            yAxisID: design?.yAxisId ?? \"y\",\n            type,\n            order: type === \"bar\" ? dataSetsValues.length + index : index,\n        };\n        config.data.datasets.push(dataset);\n        const trend = definition.dataSets?.[index].trend;\n        if (!trend?.display) {\n            continue;\n        }\n        maxLength = Math.max(maxLength, data.length);\n        const trendDataset = getTrendDatasetForBarChart(trend, dataset);\n        if (trendDataset) {\n            trendDatasets.push(trendDataset);\n        }\n    }\n    if (trendDatasets.length) {\n        /* We add a second x axis here to draw the trend lines, with the labels length being\n         * set so that the second axis points match the classical x axis\n         */\n        const trendLinesMaxLength = Math.max(...trendDatasets.map((trend) => trend.data.length));\n        config.options.scales[TREND_LINE_XAXIS_ID] = {\n            labels: Array(Math.round(trendLinesMaxLength)).fill(\"\"),\n            offset: false,\n            display: false,\n        };\n        /* These datasets must be inserted after the original datasets to ensure the way we\n         * distinguish the originals and trendLine datasets after\n         */\n        trendDatasets.forEach((x) => config.data.datasets.push(x));\n        config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {\n            return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)\n                ? undefined\n                : \"\";\n        };\n    }\n    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };\n}\n\nfunction isDataRangeValid(definition) {\n    return definition.dataRange && !rangeReference.test(definition.dataRange)\n        ? \"InvalidGaugeDataRange\" /* CommandResult.InvalidGaugeDataRange */\n        : \"Success\" /* CommandResult.Success */;\n}\nfunction checkRangeLimits(check, batchValidations) {\n    return batchValidations((definition) => {\n        if (definition.sectionRule) {\n            return check(definition.sectionRule.rangeMin, \"rangeMin\");\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }, (definition) => {\n        if (definition.sectionRule) {\n            return check(definition.sectionRule.rangeMax, \"rangeMax\");\n        }\n        return \"Success\" /* CommandResult.Success */;\n    });\n}\nfunction checkInflectionPointsValue(check, batchValidations) {\n    return batchValidations((definition) => {\n        if (definition.sectionRule) {\n            return check(definition.sectionRule.lowerInflectionPoint.value, \"lowerInflectionPointValue\");\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }, (definition) => {\n        if (definition.sectionRule) {\n            return check(definition.sectionRule.upperInflectionPoint.value, \"upperInflectionPointValue\");\n        }\n        return \"Success\" /* CommandResult.Success */;\n    });\n}\nfunction checkRangeMinBiggerThanRangeMax(definition) {\n    if (definition.sectionRule) {\n        if (Number(definition.sectionRule.rangeMin) >= Number(definition.sectionRule.rangeMax)) {\n            return \"GaugeRangeMinBiggerThanRangeMax\" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */;\n        }\n    }\n    return \"Success\" /* CommandResult.Success */;\n}\nfunction checkEmpty(value, valueName) {\n    if (value === \"\") {\n        switch (valueName) {\n            case \"rangeMin\":\n                return \"EmptyGaugeRangeMin\" /* CommandResult.EmptyGaugeRangeMin */;\n            case \"rangeMax\":\n                return \"EmptyGaugeRangeMax\" /* CommandResult.EmptyGaugeRangeMax */;\n        }\n    }\n    return \"Success\" /* CommandResult.Success */;\n}\nfunction checkNaN(value, valueName) {\n    if (isNaN(value)) {\n        switch (valueName) {\n            case \"rangeMin\":\n                return \"GaugeRangeMinNaN\" /* CommandResult.GaugeRangeMinNaN */;\n            case \"rangeMax\":\n                return \"GaugeRangeMaxNaN\" /* CommandResult.GaugeRangeMaxNaN */;\n            case \"lowerInflectionPointValue\":\n                return \"GaugeLowerInflectionPointNaN\" /* CommandResult.GaugeLowerInflectionPointNaN */;\n            case \"upperInflectionPointValue\":\n                return \"GaugeUpperInflectionPointNaN\" /* CommandResult.GaugeUpperInflectionPointNaN */;\n        }\n    }\n    return \"Success\" /* CommandResult.Success */;\n}\nclass GaugeChart extends AbstractChart {\n    dataRange;\n    sectionRule;\n    background;\n    type = \"gauge\";\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.dataRange = createValidRange(this.getters, this.sheetId, definition.dataRange);\n        this.sectionRule = definition.sectionRule;\n        this.background = definition.background;\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, isDataRangeValid, validator.chainValidations(checkRangeLimits(checkEmpty, validator.batchValidations), checkRangeLimits(checkNaN, validator.batchValidations), checkRangeMinBiggerThanRangeMax), validator.chainValidations(checkInflectionPointsValue(checkNaN, validator.batchValidations)));\n    }\n    static transformDefinition(definition, executed) {\n        let dataRangeZone;\n        if (definition.dataRange) {\n            dataRangeZone = transformZone(toUnboundedZone(definition.dataRange), executed);\n        }\n        return {\n            ...definition,\n            dataRange: dataRangeZone ? zoneToXc(dataRangeZone) : undefined,\n        };\n    }\n    static getDefinitionFromContextCreation(context) {\n        return {\n            background: context.background,\n            title: context.title || { text: \"\" },\n            type: \"gauge\",\n            dataRange: context.range ? context.range[0].dataRange : undefined,\n            sectionRule: {\n                colors: {\n                    lowerColor: DEFAULT_GAUGE_LOWER_COLOR,\n                    middleColor: DEFAULT_GAUGE_MIDDLE_COLOR,\n                    upperColor: DEFAULT_GAUGE_UPPER_COLOR,\n                },\n                rangeMin: \"0\",\n                rangeMax: \"100\",\n                lowerInflectionPoint: {\n                    type: \"percentage\",\n                    value: \"15\",\n                    operator: \"<=\",\n                },\n                upperInflectionPoint: {\n                    type: \"percentage\",\n                    value: \"40\",\n                    operator: \"<=\",\n                },\n            },\n        };\n    }\n    copyForSheetId(sheetId) {\n        const dataRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.dataRange);\n        const definition = this.getDefinitionWithSpecificRanges(dataRange, sheetId);\n        return new GaugeChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificRanges(this.dataRange, sheetId);\n        return new GaugeChart(definition, sheetId, this.getters);\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificRanges(this.dataRange);\n    }\n    getDefinitionWithSpecificRanges(dataRange, targetSheetId) {\n        return {\n            background: this.background,\n            sectionRule: this.sectionRule,\n            title: this.title,\n            type: \"gauge\",\n            dataRange: dataRange\n                ? this.getters.getRangeString(dataRange, targetSheetId || this.sheetId)\n                : undefined,\n        };\n    }\n    getDefinitionForExcel() {\n        // This kind of graph is not exportable in Excel\n        return undefined;\n    }\n    getContextCreation() {\n        return {\n            ...this,\n            range: this.dataRange\n                ? [{ dataRange: this.getters.getRangeString(this.dataRange, this.sheetId) }]\n                : undefined,\n        };\n    }\n    updateRanges(applyChange) {\n        const range = adaptChartRange(this.dataRange, applyChange);\n        if (this.dataRange === range) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificRanges(range);\n        return new GaugeChart(definition, this.sheetId, this.getters);\n    }\n}\nfunction createGaugeChartRuntime(chart, getters) {\n    const locale = getters.getLocale();\n    const chartColors = chart.sectionRule.colors;\n    let gaugeValue = undefined;\n    let formattedValue = undefined;\n    let format = undefined;\n    const dataRange = chart.dataRange;\n    if (dataRange !== undefined) {\n        const cell = getters.getEvaluatedCell({\n            sheetId: dataRange.sheetId,\n            col: dataRange.zone.left,\n            row: dataRange.zone.top,\n        });\n        if (cell.type === CellValueType.number) {\n            gaugeValue = cell.value;\n            formattedValue = cell.formattedValue;\n            format = cell.format;\n        }\n    }\n    const minValue = Number(chart.sectionRule.rangeMin);\n    const maxValue = Number(chart.sectionRule.rangeMax);\n    const lowerPoint = chart.sectionRule.lowerInflectionPoint;\n    const upperPoint = chart.sectionRule.upperInflectionPoint;\n    const lowerPointValue = getSectionThresholdValue(lowerPoint, minValue, maxValue);\n    const upperPointValue = getSectionThresholdValue(upperPoint, minValue, maxValue);\n    const inflectionValues = [];\n    const colors = [];\n    if (lowerPointValue !== undefined) {\n        inflectionValues.push({\n            value: lowerPointValue,\n            label: formatValue(lowerPointValue, { locale, format }),\n            operator: lowerPoint.operator,\n        });\n        colors.push(chartColors.lowerColor);\n    }\n    if (upperPointValue !== undefined && upperPointValue !== lowerPointValue) {\n        inflectionValues.push({\n            value: upperPointValue,\n            label: formatValue(upperPointValue, { locale, format }),\n            operator: upperPoint.operator,\n        });\n        colors.push(chartColors.middleColor);\n    }\n    if (upperPointValue !== undefined &&\n        lowerPointValue !== undefined &&\n        lowerPointValue > upperPointValue) {\n        inflectionValues.reverse();\n        colors.reverse();\n    }\n    colors.push(chartColors.upperColor);\n    return {\n        background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,\n        title: chart.title ?? { text: \"\" },\n        minValue: {\n            value: minValue,\n            label: formatValue(minValue, { locale, format }),\n        },\n        maxValue: {\n            value: maxValue,\n            label: formatValue(maxValue, { locale, format }),\n        },\n        gaugeValue: gaugeValue !== undefined && formattedValue\n            ? { value: gaugeValue, label: formattedValue }\n            : undefined,\n        inflectionValues,\n        colors,\n    };\n}\nfunction getSectionThresholdValue(threshold, minValue, maxValue) {\n    if (threshold.value === \"\" || isNaN(Number(threshold.value))) {\n        return undefined;\n    }\n    const numberValue = Number(threshold.value);\n    const value = threshold.type === \"number\"\n        ? numberValue\n        : minValue + ((maxValue - minValue) * numberValue) / 100;\n    return clip(value, minValue, maxValue);\n}\n\nclass LineChart extends AbstractChart {\n    dataSets;\n    labelRange;\n    background;\n    legendPosition;\n    labelsAsText;\n    stacked;\n    aggregated;\n    type = \"line\";\n    dataSetsHaveTitle;\n    cumulative;\n    dataSetDesign;\n    axesDesign;\n    fillArea;\n    showValues;\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);\n        this.labelRange = createValidRange(this.getters, sheetId, definition.labelRange);\n        this.background = definition.background;\n        this.legendPosition = definition.legendPosition;\n        this.labelsAsText = definition.labelsAsText;\n        this.stacked = definition.stacked;\n        this.aggregated = definition.aggregated;\n        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;\n        this.cumulative = definition.cumulative;\n        this.dataSetDesign = definition.dataSets;\n        this.axesDesign = definition.axesDesign;\n        this.fillArea = definition.fillArea;\n        this.showValues = definition.showValues;\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, checkDataset, checkLabelRange);\n    }\n    static transformDefinition(definition, executed) {\n        return transformChartDefinitionWithDataSetsWithZone(definition, executed);\n    }\n    static getDefinitionFromContextCreation(context) {\n        return {\n            background: context.background,\n            dataSets: context.range ?? [],\n            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,\n            labelsAsText: context.labelsAsText ?? false,\n            legendPosition: context.legendPosition ?? \"top\",\n            title: context.title || { text: \"\" },\n            type: \"line\",\n            labelRange: context.auxiliaryRange || undefined,\n            stacked: context.stacked ?? false,\n            aggregated: context.aggregated ?? false,\n            cumulative: context.cumulative ?? false,\n            axesDesign: context.axesDesign,\n            fillArea: context.fillArea,\n            showValues: context.showValues,\n        };\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);\n    }\n    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {\n        const ranges = [];\n        for (const [i, dataSet] of dataSets.entries()) {\n            ranges.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),\n            });\n        }\n        return {\n            type: \"line\",\n            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,\n            background: this.background,\n            dataSets: ranges,\n            legendPosition: this.legendPosition,\n            labelRange: labelRange\n                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)\n                : undefined,\n            title: this.title,\n            labelsAsText: this.labelsAsText,\n            stacked: this.stacked,\n            aggregated: this.aggregated,\n            cumulative: this.cumulative,\n            axesDesign: this.axesDesign,\n            fillArea: this.fillArea,\n            showValues: this.showValues,\n        };\n    }\n    getContextCreation() {\n        const range = [];\n        for (const [i, dataSet] of this.dataSets.entries()) {\n            range.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),\n            });\n        }\n        return {\n            ...this,\n            range,\n            auxiliaryRange: this.labelRange\n                ? this.getters.getRangeString(this.labelRange, this.sheetId)\n                : undefined,\n        };\n    }\n    updateRanges(applyChange) {\n        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);\n        if (!isStale) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);\n        return new LineChart(definition, this.sheetId, this.getters);\n    }\n    getDefinitionForExcel() {\n        // Excel does not support aggregating labels\n        if (this.aggregated)\n            return undefined;\n        const dataSets = this.dataSets\n            .map((ds) => toExcelDataset(this.getters, ds))\n            .filter((ds) => ds.range !== \"\" && ds.range !== CellErrorType.InvalidReference);\n        const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));\n        const definition = this.getDefinition();\n        return {\n            ...definition,\n            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),\n            fontColor: toXlsxHexColor(chartFontColor(this.background)),\n            dataSets,\n            labelRange,\n            verticalAxis: getDefinedAxis(definition),\n        };\n    }\n    copyForSheetId(sheetId) {\n        const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);\n        const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);\n        return new LineChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);\n        return new LineChart(definition, sheetId, this.getters);\n    }\n}\n\nclass PieChart extends AbstractChart {\n    dataSets;\n    labelRange;\n    background;\n    legendPosition;\n    type = \"pie\";\n    aggregated;\n    dataSetsHaveTitle;\n    isDoughnut;\n    showValues;\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);\n        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);\n        this.background = definition.background;\n        this.legendPosition = definition.legendPosition;\n        this.aggregated = definition.aggregated;\n        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;\n        this.isDoughnut = definition.isDoughnut;\n        this.showValues = definition.showValues;\n    }\n    static transformDefinition(definition, executed) {\n        return transformChartDefinitionWithDataSetsWithZone(definition, executed);\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, checkDataset, checkLabelRange);\n    }\n    static getDefinitionFromContextCreation(context) {\n        return {\n            background: context.background,\n            dataSets: context.range ?? [],\n            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,\n            legendPosition: context.legendPosition ?? \"top\",\n            title: context.title || { text: \"\" },\n            type: \"pie\",\n            labelRange: context.auxiliaryRange || undefined,\n            aggregated: context.aggregated ?? false,\n            isDoughnut: false,\n            showValues: context.showValues,\n        };\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);\n    }\n    getContextCreation() {\n        return {\n            ...this,\n            range: this.dataSets.map((ds) => ({\n                dataRange: this.getters.getRangeString(ds.dataRange, this.sheetId),\n            })),\n            auxiliaryRange: this.labelRange\n                ? this.getters.getRangeString(this.labelRange, this.sheetId)\n                : undefined,\n        };\n    }\n    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {\n        return {\n            type: \"pie\",\n            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,\n            background: this.background,\n            dataSets: dataSets.map((ds) => ({\n                dataRange: this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId),\n            })),\n            legendPosition: this.legendPosition,\n            labelRange: labelRange\n                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)\n                : undefined,\n            title: this.title,\n            aggregated: this.aggregated,\n            isDoughnut: this.isDoughnut,\n            showValues: this.showValues,\n        };\n    }\n    copyForSheetId(sheetId) {\n        const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);\n        const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);\n        return new PieChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);\n        return new PieChart(definition, sheetId, this.getters);\n    }\n    getDefinitionForExcel() {\n        // Excel does not support aggregating labels\n        if (this.aggregated)\n            return undefined;\n        const dataSets = this.dataSets\n            .map((ds) => toExcelDataset(this.getters, ds))\n            .filter((ds) => ds.range !== \"\" && ds.range !== CellErrorType.InvalidReference);\n        const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));\n        return {\n            ...this.getDefinition(),\n            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),\n            fontColor: toXlsxHexColor(chartFontColor(this.background)),\n            dataSets,\n            labelRange,\n        };\n    }\n    updateRanges(applyChange) {\n        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);\n        if (!isStale) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);\n        return new PieChart(definition, this.sheetId, this.getters);\n    }\n}\nfunction getPieConfiguration(chart, labels, localeFormat) {\n    const fontColor = chartFontColor(chart.background);\n    const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);\n    const legend = {\n        labels: { color: fontColor },\n    };\n    if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === \"none\") {\n        legend.display = false;\n    }\n    else {\n        legend.position = chart.legendPosition;\n    }\n    Object.assign(config.options.plugins.legend || {}, legend);\n    config.options.layout = {\n        padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },\n    };\n    config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {\n        return tooltipItems[0].dataset.label;\n    };\n    config.options.plugins.tooltip.callbacks.label = function (tooltipItem) {\n        const { format, locale } = localeFormat;\n        const data = tooltipItem.dataset.data;\n        const dataIndex = tooltipItem.dataIndex;\n        const percentage = calculatePercentage(data, dataIndex);\n        const xLabel = tooltipItem.label || tooltipItem.dataset.label;\n        const yLabel = tooltipItem.parsed.y ?? tooltipItem.parsed;\n        const toolTipFormat = !format && yLabel >= 1000 ? \"#,##\" : format;\n        const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });\n        return xLabel ? `${xLabel}: ${yLabelStr} (${percentage}%)` : `${yLabelStr} (${percentage}%)`;\n    };\n    config.options.plugins.chartShowValuesPlugin = {\n        showValues: chart.showValues,\n        callback: formatTickValue(localeFormat),\n    };\n    return config;\n}\nfunction getPieColors(colors, dataSetsValues) {\n    const pieColors = [];\n    const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));\n    for (let i = 0; i <= maxLength; i++) {\n        pieColors.push(colors.next());\n    }\n    return pieColors;\n}\nfunction calculatePercentage(dataset, dataIndex) {\n    const numericData = dataset.filter((value) => typeof value === \"number\");\n    const total = numericData.reduce((sum, value) => sum + value, 0);\n    if (!total) {\n        return \"\";\n    }\n    const percentage = (dataset[dataIndex] / total) * 100;\n    return percentage.toFixed(2);\n}\nfunction filterNegativeValues(labels, datasets) {\n    const dataPointsIndexes = labels.reduce((indexes, label, i) => {\n        const shouldKeep = datasets.some((dataset) => {\n            const dataPoint = dataset.data[i];\n            return typeof dataPoint !== \"number\" || dataPoint >= 0;\n        });\n        if (shouldKeep) {\n            indexes.push(i);\n        }\n        return indexes;\n    }, []);\n    const filteredLabels = dataPointsIndexes.map((i) => labels[i] || \"\");\n    const filteredDatasets = datasets.map((dataset) => ({\n        ...dataset,\n        data: dataPointsIndexes.map((i) => {\n            const dataPoint = dataset.data[i];\n            return typeof dataPoint !== \"number\" || dataPoint >= 0 ? dataPoint : 0;\n        }),\n    }));\n    return { labels: filteredLabels, dataSetsValues: filteredDatasets };\n}\nfunction createPieChartRuntime(chart, getters) {\n    const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);\n    let labels = labelValues.formattedValues;\n    let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);\n    if (chart.dataSetsHaveTitle &&\n        dataSetsValues[0] &&\n        labels.length > dataSetsValues[0].data.length) {\n        labels.shift();\n    }\n    ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));\n    if (chart.aggregated) {\n        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));\n    }\n    ({ dataSetsValues, labels } = filterNegativeValues(labels, dataSetsValues));\n    const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);\n    const locale = getters.getLocale();\n    const config = getPieConfiguration(chart, labels, { format: dataSetFormat, locale });\n    const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));\n    const backgroundColor = getPieColors(new ColorGenerator(dataSetsLength), dataSetsValues);\n    for (const { label, data } of dataSetsValues) {\n        const dataset = {\n            label,\n            data,\n            borderColor: BACKGROUND_CHART_COLOR,\n            backgroundColor,\n            hoverOffset: 30,\n        };\n        config.data.datasets.push(dataset);\n    }\n    if (chart.isDoughnut) {\n        config.type = \"doughnut\";\n    }\n    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };\n}\n\nclass PyramidChart extends AbstractChart {\n    dataSets;\n    labelRange;\n    background;\n    legendPosition;\n    aggregated;\n    type = \"pyramid\";\n    dataSetsHaveTitle;\n    dataSetDesign;\n    axesDesign;\n    horizontal = true;\n    stacked = true;\n    showValues;\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle).slice(0, 2);\n        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);\n        this.background = definition.background;\n        this.legendPosition = definition.legendPosition;\n        this.aggregated = definition.aggregated;\n        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;\n        this.dataSetDesign = definition.dataSets;\n        this.axesDesign = definition.axesDesign;\n        this.showValues = definition.showValues;\n    }\n    static transformDefinition(definition, executed) {\n        return transformChartDefinitionWithDataSetsWithZone(definition, executed);\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, checkDataset, checkLabelRange);\n    }\n    static getDefinitionFromContextCreation(context) {\n        return {\n            background: context.background,\n            dataSets: context.range ?? [],\n            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,\n            aggregated: context.aggregated ?? false,\n            legendPosition: context.legendPosition ?? \"top\",\n            title: context.title || { text: \"\" },\n            type: \"pyramid\",\n            labelRange: context.auxiliaryRange || undefined,\n            axesDesign: context.axesDesign,\n            horizontal: true,\n            stacked: true,\n            showValues: context.showValues,\n        };\n    }\n    getContextCreation() {\n        const range = [];\n        for (const [i, dataSet] of this.dataSets.entries()) {\n            range.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),\n            });\n        }\n        return {\n            ...this,\n            range,\n            auxiliaryRange: this.labelRange\n                ? this.getters.getRangeString(this.labelRange, this.sheetId)\n                : undefined,\n        };\n    }\n    copyForSheetId(sheetId) {\n        const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);\n        const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);\n        return new PyramidChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);\n        return new PyramidChart(definition, sheetId, this.getters);\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);\n    }\n    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {\n        const ranges = [];\n        for (const [i, dataSet] of dataSets.entries()) {\n            ranges.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),\n            });\n        }\n        return {\n            type: \"pyramid\",\n            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,\n            background: this.background,\n            dataSets: ranges,\n            legendPosition: this.legendPosition,\n            labelRange: labelRange\n                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)\n                : undefined,\n            title: this.title,\n            aggregated: this.aggregated,\n            axesDesign: this.axesDesign,\n            horizontal: true,\n            stacked: true,\n            showValues: this.showValues,\n        };\n    }\n    getDefinitionForExcel() {\n        return undefined;\n    }\n    updateRanges(applyChange) {\n        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);\n        if (!isStale) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);\n        return new PyramidChart(definition, this.sheetId, this.getters);\n    }\n}\nfunction createPyramidChartRuntime(chart, getters) {\n    const barDef = { ...chart.getDefinition(), type: \"bar\" };\n    const barChart = new BarChart(barDef, chart.sheetId, getters);\n    const barRuntime = createBarChartRuntime(barChart, getters);\n    const config = barRuntime.chartJsConfig;\n    let datasets = config.data?.datasets;\n    if (datasets && datasets[0]) {\n        datasets[0].data = datasets[0].data.map((value) => (value > 0 ? value : 0));\n    }\n    if (datasets && datasets[1]) {\n        datasets[1].data = datasets[1].data.map((value) => (value > 0 ? -value : 0));\n    }\n    const maxValue = Math.max(...config.data?.datasets.map((dataSet) => Math.max(...dataSet.data.map(Math.abs))));\n    const scales = config.options.scales;\n    const scalesXCallback = scales.x.ticks.callback;\n    scales.x.ticks.callback = (value) => scalesXCallback(Math.abs(value));\n    scales.x.suggestedMin = -maxValue;\n    scales.x.suggestedMax = maxValue;\n    const tooltipLabelCallback = config.options.plugins.tooltip.callbacks.label;\n    config.options.plugins.tooltip.callbacks.label = (item) => {\n        const tooltipItem = { ...item, parsed: { y: item.parsed.y, x: Math.abs(item.parsed.x) } };\n        return tooltipLabelCallback(tooltipItem);\n    };\n    const callback = config.options.plugins.chartShowValuesPlugin.callback;\n    config.options.plugins.chartShowValuesPlugin.callback = (x) => callback(Math.abs(x));\n    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };\n}\n\nclass ScatterChart extends AbstractChart {\n    dataSets;\n    labelRange;\n    background;\n    legendPosition;\n    labelsAsText;\n    aggregated;\n    type = \"scatter\";\n    dataSetsHaveTitle;\n    dataSetDesign;\n    axesDesign;\n    showValues;\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);\n        this.labelRange = createValidRange(this.getters, sheetId, definition.labelRange);\n        this.background = definition.background;\n        this.legendPosition = definition.legendPosition;\n        this.labelsAsText = definition.labelsAsText;\n        this.aggregated = definition.aggregated;\n        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;\n        this.dataSetDesign = definition.dataSets;\n        this.axesDesign = definition.axesDesign;\n        this.showValues = definition.showValues;\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, checkDataset, checkLabelRange);\n    }\n    static transformDefinition(definition, executed) {\n        return transformChartDefinitionWithDataSetsWithZone(definition, executed);\n    }\n    static getDefinitionFromContextCreation(context) {\n        return {\n            background: context.background,\n            dataSets: context.range ?? [],\n            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,\n            labelsAsText: context.labelsAsText ?? false,\n            legendPosition: context.legendPosition ?? \"top\",\n            title: context.title || { text: \"\" },\n            type: \"scatter\",\n            labelRange: context.auxiliaryRange || undefined,\n            aggregated: context.aggregated ?? false,\n            axesDesign: context.axesDesign,\n            showValues: context.showValues,\n        };\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);\n    }\n    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {\n        const ranges = [];\n        for (const [i, dataSet] of dataSets.entries()) {\n            ranges.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),\n            });\n        }\n        return {\n            type: \"scatter\",\n            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,\n            background: this.background,\n            dataSets: ranges,\n            legendPosition: this.legendPosition,\n            labelRange: labelRange\n                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)\n                : undefined,\n            title: this.title,\n            labelsAsText: this.labelsAsText,\n            aggregated: this.aggregated,\n            axesDesign: this.axesDesign,\n            showValues: this.showValues,\n        };\n    }\n    getContextCreation() {\n        const range = [];\n        for (const [i, dataSet] of this.dataSets.entries()) {\n            range.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),\n            });\n        }\n        return {\n            ...this,\n            range,\n            auxiliaryRange: this.labelRange\n                ? this.getters.getRangeString(this.labelRange, this.sheetId)\n                : undefined,\n        };\n    }\n    updateRanges(applyChange) {\n        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);\n        if (!isStale) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);\n        return new ScatterChart(definition, this.sheetId, this.getters);\n    }\n    getDefinitionForExcel() {\n        // Excel does not support aggregating labels\n        if (this.aggregated) {\n            return undefined;\n        }\n        const dataSets = this.dataSets\n            .map((ds) => toExcelDataset(this.getters, ds))\n            .filter((ds) => ds.range !== \"\");\n        const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));\n        const definition = this.getDefinition();\n        return {\n            ...definition,\n            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),\n            fontColor: toXlsxHexColor(chartFontColor(this.background)),\n            dataSets,\n            labelRange,\n            verticalAxis: getDefinedAxis(definition),\n        };\n    }\n    copyForSheetId(sheetId) {\n        const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);\n        const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);\n        return new ScatterChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);\n        return new ScatterChart(definition, sheetId, this.getters);\n    }\n}\nfunction createScatterChartRuntime(chart, getters) {\n    const { chartJsConfig, background } = createLineOrScatterChartRuntime(chart, getters);\n    // use chartJS line chart and disable the lines instead of chartJS scatter chart. This is because the scatter chart\n    // have less options than the line chart (it only works with linear labels)\n    chartJsConfig.type = \"line\";\n    for (const dataSet of chartJsConfig.data.datasets) {\n        dataSet.showLine = \"showLine\" in dataSet ? dataSet.showLine : false;\n    }\n    return { chartJsConfig, background };\n}\n\nclass WaterfallChart extends AbstractChart {\n    dataSets;\n    labelRange;\n    background;\n    verticalAxisPosition;\n    legendPosition;\n    aggregated;\n    type = \"waterfall\";\n    dataSetsHaveTitle;\n    showSubTotals;\n    firstValueAsSubtotal;\n    showConnectorLines;\n    positiveValuesColor;\n    negativeValuesColor;\n    subTotalValuesColor;\n    dataSetDesign;\n    axesDesign;\n    showValues;\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);\n        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);\n        this.background = definition.background;\n        this.verticalAxisPosition = definition.verticalAxisPosition;\n        this.legendPosition = definition.legendPosition;\n        this.aggregated = definition.aggregated;\n        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;\n        this.showSubTotals = definition.showSubTotals;\n        this.showConnectorLines = definition.showConnectorLines;\n        this.positiveValuesColor = definition.positiveValuesColor;\n        this.negativeValuesColor = definition.negativeValuesColor;\n        this.subTotalValuesColor = definition.subTotalValuesColor;\n        this.firstValueAsSubtotal = definition.firstValueAsSubtotal;\n        this.dataSetDesign = definition.dataSets;\n        this.axesDesign = definition.axesDesign;\n        this.showValues = definition.showValues;\n    }\n    static transformDefinition(definition, executed) {\n        return transformChartDefinitionWithDataSetsWithZone(definition, executed);\n    }\n    static validateChartDefinition(validator, definition) {\n        return validator.checkValidations(definition, checkDataset, checkLabelRange);\n    }\n    static getDefinitionFromContextCreation(context) {\n        return {\n            background: context.background,\n            dataSets: context.range ? context.range : [],\n            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,\n            aggregated: context.aggregated ?? false,\n            legendPosition: context.legendPosition ?? \"top\",\n            title: context.title || { text: \"\" },\n            type: \"waterfall\",\n            verticalAxisPosition: \"left\",\n            labelRange: context.auxiliaryRange || undefined,\n            showSubTotals: context.showSubTotals ?? false,\n            showConnectorLines: context.showConnectorLines ?? true,\n            firstValueAsSubtotal: context.firstValueAsSubtotal ?? false,\n            axesDesign: context.axesDesign,\n            showValues: context.showValues,\n        };\n    }\n    getContextCreation() {\n        const range = [];\n        for (const [i, dataSet] of this.dataSets.entries()) {\n            range.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),\n            });\n        }\n        return {\n            ...this,\n            range,\n            auxiliaryRange: this.labelRange\n                ? this.getters.getRangeString(this.labelRange, this.sheetId)\n                : undefined,\n        };\n    }\n    copyForSheetId(sheetId) {\n        const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);\n        const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);\n        return new WaterfallChart(definition, sheetId, this.getters);\n    }\n    copyInSheetId(sheetId) {\n        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);\n        return new WaterfallChart(definition, sheetId, this.getters);\n    }\n    getDefinition() {\n        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);\n    }\n    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {\n        const ranges = [];\n        for (const [i, dataSet] of dataSets.entries()) {\n            ranges.push({\n                ...this.dataSetDesign?.[i],\n                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),\n            });\n        }\n        return {\n            type: \"waterfall\",\n            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,\n            background: this.background,\n            dataSets: ranges,\n            legendPosition: this.legendPosition,\n            verticalAxisPosition: this.verticalAxisPosition,\n            labelRange: labelRange\n                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)\n                : undefined,\n            title: this.title,\n            aggregated: this.aggregated,\n            showSubTotals: this.showSubTotals,\n            showConnectorLines: this.showConnectorLines,\n            positiveValuesColor: this.positiveValuesColor,\n            negativeValuesColor: this.negativeValuesColor,\n            subTotalValuesColor: this.subTotalValuesColor,\n            firstValueAsSubtotal: this.firstValueAsSubtotal,\n            axesDesign: this.axesDesign,\n            showValues: this.showValues,\n        };\n    }\n    getDefinitionForExcel() {\n        // TODO: implement export excel\n        return undefined;\n    }\n    updateRanges(applyChange) {\n        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);\n        if (!isStale) {\n            return this;\n        }\n        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);\n        return new WaterfallChart(definition, this.sheetId, this.getters);\n    }\n}\nfunction getWaterfallConfiguration(chart, labels, dataSeriesLabels, localeFormat) {\n    const { locale, format } = localeFormat;\n    const fontColor = chartFontColor(chart.background);\n    const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);\n    const negativeColor = chart.negativeValuesColor || CHART_WATERFALL_NEGATIVE_COLOR;\n    const positiveColor = chart.positiveValuesColor || CHART_WATERFALL_POSITIVE_COLOR;\n    const subTotalColor = chart.subTotalValuesColor || CHART_WATERFALL_SUBTOTAL_COLOR;\n    const legend = {\n        labels: {\n            generateLabels: () => {\n                const legendValues = [\n                    {\n                        text: _t(\"Positive values\"),\n                        fontColor,\n                        fillStyle: positiveColor,\n                        strokeStyle: positiveColor,\n                    },\n                    {\n                        text: _t(\"Negative values\"),\n                        fontColor,\n                        fillStyle: negativeColor,\n                        strokeStyle: negativeColor,\n                    },\n                ];\n                if (chart.showSubTotals || chart.firstValueAsSubtotal) {\n                    legendValues.push({\n                        text: _t(\"Subtotals\"),\n                        fontColor,\n                        fillStyle: subTotalColor,\n                        strokeStyle: subTotalColor,\n                    });\n                }\n                return legendValues;\n            },\n        },\n    };\n    if (chart.legendPosition === \"none\") {\n        legend.display = false;\n    }\n    else {\n        legend.position = chart.legendPosition;\n    }\n    config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };\n    config.options.layout = {\n        padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },\n    };\n    config.options.scales = {\n        x: {\n            ticks: {\n                padding: 5,\n                color: fontColor,\n            },\n            grid: {\n                display: false,\n            },\n            title: getChartAxisTitleRuntime(chart.axesDesign?.x),\n        },\n        y: {\n            position: chart.verticalAxisPosition,\n            ticks: {\n                color: fontColor,\n                callback: (value) => {\n                    value = Number(value);\n                    if (isNaN(value))\n                        return value;\n                    return formatValue(value, {\n                        locale,\n                        format: !format && Math.abs(value) > 1000 ? \"#,##\" : format,\n                    });\n                },\n            },\n            grid: {\n                lineWidth: (context) => {\n                    return context.tick.value === 0 ? 2 : 1;\n                },\n            },\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y),\n        },\n    };\n    config.options.plugins.tooltip = {\n        callbacks: {\n            label: function (tooltipItem) {\n                const [lastValue, currentValue] = tooltipItem.raw;\n                const yLabel = currentValue - lastValue;\n                const dataSeriesIndex = Math.floor(tooltipItem.dataIndex / labels.length);\n                const dataSeriesLabel = dataSeriesLabels[dataSeriesIndex];\n                const toolTipFormat = !format && Math.abs(yLabel) > 1000 ? \"#,##\" : format;\n                const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });\n                return dataSeriesLabel ? `${dataSeriesLabel}: ${yLabelStr}` : yLabelStr;\n            },\n        },\n    };\n    config.options.plugins.waterfallLinesPlugin = { showConnectorLines: chart.showConnectorLines };\n    config.options.plugins.chartShowValuesPlugin = {\n        showValues: chart.showValues,\n        background: chart.background,\n        callback: formatTickValue(localeFormat),\n    };\n    return config;\n}\nfunction createWaterfallChartRuntime(chart, getters) {\n    const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);\n    let labels = labelValues.formattedValues;\n    let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);\n    if (chart.dataSetsHaveTitle &&\n        dataSetsValues[0] &&\n        labels.length > dataSetsValues[0].data.length) {\n        labels.shift();\n    }\n    ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));\n    if (chart.aggregated) {\n        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));\n    }\n    if (chart.showSubTotals) {\n        labels.push(_t(\"Subtotal\"));\n    }\n    const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);\n    const locale = getters.getLocale();\n    const dataSeriesLabels = dataSetsValues.map((dataSet) => dataSet.label);\n    const config = getWaterfallConfiguration(chart, labels, dataSeriesLabels, {\n        format: dataSetFormat,\n        locale,\n    });\n    config.type = \"bar\";\n    const negativeColor = chart.negativeValuesColor || CHART_WATERFALL_NEGATIVE_COLOR;\n    const positiveColor = chart.positiveValuesColor || CHART_WATERFALL_POSITIVE_COLOR;\n    const subTotalColor = chart.subTotalValuesColor || CHART_WATERFALL_SUBTOTAL_COLOR;\n    const backgroundColor = [];\n    const datasetValues = [];\n    const dataset = {\n        label: \"\",\n        data: datasetValues,\n        backgroundColor,\n    };\n    const labelsWithSubTotals = [];\n    let lastValue = 0;\n    for (const dataSetsValue of dataSetsValues) {\n        for (let i = 0; i < dataSetsValue.data.length; i++) {\n            const data = dataSetsValue.data[i];\n            labelsWithSubTotals.push(labels[i]);\n            if (isNaN(Number(data))) {\n                datasetValues.push([lastValue, lastValue]);\n                backgroundColor.push(\"\");\n                continue;\n            }\n            datasetValues.push([lastValue, data + lastValue]);\n            let color = data >= 0 ? positiveColor : negativeColor;\n            if (i === 0 && dataSetsValue === dataSetsValues[0] && chart.firstValueAsSubtotal) {\n                color = subTotalColor;\n            }\n            backgroundColor.push(color);\n            lastValue += data;\n        }\n        if (chart.showSubTotals) {\n            labelsWithSubTotals.push(_t(\"Subtotal\"));\n            datasetValues.push([0, lastValue]);\n            backgroundColor.push(subTotalColor);\n        }\n    }\n    config.data.datasets.push(dataset);\n    config.data.labels = labelsWithSubTotals.map(truncateLabel);\n    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };\n}\n\n/**\n * This registry is intended to map a cell content (raw string) to\n * an instance of a cell.\n */\nconst chartRegistry = new Registry();\nchartRegistry.add(\"bar\", {\n    match: (type) => type === \"bar\",\n    createChart: (definition, sheetId, getters) => new BarChart(definition, sheetId, getters),\n    getChartRuntime: createBarChartRuntime,\n    validateChartDefinition: BarChart.validateChartDefinition,\n    transformDefinition: BarChart.transformDefinition,\n    getChartDefinitionFromContextCreation: BarChart.getDefinitionFromContextCreation,\n    sequence: 10,\n});\nchartRegistry.add(\"combo\", {\n    match: (type) => type === \"combo\",\n    createChart: (definition, sheetId, getters) => new ComboChart(definition, sheetId, getters),\n    getChartRuntime: createComboChartRuntime,\n    validateChartDefinition: ComboChart.validateChartDefinition,\n    transformDefinition: ComboChart.transformDefinition,\n    getChartDefinitionFromContextCreation: ComboChart.getDefinitionFromContextCreation,\n    sequence: 15,\n});\nchartRegistry.add(\"line\", {\n    match: (type) => type === \"line\",\n    createChart: (definition, sheetId, getters) => new LineChart(definition, sheetId, getters),\n    getChartRuntime: createLineOrScatterChartRuntime,\n    validateChartDefinition: LineChart.validateChartDefinition,\n    transformDefinition: LineChart.transformDefinition,\n    getChartDefinitionFromContextCreation: LineChart.getDefinitionFromContextCreation,\n    sequence: 20,\n});\nchartRegistry.add(\"pie\", {\n    match: (type) => type === \"pie\",\n    createChart: (definition, sheetId, getters) => new PieChart(definition, sheetId, getters),\n    getChartRuntime: createPieChartRuntime,\n    validateChartDefinition: PieChart.validateChartDefinition,\n    transformDefinition: PieChart.transformDefinition,\n    getChartDefinitionFromContextCreation: PieChart.getDefinitionFromContextCreation,\n    sequence: 30,\n});\nchartRegistry.add(\"scorecard\", {\n    match: (type) => type === \"scorecard\",\n    createChart: (definition, sheetId, getters) => new ScorecardChart$1(definition, sheetId, getters),\n    getChartRuntime: createScorecardChartRuntime,\n    validateChartDefinition: ScorecardChart$1.validateChartDefinition,\n    transformDefinition: ScorecardChart$1.transformDefinition,\n    getChartDefinitionFromContextCreation: ScorecardChart$1.getDefinitionFromContextCreation,\n    sequence: 40,\n});\nchartRegistry.add(\"gauge\", {\n    match: (type) => type === \"gauge\",\n    createChart: (definition, sheetId, getters) => new GaugeChart(definition, sheetId, getters),\n    getChartRuntime: createGaugeChartRuntime,\n    validateChartDefinition: GaugeChart.validateChartDefinition,\n    transformDefinition: GaugeChart.transformDefinition,\n    getChartDefinitionFromContextCreation: GaugeChart.getDefinitionFromContextCreation,\n    sequence: 50,\n});\nchartRegistry.add(\"scatter\", {\n    match: (type) => type === \"scatter\",\n    createChart: (definition, sheetId, getters) => new ScatterChart(definition, sheetId, getters),\n    getChartRuntime: createScatterChartRuntime,\n    validateChartDefinition: ScatterChart.validateChartDefinition,\n    transformDefinition: ScatterChart.transformDefinition,\n    getChartDefinitionFromContextCreation: ScatterChart.getDefinitionFromContextCreation,\n    sequence: 60,\n});\nchartRegistry.add(\"waterfall\", {\n    match: (type) => type === \"waterfall\",\n    createChart: (definition, sheetId, getters) => new WaterfallChart(definition, sheetId, getters),\n    getChartRuntime: createWaterfallChartRuntime,\n    validateChartDefinition: WaterfallChart.validateChartDefinition,\n    transformDefinition: WaterfallChart.transformDefinition,\n    getChartDefinitionFromContextCreation: WaterfallChart.getDefinitionFromContextCreation,\n    sequence: 70,\n});\nchartRegistry.add(\"pyramid\", {\n    match: (type) => type === \"pyramid\",\n    createChart: (definition, sheetId, getters) => new PyramidChart(definition, sheetId, getters),\n    getChartRuntime: createPyramidChartRuntime,\n    validateChartDefinition: PyramidChart.validateChartDefinition,\n    transformDefinition: PyramidChart.transformDefinition,\n    getChartDefinitionFromContextCreation: PyramidChart.getDefinitionFromContextCreation,\n    sequence: 80,\n});\nconst chartComponentRegistry = new Registry();\nchartComponentRegistry.add(\"line\", ChartJsComponent);\nchartComponentRegistry.add(\"bar\", ChartJsComponent);\nchartComponentRegistry.add(\"combo\", ChartJsComponent);\nchartComponentRegistry.add(\"pie\", ChartJsComponent);\nchartComponentRegistry.add(\"gauge\", GaugeChartComponent);\nchartComponentRegistry.add(\"scatter\", ChartJsComponent);\nchartComponentRegistry.add(\"scorecard\", ScorecardChart);\nchartComponentRegistry.add(\"waterfall\", ChartJsComponent);\nchartComponentRegistry.add(\"pyramid\", ChartJsComponent);\nconst chartCategories = {\n    line: _t(\"Line\"),\n    column: _t(\"Column\"),\n    bar: _t(\"Bar\"),\n    area: _t(\"Area\"),\n    pie: _t(\"Pie\"),\n    misc: _t(\"Miscellaneous\"),\n};\nconst chartSubtypeRegistry = new Registry();\nchartSubtypeRegistry\n    .add(\"line\", {\n    matcher: (definition) => definition.type === \"line\" && !definition.stacked && !definition.fillArea,\n    displayName: _t(\"Line\"),\n    chartType: \"line\",\n    chartSubtype: \"line\",\n    subtypeDefinition: { stacked: false, fillArea: false },\n    category: \"line\",\n    preview: \"o-spreadsheet-ChartPreview.LINE_CHART\",\n})\n    .add(\"stacked_line\", {\n    matcher: (definition) => definition.type === \"line\" && !definition.fillArea && !!definition.stacked,\n    displayName: _t(\"Stacked Line\"),\n    chartType: \"line\",\n    chartSubtype: \"stacked_line\",\n    subtypeDefinition: { stacked: true, fillArea: false },\n    category: \"line\",\n    preview: \"o-spreadsheet-ChartPreview.STACKED_LINE_CHART\",\n})\n    .add(\"area\", {\n    matcher: (definition) => definition.type === \"line\" && !definition.stacked && !!definition.fillArea,\n    displayName: _t(\"Area\"),\n    chartType: \"line\",\n    chartSubtype: \"area\",\n    subtypeDefinition: { stacked: false, fillArea: true },\n    category: \"area\",\n    preview: \"o-spreadsheet-ChartPreview.AREA_CHART\",\n})\n    .add(\"stacked_area\", {\n    matcher: (definition) => definition.type === \"line\" && definition.stacked && !!definition.fillArea,\n    displayName: _t(\"Stacked Area\"),\n    chartType: \"line\",\n    chartSubtype: \"stacked_area\",\n    subtypeDefinition: { stacked: true, fillArea: true },\n    category: \"area\",\n    preview: \"o-spreadsheet-ChartPreview.STACKED_AREA_CHART\",\n})\n    .add(\"scatter\", {\n    displayName: _t(\"Scatter\"),\n    chartType: \"scatter\",\n    chartSubtype: \"scatter\",\n    category: \"misc\",\n    preview: \"o-spreadsheet-ChartPreview.SCATTER_CHART\",\n})\n    .add(\"column\", {\n    matcher: (definition) => definition.type === \"bar\" && !definition.stacked && !definition.horizontal,\n    displayName: _t(\"Column\"),\n    chartType: \"bar\",\n    chartSubtype: \"column\",\n    subtypeDefinition: { stacked: false, horizontal: false },\n    category: \"column\",\n    preview: \"o-spreadsheet-ChartPreview.COLUMN_CHART\",\n})\n    .add(\"stacked_column\", {\n    matcher: (definition) => definition.type === \"bar\" && definition.stacked && !definition.horizontal,\n    displayName: _t(\"Stacked Column\"),\n    chartType: \"bar\",\n    chartSubtype: \"stacked_column\",\n    subtypeDefinition: { stacked: true, horizontal: false },\n    category: \"column\",\n    preview: \"o-spreadsheet-ChartPreview.STACKED_COLUMN_CHART\",\n})\n    .add(\"bar\", {\n    matcher: (definition) => definition.type === \"bar\" && !definition.stacked && !!definition.horizontal,\n    displayName: _t(\"Bar\"),\n    chartType: \"bar\",\n    chartSubtype: \"bar\",\n    subtypeDefinition: { horizontal: true, stacked: false },\n    category: \"bar\",\n    preview: \"o-spreadsheet-ChartPreview.BAR_CHART\",\n})\n    .add(\"stacked_bar\", {\n    matcher: (definition) => definition.type === \"bar\" && definition.stacked && !!definition.horizontal,\n    displayName: _t(\"Stacked Bar\"),\n    chartType: \"bar\",\n    chartSubtype: \"stacked_bar\",\n    subtypeDefinition: { horizontal: true, stacked: true },\n    category: \"bar\",\n    preview: \"o-spreadsheet-ChartPreview.STACKED_BAR_CHART\",\n})\n    .add(\"combo\", {\n    displayName: _t(\"Combo\"),\n    chartSubtype: \"combo\",\n    chartType: \"combo\",\n    category: \"line\",\n    preview: \"o-spreadsheet-ChartPreview.COMBO_CHART\",\n})\n    .add(\"pie\", {\n    matcher: (definition) => definition.type === \"pie\" && !definition.isDoughnut,\n    displayName: _t(\"Pie\"),\n    chartSubtype: \"pie\",\n    chartType: \"pie\",\n    subtypeDefinition: { isDoughnut: false },\n    category: \"pie\",\n    preview: \"o-spreadsheet-ChartPreview.PIE_CHART\",\n})\n    .add(\"doughnut\", {\n    matcher: (definition) => definition.type === \"pie\" && !!definition.isDoughnut,\n    displayName: _t(\"Doughnut\"),\n    chartSubtype: \"doughnut\",\n    chartType: \"pie\",\n    subtypeDefinition: { isDoughnut: true },\n    category: \"pie\",\n    preview: \"o-spreadsheet-ChartPreview.DOUGHNUT_CHART\",\n})\n    .add(\"gauge\", {\n    displayName: _t(\"Gauge\"),\n    chartSubtype: \"gauge\",\n    chartType: \"gauge\",\n    category: \"misc\",\n    preview: \"o-spreadsheet-ChartPreview.GAUGE_CHART\",\n})\n    .add(\"scorecard\", {\n    displayName: _t(\"Scorecard\"),\n    chartSubtype: \"scorecard\",\n    chartType: \"scorecard\",\n    category: \"misc\",\n    preview: \"o-spreadsheet-ChartPreview.SCORECARD_CHART\",\n})\n    .add(\"waterfall\", {\n    displayName: _t(\"Waterfall\"),\n    chartSubtype: \"waterfall\",\n    chartType: \"waterfall\",\n    category: \"misc\",\n    preview: \"o-spreadsheet-ChartPreview.WATERFALL_CHART\",\n})\n    .add(\"pyramid\", {\n    displayName: _t(\"Population Pyramid\"),\n    chartSubtype: \"pyramid\",\n    chartType: \"pyramid\",\n    category: \"misc\",\n    preview: \"o-spreadsheet-ChartPreview.POPULATION_PYRAMID_CHART\",\n});\n\n/**\n * Registry intended to support usual currencies. It is mainly used to create\n * currency formats that can be selected or modified when customizing formats.\n */\nconst currenciesRegistry = new Registry();\n\n// -----------------------------------------------------------------------------\n// STYLE\n// -----------------------------------------------------------------------------\ncss /* scss */ `\n  .o-chart-container {\n    width: 100%;\n    height: 100%;\n    position: relative;\n  }\n`;\nclass ChartFigure extends Component {\n    static template = \"o-spreadsheet-ChartFigure\";\n    static props = {\n        figure: Object,\n        onFigureDeleted: Function,\n    };\n    static components = {};\n    onDoubleClick() {\n        this.env.model.dispatch(\"SELECT_FIGURE\", { id: this.props.figure.id });\n        this.env.openSidePanel(\"ChartPanel\");\n    }\n    get chartType() {\n        return this.env.model.getters.getChartType(this.props.figure.id);\n    }\n    get chartComponent() {\n        const type = this.chartType;\n        const component = chartComponentRegistry.get(type);\n        if (!component) {\n            throw new Error(`Component is not defined for type ${type}`);\n        }\n        return component;\n    }\n}\n\nclass ImageFigure extends Component {\n    static template = \"o-spreadsheet-ImageFigure\";\n    static props = {\n        figure: Object,\n        onFigureDeleted: Function,\n    };\n    static components = {};\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    get figureId() {\n        return this.props.figure.id;\n    }\n    get getImagePath() {\n        return this.env.model.getters.getImagePath(this.figureId);\n    }\n}\n\nfunction centerFigurePosition(getters, size) {\n    const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();\n    const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();\n    const dim = getters.getSheetViewDimension();\n    const rect = getters.getVisibleRect(getters.getActiveMainViewport());\n    const scrollableViewportWidth = Math.min(rect.width, dim.width - offsetCorrectionX);\n    const scrollableViewportHeight = Math.min(rect.height, dim.height - offsetCorrectionY);\n    const position = {\n        x: offsetCorrectionX + scrollX + Math.max(0, (scrollableViewportWidth - size.width) / 2),\n        y: offsetCorrectionY + scrollY + Math.max(0, (scrollableViewportHeight - size.height) / 2),\n    }; // Position at the center of the scrollable viewport\n    return position;\n}\nfunction getMaxFigureSize(getters, figureSize) {\n    const size = deepCopy(figureSize);\n    const dim = getters.getSheetViewDimension();\n    const maxWidth = dim.width;\n    const maxHeight = dim.height;\n    if (size.width > maxWidth) {\n        const ratio = maxWidth / size.width;\n        size.width = maxWidth;\n        size.height = size.height * ratio;\n    }\n    if (size.height > maxHeight) {\n        const ratio = maxHeight / size.height;\n        size.height = maxHeight;\n        size.width = size.width * ratio;\n    }\n    return size;\n}\n\nconst figureRegistry = new Registry();\nfigureRegistry.add(\"chart\", {\n    Component: ChartFigure,\n    SidePanelComponent: \"ChartPanel\",\n    menuBuilder: getChartMenu,\n});\nfigureRegistry.add(\"image\", {\n    Component: ImageFigure,\n    keepRatio: true,\n    minFigSize: 20,\n    borderWidth: 0,\n    menuBuilder: getImageMenuRegistry,\n});\nfunction getChartMenu(figureId, onFigureDeleted, env) {\n    const menuItemSpecs = [\n        {\n            id: \"edit\",\n            name: _t(\"Edit\"),\n            sequence: 1,\n            execute: () => {\n                env.model.dispatch(\"SELECT_FIGURE\", { id: figureId });\n                env.openSidePanel(\"ChartPanel\");\n            },\n            icon: \"o-spreadsheet-Icon.EDIT\",\n        },\n        getCopyMenuItem(figureId, env),\n        getCutMenuItem(figureId, env),\n        getDeleteMenuItem(figureId, onFigureDeleted, env),\n    ];\n    return createActions(menuItemSpecs);\n}\nfunction getImageMenuRegistry(figureId, onFigureDeleted, env) {\n    const menuItemSpecs = [\n        getCopyMenuItem(figureId, env),\n        getCutMenuItem(figureId, env),\n        {\n            id: \"reset_size\",\n            name: _t(\"Reset size\"),\n            sequence: 4,\n            execute: async () => {\n                const imagePath = env.model.getters.getImagePath(figureId);\n                const size = env.model.getters.getImageSize(figureId) ??\n                    (await env.imageProvider?.getImageOriginalSize(imagePath));\n                if (!env.model.getters.getImageSize(figureId)) {\n                    const image = env.model.getters.getImage(figureId);\n                    image.size = size;\n                }\n                const { height, width } = getMaxFigureSize(env.model.getters, size);\n                env.model.dispatch(\"UPDATE_FIGURE\", {\n                    sheetId: env.model.getters.getActiveSheetId(),\n                    id: figureId,\n                    height,\n                    width,\n                });\n            },\n            icon: \"o-spreadsheet-Icon.REFRESH\",\n        },\n        getDeleteMenuItem(figureId, onFigureDeleted, env),\n    ];\n    return createActions(menuItemSpecs);\n}\nfunction getCopyMenuItem(figureId, env) {\n    return {\n        id: \"copy\",\n        name: _t(\"Copy\"),\n        sequence: 2,\n        description: \"Ctrl+C\",\n        execute: async () => {\n            env.model.dispatch(\"SELECT_FIGURE\", { id: figureId });\n            env.model.dispatch(\"COPY\");\n            await env.clipboard.write(env.model.getters.getClipboardContent());\n        },\n        icon: \"o-spreadsheet-Icon.COPY\",\n    };\n}\nfunction getCutMenuItem(figureId, env) {\n    return {\n        id: \"cut\",\n        name: _t(\"Cut\"),\n        sequence: 3,\n        description: \"Ctrl+X\",\n        execute: async () => {\n            env.model.dispatch(\"SELECT_FIGURE\", { id: figureId });\n            env.model.dispatch(\"CUT\");\n            await env.clipboard.write(env.model.getters.getClipboardContent());\n        },\n        icon: \"o-spreadsheet-Icon.CUT\",\n    };\n}\nfunction getDeleteMenuItem(figureId, onFigureDeleted, env) {\n    return {\n        id: \"delete\",\n        name: _t(\"Delete\"),\n        sequence: 10,\n        execute: () => {\n            env.model.dispatch(\"DELETE_FIGURE\", {\n                sheetId: env.model.getters.getActiveSheetId(),\n                id: figureId,\n            });\n            onFigureDeleted();\n        },\n        icon: \"o-spreadsheet-Icon.TRASH\",\n    };\n}\n\nconst inverseCommandRegistry = new Registry()\n    .add(\"ADD_COLUMNS_ROWS\", inverseAddColumnsRows)\n    .add(\"REMOVE_COLUMNS_ROWS\", inverseRemoveColumnsRows)\n    .add(\"ADD_MERGE\", inverseAddMerge)\n    .add(\"REMOVE_MERGE\", inverseRemoveMerge)\n    .add(\"CREATE_SHEET\", inverseCreateSheet)\n    .add(\"DELETE_SHEET\", inverseDeleteSheet)\n    .add(\"DUPLICATE_SHEET\", inverseDuplicateSheet)\n    .add(\"CREATE_FIGURE\", inverseCreateFigure)\n    .add(\"CREATE_CHART\", inverseCreateChart)\n    .add(\"HIDE_COLUMNS_ROWS\", inverseHideColumnsRows)\n    .add(\"UNHIDE_COLUMNS_ROWS\", inverseUnhideColumnsRows)\n    .add(\"CREATE_TABLE_STYLE\", inverseCreateTableStyle)\n    .add(\"ADD_PIVOT\", inverseAddPivot);\nfor (const cmd of coreTypes.values()) {\n    if (!inverseCommandRegistry.contains(cmd)) {\n        inverseCommandRegistry.add(cmd, identity);\n    }\n}\nfunction identity(cmd) {\n    return [cmd];\n}\nfunction inverseAddPivot(cmd) {\n    return [\n        {\n            type: \"REMOVE_PIVOT\",\n            pivotId: cmd.pivotId,\n        },\n    ];\n}\nfunction inverseAddColumnsRows(cmd) {\n    const elements = [];\n    let start = cmd.base;\n    if (cmd.position === \"after\") {\n        start++;\n    }\n    for (let i = 0; i < cmd.quantity; i++) {\n        elements.push(i + start);\n    }\n    return [\n        {\n            type: \"REMOVE_COLUMNS_ROWS\",\n            dimension: cmd.dimension,\n            elements,\n            sheetId: cmd.sheetId,\n        },\n    ];\n}\nfunction inverseAddMerge(cmd) {\n    return [{ type: \"REMOVE_MERGE\", sheetId: cmd.sheetId, target: cmd.target }];\n}\nfunction inverseRemoveMerge(cmd) {\n    return [{ type: \"ADD_MERGE\", sheetId: cmd.sheetId, target: cmd.target }];\n}\nfunction inverseCreateSheet(cmd) {\n    return [{ type: \"DELETE_SHEET\", sheetId: cmd.sheetId }];\n}\nfunction inverseDuplicateSheet(cmd) {\n    return [{ type: \"DELETE_SHEET\", sheetId: cmd.sheetIdTo }];\n}\nfunction inverseRemoveColumnsRows(cmd) {\n    const commands = [];\n    const elements = [...cmd.elements].sort((a, b) => a - b);\n    for (let group of groupConsecutive(elements)) {\n        const column = group[0] === 0 ? 0 : group[0] - 1;\n        const position = group[0] === 0 ? \"before\" : \"after\";\n        commands.push({\n            type: \"ADD_COLUMNS_ROWS\",\n            dimension: cmd.dimension,\n            quantity: group.length,\n            base: column,\n            sheetId: cmd.sheetId,\n            position,\n        });\n    }\n    return commands;\n}\nfunction inverseDeleteSheet(cmd) {\n    return [{ type: \"CREATE_SHEET\", sheetId: cmd.sheetId, position: 1 }];\n}\nfunction inverseCreateFigure(cmd) {\n    return [{ type: \"DELETE_FIGURE\", id: cmd.figure.id, sheetId: cmd.sheetId }];\n}\nfunction inverseCreateChart(cmd) {\n    return [{ type: \"DELETE_FIGURE\", id: cmd.id, sheetId: cmd.sheetId }];\n}\nfunction inverseHideColumnsRows(cmd) {\n    return [\n        {\n            type: \"UNHIDE_COLUMNS_ROWS\",\n            sheetId: cmd.sheetId,\n            dimension: cmd.dimension,\n            elements: cmd.elements,\n        },\n    ];\n}\nfunction inverseUnhideColumnsRows(cmd) {\n    return [\n        {\n            type: \"HIDE_COLUMNS_ROWS\",\n            sheetId: cmd.sheetId,\n            dimension: cmd.dimension,\n            elements: cmd.elements,\n        },\n    ];\n}\nfunction inverseCreateTableStyle(cmd) {\n    return [{ type: \"REMOVE_TABLE_STYLE\", tableStyleId: cmd.tableStyleId }];\n}\n\n/**\n * The class Registry is extended in order to add the function addChild\n *\n */\nclass MenuItemRegistry extends Registry {\n    /**\n     * @override\n     */\n    add(key, value) {\n        if (value.id === undefined) {\n            value.id = key;\n        }\n        this.content[key] = value;\n        return this;\n    }\n    /**\n     * Add a subitem to an existing item\n     * @param path Path of items to add this subitem\n     * @param value Subitem to add\n     */\n    addChild(key, path, value, options = { force: false }) {\n        if (typeof value !== \"function\" && value.id === undefined) {\n            value.id = key;\n        }\n        const root = path.splice(0, 1)[0];\n        let node = this.content[root];\n        if (!node) {\n            throw new Error(`Path ${root + \":\" + path.join(\":\")} not found`);\n        }\n        for (let p of path) {\n            const children = node.children;\n            if (!children || typeof children === \"function\") {\n                throw new Error(`${p} is either not a node or it's dynamically computed`);\n            }\n            node = children.find((elt) => elt.id === p);\n            if (!node) {\n                throw new Error(`Path ${root + \":\" + path.join(\":\")} not found`);\n            }\n        }\n        if (!node.children) {\n            node.children = [];\n        }\n        const children = node.children;\n        if (!children || typeof children === \"function\") {\n            throw new Error(`${path} is either not a node or it's dynamically computed`);\n        }\n        if (\"id\" in value) {\n            const valueIndex = children.findIndex((elt) => \"id\" in elt && elt.id === value.id);\n            if (valueIndex > -1) {\n                if (!options.force)\n                    throw new Error(`A child with the id \"${value.id}\" already exists.`);\n                node.children.splice(valueIndex, 1, value);\n                return this;\n            }\n        }\n        node.children.push(value);\n        return this;\n    }\n    getMenuItems() {\n        return createActions(this.getAll());\n    }\n}\n\nfunction interactiveCut(env) {\n    const result = env.model.dispatch(\"CUT\");\n    if (!result.isSuccessful) {\n        if (result.isCancelledBecause(\"WrongCutSelection\" /* CommandResult.WrongCutSelection */)) {\n            env.raiseError(_t(\"This operation is not allowed with multiple selections.\"));\n        }\n    }\n}\n\nconst AddMergeInteractiveContent = {\n    MergeIsDestructive: _t(\"Merging these cells will only preserve the top-leftmost value. Merge anyway?\"),\n    MergeInFilter: _t(\"You can't merge cells inside of an existing filter.\"),\n};\nfunction interactiveAddMerge(env, sheetId, target) {\n    const result = env.model.dispatch(\"ADD_MERGE\", { sheetId, target });\n    if (result.isCancelledBecause(\"MergeInTable\" /* CommandResult.MergeInTable */)) {\n        env.raiseError(AddMergeInteractiveContent.MergeInFilter);\n    }\n    else if (result.isCancelledBecause(\"MergeIsDestructive\" /* CommandResult.MergeIsDestructive */)) {\n        env.askConfirmation(AddMergeInteractiveContent.MergeIsDestructive, () => {\n            env.model.dispatch(\"ADD_MERGE\", { sheetId, target, force: true });\n        });\n    }\n}\n\nclass HoveredCellStore extends SpreadsheetStore {\n    mutators = [\"clear\", \"hover\"];\n    col;\n    row;\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ACTIVATE_SHEET\":\n                this.clear();\n        }\n    }\n    hover(position) {\n        this.col = position.col;\n        this.row = position.row;\n    }\n    clear() {\n        this.col = undefined;\n        this.row = undefined;\n    }\n}\n\nclass CellPopoverStore extends SpreadsheetStore {\n    mutators = [\"open\", \"close\"];\n    persistentPopover;\n    hoveredCell = this.get(HoveredCellStore);\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ACTIVATE_SHEET\":\n                this.close();\n        }\n    }\n    open({ col, row }, type) {\n        const sheetId = this.getters.getActiveSheetId();\n        if (!cellPopoverRegistry.contains(type)) {\n            return;\n        }\n        this.persistentPopover = { col, row, sheetId, type };\n    }\n    close() {\n        this.persistentPopover = undefined;\n    }\n    get persistentCellPopover() {\n        return ((this.persistentPopover && { isOpen: true, ...this.persistentPopover }) || { isOpen: false });\n    }\n    get isOpen() {\n        return this.persistentPopover !== undefined;\n    }\n    get cellPopover() {\n        const sheetId = this.getters.getActiveSheetId();\n        if (this.persistentPopover && this.getters.isVisibleInViewport(this.persistentPopover)) {\n            const position = this.getters.getMainCellPosition(this.persistentPopover);\n            const popover = cellPopoverRegistry\n                .get(this.persistentPopover.type)\n                .onOpen?.(position, this.getters);\n            return !popover?.isOpen\n                ? { isOpen: false }\n                : {\n                    ...popover,\n                    anchorRect: this.computePopoverAnchorRect(this.persistentPopover),\n                };\n        }\n        const { col, row } = this.hoveredCell;\n        if (col === undefined ||\n            row === undefined ||\n            !this.getters.isVisibleInViewport({ sheetId, col, row })) {\n            return { isOpen: false };\n        }\n        const position = this.getters.getMainCellPosition({ sheetId, col, row });\n        const popover = cellPopoverRegistry\n            .getAll()\n            .map((matcher) => matcher.onHover?.(position, this.getters))\n            .find((popover) => popover?.isOpen);\n        return !popover?.isOpen\n            ? { isOpen: false }\n            : {\n                ...popover,\n                anchorRect: this.computePopoverAnchorRect(position),\n            };\n    }\n    computePopoverAnchorRect({ col, row }) {\n        const sheetId = this.getters.getActiveSheetId();\n        const merge = this.getters.getMerge({ sheetId, col, row });\n        if (merge) {\n            return this.getters.getVisibleRect(merge);\n        }\n        return this.getters.getVisibleRect(positionToZone({ col, row }));\n    }\n}\n\n/**\n * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap\n */\nfunction rectIntersection(rect1, rect2) {\n    return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2)));\n}\n/** Compute the union of the rectangles, ie. the smallest rectangle that contain them all */\nfunction rectUnion(...rects) {\n    return zoneToRect(union(...rects.map(rectToZone)));\n}\nfunction rectToZone(rect) {\n    return {\n        left: rect.x,\n        top: rect.y,\n        right: rect.x + rect.width,\n        bottom: rect.y + rect.height,\n    };\n}\nfunction zoneToRect(zone) {\n    if (!zone)\n        return undefined;\n    return {\n        x: zone.left,\n        y: zone.top,\n        width: zone.right - zone.left,\n        height: zone.bottom - zone.top,\n    };\n}\n\ncss /* scss */ `\n  .o-popover {\n    position: absolute;\n    z-index: ${ComponentsImportance.Popover};\n    overflow: auto;\n    box-shadow: 1px 2px 5px 2px rgb(51 51 51 / 15%);\n    width: fit-content;\n    height: fit-content;\n  }\n`;\nclass Popover extends Component {\n    static template = \"o-spreadsheet-Popover\";\n    static props = {\n        anchorRect: Object,\n        containerRect: { type: Object, optional: true },\n        positioning: { type: String, optional: true },\n        maxWidth: { type: Number, optional: true },\n        maxHeight: { type: Number, optional: true },\n        verticalOffset: { type: Number, optional: true },\n        onMouseWheel: { type: Function, optional: true },\n        onPopoverHidden: { type: Function, optional: true },\n        onPopoverMoved: { type: Function, optional: true },\n        zIndex: { type: Number, optional: true },\n        slots: Object,\n    };\n    static defaultProps = {\n        positioning: \"BottomLeft\",\n        verticalOffset: 0,\n        onMouseWheel: () => { },\n        onPopoverMoved: () => { },\n        onPopoverHidden: () => { },\n        zIndex: ComponentsImportance.Popover,\n    };\n    popoverRef = useRef(\"popover\");\n    currentPosition = undefined;\n    currentDisplayValue = undefined;\n    spreadsheetRect = useSpreadsheetRect();\n    containerRect;\n    setup() {\n        this.containerRect = usePopoverContainer();\n        // useEffect occurs after the DOM is created and the element width/height are computed, but before\n        // the element in rendered, so we can still set its position\n        useEffect(() => {\n            if (!this.containerRect)\n                throw new Error(\"Popover container is not defined\");\n            const el = this.popoverRef.el;\n            const anchor = rectIntersection(this.props.anchorRect, this.containerRect);\n            const newDisplay = anchor ? \"block\" : \"none\";\n            if (this.currentDisplayValue !== \"none\" && newDisplay === \"none\") {\n                this.props.onPopoverHidden?.();\n            }\n            el.style.display = newDisplay;\n            this.currentDisplayValue = newDisplay;\n            if (!anchor)\n                return;\n            const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };\n            let elDims = {\n                width: el.getBoundingClientRect().width,\n                height: el.getBoundingClientRect().height,\n            };\n            const spreadsheetRect = this.spreadsheetRect;\n            const popoverPositionHelper = this.props.positioning === \"BottomLeft\"\n                ? new BottomLeftPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect)\n                : new TopRightPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect);\n            el.style[\"max-height\"] = popoverPositionHelper.getMaxHeight(elDims.height) + \"px\";\n            el.style[\"max-width\"] = popoverPositionHelper.getMaxWidth(elDims.width) + \"px\";\n            // Re-compute the dimensions after setting the max-width and max-height\n            elDims = {\n                width: el.getBoundingClientRect().width,\n                height: el.getBoundingClientRect().height,\n            };\n            let style = popoverPositionHelper.getCss(elDims, this.props.verticalOffset);\n            for (const property of Object.keys(style)) {\n                el.style[property] = style[property];\n            }\n            const newPosition = popoverPositionHelper.getCurrentPosition(elDims);\n            if (this.currentPosition && newPosition !== this.currentPosition) {\n                this.props.onPopoverMoved?.();\n            }\n            this.currentPosition = newPosition;\n        });\n    }\n    get popoverStyle() {\n        return cssPropertiesToCss({\n            \"z-index\": `${this.props.zIndex}`,\n        });\n    }\n}\nclass PopoverPositionContext {\n    anchorRect;\n    containerRect;\n    propsMaxSize;\n    spreadsheetOffset;\n    constructor(anchorRect, containerRect, propsMaxSize, spreadsheetOffset) {\n        this.anchorRect = anchorRect;\n        this.containerRect = containerRect;\n        this.propsMaxSize = propsMaxSize;\n        this.spreadsheetOffset = spreadsheetOffset;\n    }\n    /** Check if there is enough space for the popover to be rendered at the bottom of the anchorRect */\n    shouldRenderAtBottom(elementHeight) {\n        return (elementHeight <= this.availableHeightDown ||\n            this.availableHeightDown >= this.availableHeightUp);\n    }\n    /** Check if there is enough space for the popover to be rendered at the right of the anchorRect */\n    shouldRenderAtRight(elementWidth) {\n        return (elementWidth <= this.availableWidthRight ||\n            this.availableWidthRight >= this.availableWidthLeft);\n    }\n    getMaxHeight(elementHeight) {\n        const shouldRenderAtBottom = this.shouldRenderAtBottom(elementHeight);\n        const availableHeight = shouldRenderAtBottom\n            ? this.availableHeightDown\n            : this.availableHeightUp;\n        return this.propsMaxSize.height\n            ? Math.min(availableHeight, this.propsMaxSize.height)\n            : availableHeight;\n    }\n    getMaxWidth(elementWidth) {\n        const shouldRenderAtRight = this.shouldRenderAtRight(elementWidth);\n        const availableWidth = shouldRenderAtRight ? this.availableWidthRight : this.availableWidthLeft;\n        return this.propsMaxSize.width\n            ? Math.min(availableWidth, this.propsMaxSize.width)\n            : availableWidth;\n    }\n    getCss(elDims, verticalOffset) {\n        const maxHeight = this.getMaxHeight(elDims.height);\n        const maxWidth = this.getMaxWidth(elDims.width);\n        const actualHeight = Math.min(maxHeight, elDims.height);\n        const actualWidth = Math.min(maxWidth, elDims.width);\n        const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);\n        const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);\n        verticalOffset = shouldRenderAtBottom ? verticalOffset : -verticalOffset;\n        const cssProperties = {\n            top: this.getTopCoordinate(actualHeight, shouldRenderAtBottom) -\n                this.spreadsheetOffset.y -\n                verticalOffset +\n                \"px\",\n            left: this.getLeftCoordinate(actualWidth, shouldRenderAtRight) - this.spreadsheetOffset.x + \"px\",\n        };\n        return cssProperties;\n    }\n    getCurrentPosition(elDims) {\n        const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);\n        const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);\n        if (shouldRenderAtBottom && shouldRenderAtRight)\n            return \"BottomRight\";\n        if (shouldRenderAtBottom && !shouldRenderAtRight)\n            return \"BottomLeft\";\n        if (!shouldRenderAtBottom && shouldRenderAtRight)\n            return \"TopRight\";\n        return \"TopLeft\";\n    }\n}\nclass BottomLeftPopoverContext extends PopoverPositionContext {\n    get availableHeightUp() {\n        return this.anchorRect.y - this.containerRect.y;\n    }\n    get availableHeightDown() {\n        return this.containerRect.height - this.availableHeightUp - this.anchorRect.height;\n    }\n    get availableWidthRight() {\n        return this.containerRect.x + this.containerRect.width - this.anchorRect.x;\n    }\n    get availableWidthLeft() {\n        return this.anchorRect.x + this.anchorRect.width - this.containerRect.x;\n    }\n    getTopCoordinate(elementHeight, shouldRenderAtBottom) {\n        if (shouldRenderAtBottom) {\n            return this.anchorRect.y + this.anchorRect.height;\n        }\n        else {\n            return this.anchorRect.y - elementHeight;\n        }\n    }\n    getLeftCoordinate(elementWidth, shouldRenderAtRight) {\n        if (shouldRenderAtRight) {\n            return this.anchorRect.x;\n        }\n        else {\n            return this.anchorRect.x + this.anchorRect.width - elementWidth;\n        }\n    }\n}\nclass TopRightPopoverContext extends PopoverPositionContext {\n    get availableHeightUp() {\n        return this.anchorRect.y + this.anchorRect.height - this.containerRect.y;\n    }\n    get availableHeightDown() {\n        return this.containerRect.y + this.containerRect.height - this.anchorRect.y;\n    }\n    get availableWidthRight() {\n        return this.containerRect.width - this.anchorRect.width - this.availableWidthLeft;\n    }\n    get availableWidthLeft() {\n        return this.anchorRect.x - this.containerRect.x;\n    }\n    getTopCoordinate(elementHeight, shouldRenderAtBottom) {\n        if (shouldRenderAtBottom) {\n            return this.anchorRect.y;\n        }\n        else {\n            return this.anchorRect.y + this.anchorRect.height - elementHeight;\n        }\n    }\n    getLeftCoordinate(elementWidth, shouldRenderAtRight) {\n        if (shouldRenderAtRight) {\n            return this.anchorRect.x + this.anchorRect.width;\n        }\n        else {\n            return this.anchorRect.x - elementWidth;\n        }\n    }\n}\n\nconst ERROR_TOOLTIP_MAX_HEIGHT = 80;\nconst ERROR_TOOLTIP_WIDTH = 180;\ncss /* scss */ `\n  .o-error-tooltip {\n    font-size: 13px;\n    background-color: white;\n    border-left: 3px solid red;\n    padding: 10px;\n    width: ${ERROR_TOOLTIP_WIDTH}px;\n    box-sizing: border-box !important;\n    overflow-wrap: break-word;\n\n    .o-error-tooltip-message {\n      overflow: hidden;\n    }\n  }\n`;\nclass ErrorToolTip extends Component {\n    static maxSize = { maxHeight: ERROR_TOOLTIP_MAX_HEIGHT };\n    static template = \"o-spreadsheet-ErrorToolTip\";\n    static props = {\n        errors: Array,\n        onClosed: { type: Function, optional: true },\n    };\n}\nconst ErrorToolTipPopoverBuilder = {\n    onHover: (position, getters) => {\n        const cell = getters.getEvaluatedCell(position);\n        const errors = [];\n        if (cell.type === CellValueType.error && !!cell.message) {\n            errors.push({\n                title: _t(\"Error\"),\n                message: cell.message,\n            });\n        }\n        const validationErrorMessage = getters.getInvalidDataValidationMessage(position);\n        if (validationErrorMessage) {\n            errors.push({\n                title: _t(\"Invalid\"),\n                message: validationErrorMessage,\n            });\n        }\n        if (!errors.length) {\n            return { isOpen: false };\n        }\n        return {\n            isOpen: true,\n            props: { errors: errors },\n            Component: ErrorToolTip,\n            cellCorner: \"TopRight\",\n        };\n    },\n};\n\ncss /*SCSS*/ `\n  .o-filter-menu-value {\n    padding: 4px;\n    line-height: 20px;\n    height: 28px;\n    .o-filter-menu-value-checked {\n      width: 20px;\n    }\n  }\n`;\nclass FilterMenuValueItem extends Component {\n    static template = \"o-spreadsheet-FilterMenuValueItem\";\n    static props = {\n        value: String,\n        isChecked: Boolean,\n        isSelected: Boolean,\n        onMouseMove: Function,\n        onClick: Function,\n        scrolledTo: { type: String, optional: true },\n    };\n    itemRef = useRef(\"menuValueItem\");\n    setup() {\n        onWillPatch(() => {\n            if (this.props.scrolledTo) {\n                this.scrollListToSelectedValue();\n            }\n        });\n    }\n    scrollListToSelectedValue() {\n        if (!this.itemRef.el) {\n            return;\n        }\n        this.itemRef.el.scrollIntoView?.({\n            block: this.props.scrolledTo === \"bottom\" ? \"end\" : \"start\",\n        });\n    }\n}\n\nconst FILTER_MENU_HEIGHT = 295;\nconst CSS = css /* scss */ `\n  .o-filter-menu {\n    box-sizing: border-box;\n    padding: 8px 16px;\n    height: ${FILTER_MENU_HEIGHT}px;\n    line-height: 1;\n\n    .o-filter-menu-item {\n      display: flex;\n      box-sizing: border-box;\n      height: ${MENU_ITEM_HEIGHT}px;\n      padding: 4px 4px 4px 0px;\n      cursor: pointer;\n      user-select: none;\n\n      &.selected {\n        background-color: rgba(0, 0, 0, 0.08);\n      }\n    }\n\n    input {\n      box-sizing: border-box;\n      margin-bottom: 5px;\n      border: 1px solid #949494;\n      height: 24px;\n      padding-right: 28px;\n    }\n\n    .o-search-icon {\n      right: 5px;\n      top: 3px;\n      opacity: 0.4;\n\n      svg {\n        height: 16px;\n        width: 16px;\n        vertical-align: middle;\n      }\n    }\n\n    .o-filter-menu-actions {\n      display: flex;\n      flex-direction: row;\n      margin-bottom: 4px;\n\n      .o-filter-menu-action-text {\n        cursor: pointer;\n        margin-right: 10px;\n        color: blue;\n        text-decoration: underline;\n      }\n    }\n\n    .o-filter-menu-list {\n      flex: auto;\n      overflow-y: auto;\n      border: 1px solid #949494;\n\n      .o-filter-menu-no-values {\n        color: #949494;\n        font-style: italic;\n      }\n    }\n\n    .o-filter-menu-buttons {\n      margin-top: 9px;\n\n      .o-button {\n        height: 26px;\n      }\n    }\n  }\n`;\nclass FilterMenu extends Component {\n    static template = \"o-spreadsheet-FilterMenu\";\n    static props = {\n        filterPosition: Object,\n        onClosed: { type: Function, optional: true },\n    };\n    static style = CSS;\n    static components = { FilterMenuValueItem };\n    state = useState({\n        values: [],\n        textFilter: \"\",\n        selectedValue: undefined,\n    });\n    searchBar = useRef(\"filterMenuSearchBar\");\n    setup() {\n        onWillUpdateProps((nextProps) => {\n            if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {\n                this.state.values = this.getFilterHiddenValues(nextProps.filterPosition);\n            }\n        });\n        this.state.values = this.getFilterHiddenValues(this.props.filterPosition);\n    }\n    get isSortable() {\n        if (!this.table) {\n            return false;\n        }\n        const coreTable = this.env.model.getters.getCoreTableMatchingTopLeft(this.table.range.sheetId, this.table.range.zone);\n        return !this.env.model.getters.isReadonly() && coreTable?.type !== \"dynamic\";\n    }\n    getFilterHiddenValues(position) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const filter = this.env.model.getters.getFilter({ sheetId, ...position });\n        if (!filter) {\n            return [];\n        }\n        const cellValues = (filter.filteredRange ? positions(filter.filteredRange.zone) : [])\n            .filter(({ row }) => !this.env.model.getters.isRowHidden(sheetId, row))\n            .map(({ col, row }) => this.env.model.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue);\n        const filterValues = this.env.model.getters.getFilterHiddenValues({ sheetId, ...position });\n        const strValues = [...cellValues, ...filterValues];\n        const normalizedFilteredValues = filterValues.map(toLowerCase);\n        // Set with lowercase values to avoid duplicates\n        const normalizedValues = [...new Set(strValues.map(toLowerCase))];\n        const sortedValues = normalizedValues.sort((val1, val2) => val1.localeCompare(val2, undefined, { numeric: true, sensitivity: \"base\" }));\n        return sortedValues.map((normalizedValue) => {\n            const checked = normalizedFilteredValues.findIndex((filteredValue) => filteredValue === normalizedValue) ===\n                -1;\n            return {\n                checked,\n                string: strValues.find((val) => toLowerCase(val) === normalizedValue) || \"\",\n            };\n        });\n    }\n    checkValue(value) {\n        this.state.selectedValue = value.string;\n        value.checked = !value.checked;\n        this.searchBar.el?.focus();\n    }\n    onMouseMove(value) {\n        this.state.selectedValue = value.string;\n    }\n    selectAll() {\n        this.displayedValues.forEach((value) => (value.checked = true));\n    }\n    clearAll() {\n        this.displayedValues.forEach((value) => (value.checked = false));\n    }\n    get table() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const position = this.props.filterPosition;\n        return this.env.model.getters.getTable({ sheetId, ...position });\n    }\n    get displayedValues() {\n        if (!this.state.textFilter) {\n            return this.state.values;\n        }\n        return fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);\n    }\n    confirm() {\n        const position = this.props.filterPosition;\n        this.env.model.dispatch(\"UPDATE_FILTER\", {\n            ...position,\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            hiddenValues: this.state.values.filter((val) => !val.checked).map((val) => val.string),\n        });\n        this.props.onClosed?.();\n    }\n    cancel() {\n        this.props.onClosed?.();\n    }\n    onKeyDown(ev) {\n        const displayedValues = this.displayedValues;\n        if (displayedValues.length === 0)\n            return;\n        let selectedIndex = undefined;\n        if (this.state.selectedValue !== undefined) {\n            const index = displayedValues.findIndex((val) => val.string === this.state.selectedValue);\n            selectedIndex = index === -1 ? undefined : index;\n        }\n        switch (ev.key) {\n            case \"ArrowDown\":\n                if (selectedIndex === undefined) {\n                    selectedIndex = 0;\n                }\n                else {\n                    selectedIndex = Math.min(selectedIndex + 1, displayedValues.length - 1);\n                }\n                ev.preventDefault();\n                ev.stopPropagation();\n                break;\n            case \"ArrowUp\":\n                if (selectedIndex === undefined) {\n                    selectedIndex = displayedValues.length - 1;\n                }\n                else {\n                    selectedIndex = Math.max(selectedIndex - 1, 0);\n                }\n                ev.preventDefault();\n                ev.stopPropagation();\n                break;\n            case \"Enter\":\n                if (selectedIndex !== undefined) {\n                    this.checkValue(displayedValues[selectedIndex]);\n                }\n                ev.stopPropagation();\n                ev.preventDefault();\n                break;\n        }\n        this.state.selectedValue =\n            selectedIndex !== undefined ? displayedValues[selectedIndex].string : undefined;\n        if (ev.key === \"ArrowUp\" || ev.key === \"ArrowDown\") {\n            this.scrollListToSelectedValue(ev.key);\n        }\n    }\n    clearScrolledToValue() {\n        this.state.values.forEach((val) => (val.scrolledTo = undefined));\n    }\n    scrollListToSelectedValue(arrow) {\n        this.clearScrolledToValue();\n        const selectedValue = this.state.values.find((val) => val.string === this.state.selectedValue);\n        if (selectedValue) {\n            selectedValue.scrolledTo = arrow === \"ArrowUp\" ? \"top\" : \"bottom\";\n        }\n    }\n    sortFilterZone(sortDirection) {\n        const filterPosition = this.props.filterPosition;\n        const table = this.table;\n        const tableZone = table?.range.zone;\n        if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) {\n            return;\n        }\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const contentZone = { ...tableZone, top: tableZone.top + 1 };\n        this.env.model.dispatch(\"SORT_CELLS\", {\n            sheetId,\n            col: filterPosition.col,\n            row: contentZone.top,\n            zone: contentZone,\n            sortDirection,\n            sortOptions: { emptyCellAsZero: true, sortHeaders: true },\n        });\n        this.props.onClosed?.();\n    }\n}\nconst FilterMenuPopoverBuilder = {\n    onOpen: (position, getters) => {\n        return {\n            isOpen: true,\n            props: { filterPosition: position },\n            Component: FilterMenu,\n            cellCorner: \"BottomLeft\",\n        };\n    },\n};\n\nconst LINK_TOOLTIP_HEIGHT = 32;\nconst LINK_TOOLTIP_WIDTH = 220;\ncss /* scss */ `\n  .o-link-tool {\n    font-size: 13px;\n    background-color: white;\n    box-shadow: 0 1px 4px 3px rgba(60, 64, 67, 0.15);\n    padding: 6px 12px;\n    border-radius: 4px;\n    display: flex;\n    justify-content: space-between;\n    height: ${LINK_TOOLTIP_HEIGHT}px;\n    width: ${LINK_TOOLTIP_WIDTH}px;\n    box-sizing: border-box !important;\n\n    img {\n      margin-right: 3px;\n      width: 16px;\n      height: 16px;\n    }\n\n    a.o-link {\n      color: ${LINK_COLOR};\n      text-decoration: none;\n      flex-grow: 2;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n    a.o-link:hover {\n      text-decoration: none;\n      color: #001d1f;\n      cursor: pointer;\n    }\n  }\n  .o-link-icon {\n    float: right;\n    padding-left: 5px;\n    .o-icon {\n      height: 16px;\n    }\n  }\n  .o-link-icon .o-icon {\n    height: 13px;\n  }\n  .o-link-icon:hover {\n    cursor: pointer;\n    color: #000;\n  }\n`;\nclass LinkDisplay extends Component {\n    static template = \"o-spreadsheet-LinkDisplay\";\n    static props = {\n        cellPosition: Object,\n        onClosed: { type: Function, optional: true },\n    };\n    cellPopovers;\n    setup() {\n        this.cellPopovers = useStore(CellPopoverStore);\n    }\n    get cell() {\n        const { col, row } = this.props.cellPosition;\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.env.model.getters.getEvaluatedCell({ sheetId, col, row });\n    }\n    get link() {\n        if (this.cell.link) {\n            return this.cell.link;\n        }\n        const { col, row } = this.props.cellPosition;\n        throw new Error(`LinkDisplay Component can only be used with link cells. ${toXC(col, row)} is not a link.`);\n    }\n    getUrlRepresentation(link) {\n        return urlRepresentation(link, this.env.model.getters);\n    }\n    openLink() {\n        openLink(this.link, this.env);\n    }\n    edit() {\n        const { col, row } = this.props.cellPosition;\n        this.env.model.selection.selectCell(col, row);\n        this.cellPopovers.open({ col, row }, \"LinkEditor\");\n    }\n    unlink() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const { col, row } = this.props.cellPosition;\n        const style = this.env.model.getters.getCellComputedStyle({ sheetId, col, row });\n        const textColor = style?.textColor === LINK_COLOR ? undefined : style?.textColor;\n        this.env.model.dispatch(\"UPDATE_CELL\", {\n            col,\n            row,\n            sheetId,\n            content: this.link.label,\n            style: { ...style, textColor, underline: undefined },\n        });\n    }\n}\nconst LinkCellPopoverBuilder = {\n    onHover: (position, getters) => {\n        const cell = getters.getEvaluatedCell(position);\n        const shouldDisplayLink = !getters.isDashboard() && cell.link && getters.isVisibleInViewport(position);\n        if (!shouldDisplayLink)\n            return { isOpen: false };\n        return {\n            isOpen: true,\n            Component: LinkDisplay,\n            props: { cellPosition: position },\n            cellCorner: \"BottomLeft\",\n        };\n    },\n};\n\nconst linkSheet = {\n    name: _t(\"Link sheet\"),\n    children: [\n        (env) => {\n            const sheets = env.model.getters\n                .getSheetIds()\n                .map((sheetId) => env.model.getters.getSheet(sheetId));\n            return sheets.map((sheet) => ({\n                id: sheet.id,\n                name: sheet.name,\n                execute: () => markdownLink(sheet.name, buildSheetLink(sheet.id)),\n            }));\n        },\n    ],\n};\nconst deleteSheet = {\n    name: _t(\"Delete\"),\n    isVisible: (env) => {\n        return env.model.getters.getSheetIds().length > 1;\n    },\n    execute: (env) => env.askConfirmation(_t(\"Are you sure you want to delete this sheet?\"), () => {\n        env.model.dispatch(\"DELETE_SHEET\", { sheetId: env.model.getters.getActiveSheetId() });\n    }),\n    icon: \"o-spreadsheet-Icon.TRASH\",\n};\nconst duplicateSheet = {\n    name: _t(\"Duplicate\"),\n    execute: (env) => {\n        const sheetIdFrom = env.model.getters.getActiveSheetId();\n        const sheetIdTo = env.model.uuidGenerator.uuidv4();\n        env.model.dispatch(\"DUPLICATE_SHEET\", {\n            sheetId: sheetIdFrom,\n            sheetIdTo,\n        });\n        env.model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom, sheetIdTo });\n    },\n    icon: \"o-spreadsheet-Icon.COPY\",\n};\nconst renameSheet = (args) => {\n    return {\n        name: _t(\"Rename\"),\n        execute: args.renameSheetCallback,\n        icon: \"o-spreadsheet-Icon.RENAME_SHEET\",\n    };\n};\nconst changeSheetColor = (args) => {\n    return {\n        name: _t(\"Change color\"),\n        execute: args.openSheetColorPickerCallback,\n        icon: \"o-spreadsheet-Icon.PAINT_FORMAT\",\n    };\n};\nconst sheetMoveRight = {\n    name: _t(\"Move right\"),\n    isVisible: (env) => {\n        const sheetId = env.model.getters.getActiveSheetId();\n        const sheetIds = env.model.getters.getVisibleSheetIds();\n        return sheetIds.indexOf(sheetId) !== sheetIds.length - 1;\n    },\n    execute: (env) => env.model.dispatch(\"MOVE_SHEET\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        delta: 1,\n    }),\n    icon: \"o-spreadsheet-Icon.MOVE_SHEET_RIGHT\",\n};\nconst sheetMoveLeft = {\n    name: _t(\"Move left\"),\n    isVisible: (env) => {\n        const sheetId = env.model.getters.getActiveSheetId();\n        return env.model.getters.getVisibleSheetIds()[0] !== sheetId;\n    },\n    execute: (env) => env.model.dispatch(\"MOVE_SHEET\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        delta: -1,\n    }),\n    icon: \"o-spreadsheet-Icon.MOVE_SHEET_LEFT\",\n};\nconst hideSheet = {\n    name: _t(\"Hide sheet\"),\n    isVisible: (env) => env.model.getters.getVisibleSheetIds().length !== 1,\n    execute: (env) => env.model.dispatch(\"HIDE_SHEET\", { sheetId: env.model.getters.getActiveSheetId() }),\n    icon: \"o-spreadsheet-Icon.HIDE_SHEET\",\n};\n\n//------------------------------------------------------------------------------\n// Link Menu Registry\n//------------------------------------------------------------------------------\nconst linkMenuRegistry = new MenuItemRegistry();\nlinkMenuRegistry.add(\"sheet\", {\n    ...linkSheet,\n    sequence: 10,\n});\n\n/**\n * Repeatedly calls a callback function with a time delay between calls.\n */\nfunction useInterval(callback, delay) {\n    let intervalId;\n    const { setInterval, clearInterval } = window;\n    useEffect(() => {\n        intervalId = setInterval(callback, delay);\n        return () => clearInterval(intervalId);\n    }, () => [delay]);\n    return {\n        pause: () => {\n            clearInterval(intervalId);\n            intervalId = undefined;\n        },\n        resume: () => {\n            if (intervalId === undefined) {\n                intervalId = setInterval(callback, delay);\n            }\n        },\n    };\n}\n/**\n * Calls a callback function with a time delay\n */\nfunction useTimeOut() {\n    let timeOutId;\n    function clear() {\n        if (timeOutId !== undefined) {\n            clearTimeout(timeOutId);\n            timeOutId = undefined;\n        }\n    }\n    function schedule(callback, delay) {\n        clear();\n        timeOutId = setTimeout(callback, delay);\n    }\n    onWillUnmount(clear);\n    return {\n        clear,\n        schedule,\n    };\n}\n\n//------------------------------------------------------------------------------\n// Context Menu Component\n//------------------------------------------------------------------------------\ncss /* scss */ `\n  .o-menu {\n    background-color: white;\n    padding: ${MENU_VERTICAL_PADDING}px 0px;\n    width: ${MENU_WIDTH}px;\n    box-sizing: border-box !important;\n    user-select: none;\n\n    .o-menu-item {\n      box-sizing: border-box;\n      height: ${MENU_ITEM_HEIGHT}px;\n      padding: ${MENU_ITEM_PADDING_VERTICAL}px ${MENU_ITEM_PADDING_HORIZONTAL}px;\n      cursor: pointer;\n      user-select: none;\n\n      .o-menu-item-name {\n        min-width: 40%;\n      }\n\n      .o-menu-item-icon {\n        display: inline-block;\n        margin: 0px 8px 0px 0px;\n        width: ${MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px;\n        line-height: ${MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px;\n      }\n\n      &:not(.disabled) {\n        &:hover,\n        &.o-menu-item-active {\n          background-color: ${BUTTON_ACTIVE_BG};\n          color: ${BUTTON_ACTIVE_TEXT_COLOR};\n        }\n        .o-menu-item-description {\n          color: grey;\n        }\n        .o-menu-item-icon {\n          .o-icon {\n            color: ${ICONS_COLOR};\n          }\n        }\n      }\n      &.disabled {\n        color: ${DISABLED_TEXT_COLOR};\n        cursor: not-allowed;\n      }\n    }\n  }\n`;\nconst TIMEOUT_DELAY = 250;\nclass Menu extends Component {\n    static template = \"o-spreadsheet-Menu\";\n    static props = {\n        position: Object,\n        menuItems: Array,\n        depth: { type: Number, optional: true },\n        maxHeight: { type: Number, optional: true },\n        onClose: Function,\n        onMenuClicked: { type: Function, optional: true },\n        menuId: { type: String, optional: true },\n        onMouseOver: { type: Function, optional: true },\n        width: { type: Number, optional: true },\n    };\n    static components = { Menu, Popover };\n    static defaultProps = {\n        depth: 1,\n    };\n    subMenu = useState({\n        isOpen: false,\n        position: null,\n        scrollOffset: 0,\n        menuItems: [],\n        isHoveringChild: false,\n    });\n    menuRef = useRef(\"menu\");\n    hoveredMenu = undefined;\n    position = useAbsoluteBoundingRect(this.menuRef);\n    openingTimeOut = useTimeOut();\n    setup() {\n        useExternalListener(window, \"click\", this.onExternalClick, { capture: true });\n        useExternalListener(window, \"contextmenu\", this.onExternalClick, { capture: true });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.menuItems !== this.props.menuItems) {\n                this.closeSubMenu();\n            }\n        });\n        onWillUnmount(() => {\n            this.hoveredMenu?.onStopHover?.(this.env);\n        });\n    }\n    get menuItemsAndSeparators() {\n        const menuItemsAndSeparators = [];\n        for (let i = 0; i < this.props.menuItems.length; i++) {\n            const menuItem = this.props.menuItems[i];\n            if (menuItem.isVisible(this.env)) {\n                menuItemsAndSeparators.push(menuItem);\n            }\n            if (menuItem.separator &&\n                i !== this.props.menuItems.length - 1 && // no separator at the end\n                menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== \"separator\" // no double separator\n            ) {\n                menuItemsAndSeparators.push(\"separator\");\n            }\n        }\n        if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === \"separator\") {\n            menuItemsAndSeparators.pop();\n        }\n        if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === \"separator\") {\n            return [];\n        }\n        return menuItemsAndSeparators;\n    }\n    get subMenuPosition() {\n        const position = Object.assign({}, this.subMenu.position);\n        position.y -= this.subMenu.scrollOffset || 0;\n        return position;\n    }\n    get popoverProps() {\n        const isRoot = this.props.depth === 1;\n        return {\n            anchorRect: {\n                x: this.props.position.x,\n                y: this.props.position.y,\n                width: isRoot ? 0 : this.props.width || MENU_WIDTH,\n                height: isRoot ? 0 : MENU_ITEM_HEIGHT,\n            },\n            positioning: \"TopRight\",\n            verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,\n            onPopoverHidden: () => this.closeSubMenu(),\n            onPopoverMoved: () => this.closeSubMenu(),\n        };\n    }\n    get childrenHaveIcon() {\n        return this.props.menuItems.some((menuItem) => !!this.getIconName(menuItem));\n    }\n    getIconName(menu) {\n        if (menu.icon(this.env)) {\n            return menu.icon(this.env);\n        }\n        if (menu.isActive?.(this.env)) {\n            return \"o-spreadsheet-Icon.CHECK\";\n        }\n        return \"\";\n    }\n    getColor(menu) {\n        return cssPropertiesToCss({ color: menu.textColor });\n    }\n    getIconColor(menu) {\n        return cssPropertiesToCss({ color: menu.iconColor });\n    }\n    async activateMenu(menu) {\n        const result = await menu.execute?.(this.env);\n        this.close();\n        this.props.onMenuClicked?.({ detail: result });\n    }\n    close() {\n        this.closeSubMenu();\n        this.props.onClose();\n    }\n    onExternalClick(ev) {\n        // Don't close a root menu when clicked to open the submenus.\n        const el = this.menuRef.el;\n        if (el && getOpenedMenus().some((el) => isChildEvent(el, ev))) {\n            return;\n        }\n        ev.closedMenuId = this.props.menuId;\n        this.close();\n    }\n    getName(menu) {\n        return menu.name(this.env);\n    }\n    isRoot(menu) {\n        return !menu.execute;\n    }\n    isEnabled(menu) {\n        if (menu.isEnabled(this.env)) {\n            return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;\n        }\n        return false;\n    }\n    isActive(menuItem) {\n        return (this.subMenu?.isHoveringChild || false) && this.isParentMenu(this.subMenu, menuItem);\n    }\n    onScroll(ev) {\n        this.subMenu.scrollOffset = ev.target.scrollTop;\n    }\n    /**\n     * If the given menu is not disabled, open it's submenu at the\n     * correct position according to available surrounding space.\n     */\n    openSubMenu(menu, parentMenuEl) {\n        if (!parentMenuEl) {\n            return;\n        }\n        const y = parentMenuEl.getBoundingClientRect().top;\n        this.subMenu.position = {\n            x: this.position.x,\n            y: y - (this.subMenu.scrollOffset || 0),\n        };\n        this.subMenu.menuItems = menu.children(this.env);\n        this.subMenu.isOpen = true;\n        this.subMenu.parentMenu = menu;\n    }\n    isParentMenu(subMenu, menuItem) {\n        return subMenu.parentMenu?.id === menuItem.id;\n    }\n    closeSubMenu() {\n        if (this.subMenu.isHoveringChild) {\n            return;\n        }\n        this.subMenu.isOpen = false;\n        this.subMenu.parentMenu = undefined;\n    }\n    onClickMenu(menu, ev) {\n        if (this.isEnabled(menu)) {\n            if (this.isRoot(menu)) {\n                this.openSubMenu(menu, ev.currentTarget);\n            }\n            else {\n                this.activateMenu(menu);\n            }\n        }\n    }\n    onMouseOver(menu, ev) {\n        if (this.isEnabled(menu)) {\n            if (this.isParentMenu(this.subMenu, menu)) {\n                this.openingTimeOut.clear();\n                return;\n            }\n            const currentTarget = ev.currentTarget;\n            if (this.isRoot(menu)) {\n                this.openingTimeOut.schedule(() => {\n                    this.openSubMenu(menu, currentTarget);\n                }, TIMEOUT_DELAY);\n            }\n        }\n    }\n    onMouseOverMainMenu() {\n        this.props.onMouseOver?.();\n        this.subMenu.isHoveringChild = false;\n    }\n    onMouseOverChildMenu() {\n        this.subMenu.isHoveringChild = true;\n        this.openingTimeOut.clear();\n    }\n    onMouseEnter(menu, ev) {\n        this.hoveredMenu = menu;\n        menu.onStartHover?.(this.env);\n    }\n    onMouseLeave(menu) {\n        this.openingTimeOut.schedule(this.closeSubMenu.bind(this), TIMEOUT_DELAY);\n        this.hoveredMenu = undefined;\n        menu.onStopHover?.(this.env);\n    }\n    get menuStyle() {\n        return this.props.width ? cssPropertiesToCss({ width: this.props.width + \"px\" }) : \"\";\n    }\n}\n\nconst MENU_OFFSET_X = 320;\nconst MENU_OFFSET_Y = 100;\nconst PADDING = 12;\nconst LINK_EDITOR_WIDTH = 340;\ncss /* scss */ `\n  .o-link-editor {\n    font-size: 13px;\n    background-color: white;\n    box-shadow: 0 1px 4px 3px rgba(60, 64, 67, 0.15);\n    padding: ${PADDING}px;\n    display: flex;\n    flex-direction: column;\n    border-radius: 4px;\n    width: ${LINK_EDITOR_WIDTH}px;\n\n    .o-section {\n      .o-section-title {\n        font-weight: bold;\n        margin-bottom: 5px;\n      }\n    }\n    .o-buttons {\n      padding-left: 16px;\n      padding-top: 16px;\n      text-align: right;\n    }\n    input.o-input {\n      box-sizing: border-box;\n      width: 100%;\n      padding: 0 23px 4px 0;\n    }\n    .o-link-url {\n      position: relative;\n      flex-grow: 1;\n      button {\n        position: absolute;\n        right: 0px;\n        top: 0px;\n        border: none;\n        height: 20px;\n        width: 20px;\n        background-color: #fff;\n        margin: 2px 3px 1px 0px;\n        padding: 0px 1px 0px 0px;\n      }\n      button:hover {\n        cursor: pointer;\n      }\n    }\n  }\n`;\nclass LinkEditor extends Component {\n    static template = \"o-spreadsheet-LinkEditor\";\n    static props = {\n        cellPosition: Object,\n        onClosed: { type: Function, optional: true },\n    };\n    static components = { Menu };\n    menuItems = linkMenuRegistry.getMenuItems();\n    link = useState(this.defaultState);\n    menu = useState({\n        isOpen: false,\n    });\n    linkEditorRef = useRef(\"linkEditor\");\n    position = useAbsoluteBoundingRect(this.linkEditorRef);\n    urlInput = useRef(\"urlInput\");\n    setup() {\n        onMounted(() => this.urlInput.el?.focus());\n    }\n    get defaultState() {\n        const { col, row } = this.props.cellPosition;\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const cell = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });\n        if (cell.link) {\n            return {\n                url: cell.link.url,\n                label: cell.formattedValue,\n                isUrlEditable: cell.link.isUrlEditable,\n            };\n        }\n        return {\n            label: cell.formattedValue,\n            url: \"\",\n            isUrlEditable: true,\n        };\n    }\n    get menuPosition() {\n        return {\n            x: this.position.x + MENU_OFFSET_X - PADDING - 2,\n            y: this.position.y + MENU_OFFSET_Y,\n        };\n    }\n    onSpecialLink(ev) {\n        const { detail: markdownLink } = ev;\n        const link = detectLink(markdownLink);\n        if (!link) {\n            return;\n        }\n        this.link.url = link.url;\n        this.link.label = link.label;\n        this.link.isUrlEditable = link.isUrlEditable;\n    }\n    getUrlRepresentation(link) {\n        return urlRepresentation(link, this.env.model.getters);\n    }\n    openMenu() {\n        this.menu.isOpen = true;\n    }\n    removeLink() {\n        this.link.url = \"\";\n        this.link.isUrlEditable = true;\n    }\n    save() {\n        const { col, row } = this.props.cellPosition;\n        const locale = this.env.model.getters.getLocale();\n        const label = this.link.label\n            ? canonicalizeNumberContent(this.link.label, locale)\n            : this.link.url;\n        this.env.model.dispatch(\"UPDATE_CELL\", {\n            col: col,\n            row: row,\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            content: markdownLink(label, this.link.url),\n        });\n        this.props.onClosed?.();\n    }\n    cancel() {\n        this.props.onClosed?.();\n    }\n    onKeyDown(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                if (this.link.url) {\n                    this.save();\n                }\n                ev.stopPropagation();\n                ev.preventDefault();\n                break;\n            case \"Escape\":\n                this.cancel();\n                ev.stopPropagation();\n                break;\n        }\n    }\n}\nconst LinkEditorPopoverBuilder = {\n    onOpen: (position, getters) => {\n        return {\n            isOpen: true,\n            props: { cellPosition: position },\n            Component: LinkEditor,\n            cellCorner: \"BottomLeft\",\n        };\n    },\n};\n\ncellPopoverRegistry\n    .add(\"ErrorToolTip\", ErrorToolTipPopoverBuilder)\n    .add(\"LinkCell\", LinkCellPopoverBuilder)\n    .add(\"LinkEditor\", LinkEditorPopoverBuilder)\n    .add(\"FilterMenu\", FilterMenuPopoverBuilder);\n\n/**\n * Create a function used to create a Chart based on the definition\n */\nfunction chartFactory(getters) {\n    const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);\n    function createChart(id, definition, sheetId) {\n        const builder = builders.find((builder) => builder.match(definition.type));\n        if (!builder) {\n            throw new Error(`No builder for this chart: ${definition.type}`);\n        }\n        return builder.createChart(definition, sheetId, getters);\n    }\n    return createChart;\n}\n/**\n * Create a function used to create a Chart Runtime based on the chart class\n * instance\n */\nfunction chartRuntimeFactory(getters) {\n    const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);\n    function createRuntimeChart(chart) {\n        const builder = builders.find((builder) => builder.match(chart.type));\n        if (!builder) {\n            throw new Error(\"No runtime builder for this chart.\");\n        }\n        return builder.getChartRuntime(chart, getters);\n    }\n    return createRuntimeChart;\n}\n/**\n * Validate the chart definition given in arguments\n */\nfunction validateChartDefinition(validator, definition) {\n    const validators = chartRegistry.getAll().find((validator) => validator.match(definition.type));\n    if (!validators) {\n        throw new Error(\"Unknown chart type.\");\n    }\n    return validators.validateChartDefinition(validator, definition);\n}\n/**\n * Get a new chart definition transformed with the executed command. This\n * functions will be called during operational transform process\n */\nfunction transformDefinition(definition, executed) {\n    const transformation = chartRegistry.getAll().find((factory) => factory.match(definition.type));\n    if (!transformation) {\n        throw new Error(\"Unknown chart type.\");\n    }\n    return transformation.transformDefinition(definition, executed);\n}\n/**\n * Return a \"smart\" chart definition in the given zone. The definition is \"smart\" because it will\n * use the best type of chart to display the data of the zone.\n *\n * It will also try to find labels and datasets in the range, and try to find title for the datasets.\n *\n * The type of chart will be :\n * - If the zone is a single non-empty cell, returns a scorecard\n * - If the all the labels are numbers/date, returns a line chart\n * - Else returns a bar chart\n */\nfunction getSmartChartDefinition(zone, getters) {\n    let dataSetZone = zone;\n    const singleColumn = zoneToDimension(zone).numberOfCols === 1;\n    if (!singleColumn) {\n        dataSetZone = { ...zone, left: zone.left + 1 };\n    }\n    const dataRange = zoneToXc(dataSetZone);\n    const dataSets = [{ dataRange, yAxisId: \"y\" }];\n    const sheetId = getters.getActiveSheetId();\n    const topLeftCell = getters.getCell({ sheetId, col: zone.left, row: zone.top });\n    if (getZoneArea(zone) === 1 && topLeftCell?.content) {\n        return {\n            type: \"scorecard\",\n            title: {},\n            background: topLeftCell.style?.fillColor || undefined,\n            keyValue: zoneToXc(zone),\n            baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,\n            baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,\n            baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,\n        };\n    }\n    const cellsInFirstRow = getters.getEvaluatedCellsInZone(sheetId, {\n        ...dataSetZone,\n        bottom: dataSetZone.top,\n    });\n    const dataSetsHaveTitle = !!cellsInFirstRow.find((cell) => cell.type !== CellValueType.empty && cell.type !== CellValueType.number);\n    let labelRangeXc;\n    if (!singleColumn) {\n        labelRangeXc = zoneToXc({\n            ...zone,\n            right: zone.left,\n        });\n    }\n    // Only display legend for several datasets.\n    const newLegendPos = dataSetZone.right === dataSetZone.left ? \"none\" : \"top\";\n    const labelRange = labelRangeXc ? getters.getRangeFromSheetXC(sheetId, labelRangeXc) : undefined;\n    if (canChartParseLabels(labelRange, getters)) {\n        return {\n            title: {},\n            dataSets,\n            labelsAsText: false,\n            stacked: false,\n            aggregated: false,\n            cumulative: false,\n            labelRange: labelRangeXc,\n            type: \"line\",\n            dataSetsHaveTitle,\n            legendPosition: newLegendPos,\n        };\n    }\n    const _dataSets = createDataSets(getters, dataSets, sheetId, dataSetsHaveTitle);\n    if (singleColumn &&\n        getData(getters, _dataSets[0]).every((e) => typeof e === \"string\" && !isEvaluationError(e))) {\n        return {\n            title: {},\n            dataSets: [{ dataRange }],\n            aggregated: true,\n            labelRange: dataRange,\n            type: \"pie\",\n            legendPosition: \"top\",\n            dataSetsHaveTitle: false,\n        };\n    }\n    return {\n        title: {},\n        dataSets,\n        labelRange: labelRangeXc,\n        type: \"bar\",\n        stacked: false,\n        aggregated: false,\n        dataSetsHaveTitle,\n        legendPosition: newLegendPos,\n    };\n}\n\n/**\n * Create a table on the selected zone, with UI warnings to the user if the creation fails.\n * If a single cell is selected, expand the selection to non-empty adjacent cells to create a table.\n */\nfunction interactiveCreateTable(env, sheetId, tableConfig) {\n    let target = env.model.getters.getSelectedZones();\n    let isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);\n    if (target.length === 1 && !isDynamic && getZoneArea(target[0]) === 1) {\n        env.model.selection.selectTableAroundSelection();\n        target = env.model.getters.getSelectedZones();\n        isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);\n    }\n    const ranges = target.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));\n    const result = env.model.dispatch(\"CREATE_TABLE\", {\n        ranges,\n        sheetId,\n        config: tableConfig,\n        tableType: isDynamic ? \"dynamic\" : \"static\",\n    });\n    if (result.isCancelledBecause(\"TableOverlap\" /* CommandResult.TableOverlap */)) {\n        env.raiseError(TableTerms.Errors.TableOverlap);\n    }\n    else if (result.isCancelledBecause(\"NonContinuousTargets\" /* CommandResult.NonContinuousTargets */)) {\n        env.raiseError(TableTerms.Errors.NonContinuousTargets);\n    }\n    return result;\n}\n\n//------------------------------------------------------------------------------\n// Helpers\n//------------------------------------------------------------------------------\nfunction setFormatter(env, format) {\n    env.model.dispatch(\"SET_FORMATTING_WITH_PIVOT\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        target: env.model.getters.getSelectedZones(),\n        format,\n    });\n}\nfunction setStyle(env, style) {\n    env.model.dispatch(\"SET_FORMATTING\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        target: env.model.getters.getSelectedZones(),\n        style,\n    });\n}\n//------------------------------------------------------------------------------\n// Simple actions\n//------------------------------------------------------------------------------\nconst PASTE_ACTION = async (env) => paste$1(env);\nconst PASTE_AS_VALUE_ACTION = async (env) => paste$1(env, \"asValue\");\nasync function paste$1(env, pasteOption) {\n    const osClipboard = await env.clipboard.read();\n    switch (osClipboard.status) {\n        case \"ok\":\n            const clipboardContent = parseOSClipboardContent(osClipboard.content);\n            const clipboardId = clipboardContent.data?.clipboardId;\n            const target = env.model.getters.getSelectedZones();\n            if (env.model.getters.getClipboardId() !== clipboardId) {\n                interactivePasteFromOS(env, target, clipboardContent, pasteOption);\n            }\n            else {\n                interactivePaste(env, target, pasteOption);\n            }\n            if (env.model.getters.isCutOperation() && pasteOption !== \"asValue\") {\n                await env.clipboard.write({ [ClipboardMIMEType.PlainText]: \"\" });\n            }\n            break;\n        case \"notImplemented\":\n            env.raiseError(_t(\"Pasting from the context menu is not supported in this browser. Use keyboard shortcuts ctrl+c / ctrl+v instead.\"));\n            break;\n        case \"permissionDenied\":\n            env.raiseError(_t(\"Access to the clipboard denied by the browser. Please enable clipboard permission for this page in your browser settings.\"));\n            break;\n    }\n}\nconst PASTE_FORMAT_ACTION = (env) => paste$1(env, \"onlyFormat\");\n//------------------------------------------------------------------------------\n// Grid manipulations\n//------------------------------------------------------------------------------\nconst DELETE_CONTENT_ROWS_NAME = (env) => {\n    if (env.model.getters.getSelectedZones().length > 1) {\n        return _t(\"Clear rows\");\n    }\n    let first;\n    let last;\n    const activesRows = env.model.getters.getActiveRows();\n    if (activesRows.size !== 0) {\n        first = largeMin([...activesRows]);\n        last = largeMax([...activesRows]);\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        first = zone.top;\n        last = zone.bottom;\n    }\n    if (first === last) {\n        return _t(\"Clear row %s\", (first + 1).toString());\n    }\n    return _t(\"Clear rows %s - %s\", (first + 1).toString(), (last + 1).toString());\n};\nconst DELETE_CONTENT_ROWS_ACTION = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const target = [...env.model.getters.getActiveRows()].map((index) => env.model.getters.getRowsZone(sheetId, index, index));\n    env.model.dispatch(\"DELETE_CONTENT\", {\n        target,\n        sheetId: env.model.getters.getActiveSheetId(),\n    });\n};\nconst DELETE_CONTENT_COLUMNS_NAME = (env) => {\n    if (env.model.getters.getSelectedZones().length > 1) {\n        return _t(\"Clear columns\");\n    }\n    let first;\n    let last;\n    const activeCols = env.model.getters.getActiveCols();\n    if (activeCols.size !== 0) {\n        first = largeMin([...activeCols]);\n        last = largeMax([...activeCols]);\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        first = zone.left;\n        last = zone.right;\n    }\n    if (first === last) {\n        return _t(\"Clear column %s\", numberToLetters(first));\n    }\n    return _t(\"Clear columns %s - %s\", numberToLetters(first), numberToLetters(last));\n};\nconst DELETE_CONTENT_COLUMNS_ACTION = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const target = [...env.model.getters.getActiveCols()].map((index) => env.model.getters.getColsZone(sheetId, index, index));\n    env.model.dispatch(\"DELETE_CONTENT\", {\n        target,\n        sheetId: env.model.getters.getActiveSheetId(),\n    });\n};\nconst REMOVE_ROWS_NAME = (env) => {\n    if (env.model.getters.getSelectedZones().length > 1) {\n        return _t(\"Delete rows\");\n    }\n    let first;\n    let last;\n    const activesRows = env.model.getters.getActiveRows();\n    if (activesRows.size !== 0) {\n        first = largeMin([...activesRows]);\n        last = largeMax([...activesRows]);\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        first = zone.top;\n        last = zone.bottom;\n    }\n    if (first === last) {\n        return _t(\"Delete row %s\", (first + 1).toString());\n    }\n    return _t(\"Delete rows %s - %s\", (first + 1).toString(), (last + 1).toString());\n};\nconst REMOVE_ROWS_ACTION = (env) => {\n    let rows = [...env.model.getters.getActiveRows()];\n    if (!rows.length) {\n        const zone = env.model.getters.getSelectedZones()[0];\n        for (let i = zone.top; i <= zone.bottom; i++) {\n            rows.push(i);\n        }\n    }\n    env.model.dispatch(\"REMOVE_COLUMNS_ROWS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        dimension: \"ROW\",\n        elements: rows,\n    });\n};\nconst CAN_REMOVE_COLUMNS_ROWS = (dimension, env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const selectedElements = env.model.getters.getElementsFromSelection(dimension);\n    const includesAllVisibleHeaders = env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, dimension, selectedElements);\n    const includesAllNonFrozenHeaders = env.model.getters.checkElementsIncludeAllNonFrozenHeaders(sheetId, dimension, selectedElements);\n    return !includesAllVisibleHeaders && !includesAllNonFrozenHeaders;\n};\nconst REMOVE_COLUMNS_NAME = (env) => {\n    if (env.model.getters.getSelectedZones().length > 1) {\n        return _t(\"Delete columns\");\n    }\n    let first;\n    let last;\n    const activeCols = env.model.getters.getActiveCols();\n    if (activeCols.size !== 0) {\n        first = largeMin([...activeCols]);\n        last = largeMax([...activeCols]);\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        first = zone.left;\n        last = zone.right;\n    }\n    if (first === last) {\n        return _t(\"Delete column %s\", numberToLetters(first));\n    }\n    return _t(\"Delete columns %s - %s\", numberToLetters(first), numberToLetters(last));\n};\nconst NOT_ALL_VISIBLE_ROWS_SELECTED = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const selectedRows = env.model.getters.getElementsFromSelection(\"ROW\");\n    return !env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, \"ROW\", selectedRows);\n};\nconst REMOVE_COLUMNS_ACTION = (env) => {\n    let columns = [...env.model.getters.getActiveCols()];\n    if (!columns.length) {\n        const zone = env.model.getters.getSelectedZones()[0];\n        for (let i = zone.left; i <= zone.right; i++) {\n            columns.push(i);\n        }\n    }\n    env.model.dispatch(\"REMOVE_COLUMNS_ROWS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        dimension: \"COL\",\n        elements: columns,\n    });\n};\nconst NOT_ALL_VISIBLE_COLS_SELECTED = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const selectedCols = env.model.getters.getElementsFromSelection(\"COL\");\n    return !env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, \"COL\", selectedCols);\n};\nconst INSERT_ROWS_BEFORE_ACTION = (env) => {\n    const activeRows = env.model.getters.getActiveRows();\n    let row;\n    let quantity;\n    if (activeRows.size) {\n        row = largeMin([...activeRows]);\n        quantity = activeRows.size;\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        row = zone.top;\n        quantity = zone.bottom - zone.top + 1;\n    }\n    env.model.dispatch(\"ADD_COLUMNS_ROWS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        position: \"before\",\n        base: row,\n        quantity,\n        dimension: \"ROW\",\n    });\n};\nconst INSERT_ROWS_AFTER_ACTION = (env) => {\n    const activeRows = env.model.getters.getActiveRows();\n    let row;\n    let quantity;\n    if (activeRows.size) {\n        row = largeMax([...activeRows]);\n        quantity = activeRows.size;\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        row = zone.bottom;\n        quantity = zone.bottom - zone.top + 1;\n    }\n    env.model.dispatch(\"ADD_COLUMNS_ROWS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        position: \"after\",\n        base: row,\n        quantity,\n        dimension: \"ROW\",\n    });\n};\nconst INSERT_COLUMNS_BEFORE_ACTION = (env) => {\n    const activeCols = env.model.getters.getActiveCols();\n    let column;\n    let quantity;\n    if (activeCols.size) {\n        column = largeMin([...activeCols]);\n        quantity = activeCols.size;\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        column = zone.left;\n        quantity = zone.right - zone.left + 1;\n    }\n    env.model.dispatch(\"ADD_COLUMNS_ROWS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        position: \"before\",\n        dimension: \"COL\",\n        base: column,\n        quantity,\n    });\n};\nconst INSERT_COLUMNS_AFTER_ACTION = (env) => {\n    const activeCols = env.model.getters.getActiveCols();\n    let column;\n    let quantity;\n    if (activeCols.size) {\n        column = largeMax([...activeCols]);\n        quantity = activeCols.size;\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        column = zone.right;\n        quantity = zone.right - zone.left + 1;\n    }\n    env.model.dispatch(\"ADD_COLUMNS_ROWS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        position: \"after\",\n        dimension: \"COL\",\n        base: column,\n        quantity,\n    });\n};\nconst HIDE_COLUMNS_NAME = (env) => {\n    const cols = env.model.getters.getElementsFromSelection(\"COL\");\n    let first = cols[0];\n    let last = cols[cols.length - 1];\n    if (cols.length === 1) {\n        return _t(\"Hide column %s\", numberToLetters(first).toString());\n    }\n    else if (last - first + 1 === cols.length) {\n        return _t(\"Hide columns %s - %s\", numberToLetters(first).toString(), numberToLetters(last).toString());\n    }\n    else {\n        return _t(\"Hide columns\");\n    }\n};\nconst HIDE_ROWS_NAME = (env) => {\n    const rows = env.model.getters.getElementsFromSelection(\"ROW\");\n    let first = rows[0];\n    let last = rows[rows.length - 1];\n    if (rows.length === 1) {\n        return _t(\"Hide row %s\", (first + 1).toString());\n    }\n    else if (last - first + 1 === rows.length) {\n        return _t(\"Hide rows %s - %s\", (first + 1).toString(), (last + 1).toString());\n    }\n    else {\n        return _t(\"Hide rows\");\n    }\n};\n//------------------------------------------------------------------------------\n// Charts\n//------------------------------------------------------------------------------\nconst CREATE_CHART = (env) => {\n    const getters = env.model.getters;\n    const id = env.model.uuidGenerator.uuidv4();\n    const sheetId = getters.getActiveSheetId();\n    if (getZoneArea(env.model.getters.getSelectedZone()) === 1) {\n        env.model.selection.selectTableAroundSelection();\n    }\n    const size = { width: DEFAULT_FIGURE_WIDTH, height: DEFAULT_FIGURE_HEIGHT };\n    const position = getChartPositionAtCenterOfViewport(getters, size);\n    const result = env.model.dispatch(\"CREATE_CHART\", {\n        sheetId,\n        id,\n        position,\n        size,\n        definition: getSmartChartDefinition(env.model.getters.getSelectedZone(), env.model.getters),\n    });\n    if (result.isSuccessful) {\n        env.model.dispatch(\"SELECT_FIGURE\", { id });\n        env.openSidePanel(\"ChartPanel\");\n    }\n};\n//------------------------------------------------------------------------------\n// Pivots\n//------------------------------------------------------------------------------\nconst CREATE_PIVOT = (env) => {\n    const pivotId = env.model.uuidGenerator.uuidv4();\n    const newSheetId = env.model.uuidGenerator.uuidv4();\n    const result = env.model.dispatch(\"INSERT_NEW_PIVOT\", { pivotId, newSheetId });\n    if (result.isSuccessful) {\n        env.openSidePanel(\"PivotSidePanel\", { pivotId });\n    }\n};\nconst REINSERT_DYNAMIC_PIVOT_CHILDREN = (env) => env.model.getters.getPivotIds().map((pivotId, index) => ({\n    id: `reinsert_dynamic_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,\n    name: env.model.getters.getPivotDisplayName(pivotId),\n    sequence: index,\n    execute: (env) => {\n        const zone = env.model.getters.getSelectedZone();\n        const table = env.model.getters.getPivot(pivotId).getTableStructure().export();\n        env.model.dispatch(\"INSERT_PIVOT_WITH_TABLE\", {\n            pivotId,\n            table,\n            col: zone.left,\n            row: zone.top,\n            sheetId: env.model.getters.getActiveSheetId(),\n            pivotMode: \"dynamic\",\n        });\n        env.model.dispatch(\"REFRESH_PIVOT\", { id: pivotId });\n    },\n    isVisible: (env) => env.model.getters.getPivot(pivotId).isValid(),\n}));\nconst REINSERT_STATIC_PIVOT_CHILDREN = (env) => env.model.getters.getPivotIds().map((pivotId, index) => ({\n    id: `reinsert_static_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,\n    name: env.model.getters.getPivotDisplayName(pivotId),\n    sequence: index,\n    execute: (env) => {\n        const zone = env.model.getters.getSelectedZone();\n        const table = env.model.getters.getPivot(pivotId).getTableStructure().export();\n        env.model.dispatch(\"INSERT_PIVOT_WITH_TABLE\", {\n            pivotId,\n            table,\n            col: zone.left,\n            row: zone.top,\n            sheetId: env.model.getters.getActiveSheetId(),\n            pivotMode: \"static\",\n        });\n        env.model.dispatch(\"REFRESH_PIVOT\", { id: pivotId });\n    },\n    isVisible: (env) => env.model.getters.getPivot(pivotId).isValid(),\n}));\n//------------------------------------------------------------------------------\n// Image\n//------------------------------------------------------------------------------\nasync function requestImage(env) {\n    try {\n        return await env.imageProvider.requestImage();\n    }\n    catch {\n        env.raiseError(_t(\"An unexpected error occurred during the image transfer\"));\n        return undefined;\n    }\n}\nconst CREATE_IMAGE = async (env) => {\n    if (env.imageProvider) {\n        const sheetId = env.model.getters.getActiveSheetId();\n        const figureId = env.model.uuidGenerator.uuidv4();\n        const image = await requestImage(env);\n        if (!image) {\n            throw new Error(\"No image provider was given to the environment\");\n        }\n        const size = getMaxFigureSize(env.model.getters, image.size);\n        const position = centerFigurePosition(env.model.getters, size);\n        env.model.dispatch(\"CREATE_IMAGE\", {\n            sheetId,\n            figureId,\n            position,\n            size,\n            definition: image,\n        });\n    }\n};\n//------------------------------------------------------------------------------\n// Style/Format\n//------------------------------------------------------------------------------\nconst FORMAT_PERCENT_ACTION = (env) => setFormatter(env, \"0.00%\");\n//------------------------------------------------------------------------------\n// Side panel\n//------------------------------------------------------------------------------\nconst OPEN_CF_SIDEPANEL_ACTION = (env) => {\n    env.openSidePanel(\"ConditionalFormatting\", { selection: env.model.getters.getSelectedZones() });\n};\nconst INSERT_LINK = (env) => {\n    let { col, row } = env.model.getters.getActivePosition();\n    env.getStore(CellPopoverStore).open({ col, row }, \"LinkEditor\");\n};\nconst INSERT_LINK_NAME = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const { col, row } = env.model.getters.getActivePosition();\n    const cell = env.model.getters.getEvaluatedCell({ sheetId, col, row });\n    return cell && cell.link ? _t(\"Edit link\") : _t(\"Insert link\");\n};\n//------------------------------------------------------------------------------\n// Filters action\n//------------------------------------------------------------------------------\nconst SELECTED_TABLE_HAS_FILTERS = (env) => {\n    const table = env.model.getters.getFirstTableInSelection();\n    return table?.config.hasFilters || false;\n};\nconst SELECTION_CONTAINS_SINGLE_TABLE = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const selectedZones = env.model.getters.getSelectedZones();\n    return env.model.getters.getTablesOverlappingZones(sheetId, selectedZones).length === 1;\n};\nconst IS_SELECTION_CONTINUOUS = (env) => {\n    return areZonesContinuous(env.model.getters.getSelectedZones());\n};\nconst ADD_DATA_FILTER = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const table = env.model.getters.getFirstTableInSelection();\n    if (table) {\n        env.model.dispatch(\"UPDATE_TABLE\", {\n            sheetId,\n            zone: table.range.zone,\n            config: { hasFilters: true },\n        });\n    }\n    else {\n        const tableConfig = {\n            ...DEFAULT_TABLE_CONFIG,\n            hasFilters: true,\n            bandedRows: false,\n            styleId: \"TableStyleLight11\",\n        };\n        interactiveCreateTable(env, sheetId, tableConfig);\n    }\n};\nconst REMOVE_DATA_FILTER = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const table = env.model.getters.getFirstTableInSelection();\n    if (!table) {\n        return;\n    }\n    env.model.dispatch(\"UPDATE_TABLE\", {\n        sheetId,\n        zone: table.range.zone,\n        config: { hasFilters: false },\n    });\n};\nconst INSERT_TABLE = (env) => {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const result = interactiveCreateTable(env, sheetId);\n    if (result.isSuccessful) {\n        env.openSidePanel(\"TableSidePanel\", {});\n    }\n};\nconst DELETE_SELECTED_TABLE = (env) => {\n    const position = env.model.getters.getActivePosition();\n    const table = env.model.getters.getTable(position);\n    if (!table) {\n        return;\n    }\n    env.model.dispatch(\"REMOVE_TABLE\", {\n        sheetId: position.sheetId,\n        target: [table.range.zone],\n    });\n};\n//------------------------------------------------------------------------------\n// Sorting action\n//------------------------------------------------------------------------------\nconst IS_ONLY_ONE_RANGE = (env) => {\n    return env.model.getters.getSelectedZones().length === 1;\n};\nconst CAN_INSERT_HEADER = (env, dimension) => {\n    if (!IS_ONLY_ONE_RANGE(env)) {\n        return false;\n    }\n    const activeHeaders = dimension === \"COL\" ? env.model.getters.getActiveCols() : env.model.getters.getActiveRows();\n    const ortogonalActiveHeaders = dimension === \"COL\" ? env.model.getters.getActiveRows() : env.model.getters.getActiveCols();\n    const sheetId = env.model.getters.getActiveSheetId();\n    const zone = env.model.getters.getSelectedZone();\n    const allSheetSelected = isEqual(zone, env.model.getters.getSheetZone(sheetId));\n    return isConsecutive(activeHeaders) && (ortogonalActiveHeaders.size === 0 || allSheetSelected);\n};\nconst CREATE_OR_REMOVE_FILTER_ACTION = {\n    name: (env) => SELECTED_TABLE_HAS_FILTERS(env) ? _t(\"Remove selected filters\") : _t(\"Add filters\"),\n    isEnabled: (env) => IS_SELECTION_CONTINUOUS(env),\n    execute: (env) => SELECTED_TABLE_HAS_FILTERS(env) ? REMOVE_DATA_FILTER(env) : ADD_DATA_FILTER(env),\n    icon: \"o-spreadsheet-Icon.FILTER_ICON_ACTIVE\",\n};\n\nconst undo = {\n    name: _t(\"Undo\"),\n    description: \"Ctrl+Z\",\n    execute: (env) => env.model.dispatch(\"REQUEST_UNDO\"),\n    isEnabled: (env) => env.model.getters.canUndo(),\n    icon: \"o-spreadsheet-Icon.UNDO\",\n};\nconst redo = {\n    name: _t(\"Redo\"),\n    description: \"Ctrl+Y\",\n    execute: (env) => env.model.dispatch(\"REQUEST_REDO\"),\n    isEnabled: (env) => env.model.getters.canRedo(),\n    icon: \"o-spreadsheet-Icon.REDO\",\n};\nconst copy = {\n    name: _t(\"Copy\"),\n    description: \"Ctrl+C\",\n    isReadonlyAllowed: true,\n    execute: async (env) => {\n        env.model.dispatch(\"COPY\");\n        await env.clipboard.write(env.model.getters.getClipboardContent());\n    },\n    icon: \"o-spreadsheet-Icon.COPY\",\n};\nconst cut = {\n    name: _t(\"Cut\"),\n    description: \"Ctrl+X\",\n    execute: async (env) => {\n        interactiveCut(env);\n        await env.clipboard.write(env.model.getters.getClipboardContent());\n    },\n    icon: \"o-spreadsheet-Icon.CUT\",\n};\nconst paste = {\n    name: _t(\"Paste\"),\n    description: \"Ctrl+V\",\n    execute: PASTE_ACTION,\n    icon: \"o-spreadsheet-Icon.PASTE\",\n};\nconst pasteSpecial = {\n    name: _t(\"Paste special\"),\n    isVisible: (env) => {\n        return !env.model.getters.isCutOperation();\n    },\n    icon: \"o-spreadsheet-Icon.PASTE\",\n};\nconst pasteSpecialValue = {\n    name: _t(\"Paste as value\"),\n    description: \"Ctrl+Shift+V\",\n    execute: PASTE_AS_VALUE_ACTION,\n};\nconst pasteSpecialFormat = {\n    name: _t(\"Paste format only\"),\n    execute: PASTE_FORMAT_ACTION,\n};\nconst findAndReplace = {\n    name: _t(\"Find and replace\"),\n    description: \"Ctrl+H\",\n    isReadonlyAllowed: true,\n    execute: (env) => {\n        env.openSidePanel(\"FindAndReplace\", {});\n    },\n    icon: \"o-spreadsheet-Icon.SEARCH\",\n};\nconst deleteValues = {\n    name: _t(\"Delete values\"),\n    execute: (env) => env.model.dispatch(\"DELETE_CONTENT\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        target: env.model.getters.getSelectedZones(),\n    }),\n};\nconst deleteRows = {\n    name: REMOVE_ROWS_NAME,\n    execute: REMOVE_ROWS_ACTION,\n    isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS(\"ROW\", env),\n};\nconst deleteRow = {\n    ...deleteRows,\n    isVisible: IS_ONLY_ONE_RANGE,\n};\nconst clearRows = {\n    name: DELETE_CONTENT_ROWS_NAME,\n    execute: DELETE_CONTENT_ROWS_ACTION,\n};\nconst deleteCols = {\n    name: REMOVE_COLUMNS_NAME,\n    execute: REMOVE_COLUMNS_ACTION,\n    isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS(\"COL\", env),\n};\nconst deleteCol = {\n    ...deleteCols,\n    isVisible: IS_ONLY_ONE_RANGE,\n};\nconst clearCols = {\n    name: DELETE_CONTENT_COLUMNS_NAME,\n    execute: DELETE_CONTENT_COLUMNS_ACTION,\n};\nconst deleteCells = {\n    name: _t(\"Delete cells\"),\n    isVisible: IS_ONLY_ONE_RANGE,\n};\nconst deleteCellShiftUp = {\n    name: _t(\"Delete cell and shift up\"),\n    execute: (env) => {\n        const zone = env.model.getters.getSelectedZone();\n        const result = env.model.dispatch(\"DELETE_CELL\", { zone, shiftDimension: \"ROW\" });\n        handlePasteResult(env, result);\n    },\n};\nconst deleteCellShiftLeft = {\n    name: _t(\"Delete cell and shift left\"),\n    execute: (env) => {\n        const zone = env.model.getters.getSelectedZone();\n        const result = env.model.dispatch(\"DELETE_CELL\", { zone, shiftDimension: \"COL\" });\n        handlePasteResult(env, result);\n    },\n};\nconst mergeCells = {\n    name: _t(\"Merge cells\"),\n    isEnabled: (env) => !cannotMerge(env),\n    isActive: (env) => isInMerge(env),\n    execute: (env) => toggleMerge(env),\n    icon: \"o-spreadsheet-Icon.MERGE_CELL\",\n};\nconst editTable = {\n    name: () => _t(\"Edit table\"),\n    execute: (env) => env.openSidePanel(\"TableSidePanel\", {}),\n    icon: \"o-spreadsheet-Icon.EDIT_TABLE\",\n};\nconst deleteTable = {\n    name: () => _t(\"Delete table\"),\n    execute: DELETE_SELECTED_TABLE,\n    icon: \"o-spreadsheet-Icon.DELETE_TABLE\",\n};\nfunction cannotMerge(env) {\n    const zones = env.model.getters.getSelectedZones();\n    const { top, left, right, bottom } = env.model.getters.getSelectedZone();\n    const { sheetId } = env.model.getters.getActivePosition();\n    const { xSplit, ySplit } = env.model.getters.getPaneDivisions(sheetId);\n    return (zones.length > 1 ||\n        (top === bottom && left === right) ||\n        (left < xSplit && xSplit <= right) ||\n        (top < ySplit && ySplit <= bottom));\n}\nfunction isInMerge(env) {\n    if (!cannotMerge(env)) {\n        const zones = env.model.getters.getSelectedZones();\n        const { col, row, sheetId } = env.model.getters.getActivePosition();\n        const zone = env.model.getters.expandZone(sheetId, positionToZone({ col, row }));\n        return isEqual(zones[0], zone);\n    }\n    return false;\n}\nfunction toggleMerge(env) {\n    if (cannotMerge(env)) {\n        return;\n    }\n    const zones = env.model.getters.getSelectedZones();\n    const target = [zones[zones.length - 1]];\n    const sheetId = env.model.getters.getActiveSheetId();\n    if (isInMerge(env)) {\n        env.model.dispatch(\"REMOVE_MERGE\", { sheetId, target });\n    }\n    else {\n        interactiveAddMerge(env, sheetId, target);\n    }\n}\n\nvar ACTION_EDIT = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    clearCols: clearCols,\n    clearRows: clearRows,\n    copy: copy,\n    cut: cut,\n    deleteCellShiftLeft: deleteCellShiftLeft,\n    deleteCellShiftUp: deleteCellShiftUp,\n    deleteCells: deleteCells,\n    deleteCol: deleteCol,\n    deleteCols: deleteCols,\n    deleteRow: deleteRow,\n    deleteRows: deleteRows,\n    deleteTable: deleteTable,\n    deleteValues: deleteValues,\n    editTable: editTable,\n    findAndReplace: findAndReplace,\n    mergeCells: mergeCells,\n    paste: paste,\n    pasteSpecial: pasteSpecial,\n    pasteSpecialFormat: pasteSpecialFormat,\n    pasteSpecialValue: pasteSpecialValue,\n    redo: redo,\n    undo: undo\n});\n\nconst insertRow = {\n    name: (env) => {\n        const number = getRowsNumber(env);\n        return number === 1 ? _t(\"Insert row\") : _t(\"Insert %s rows\", number.toString());\n    },\n    isVisible: (env) => CAN_INSERT_HEADER(env, \"ROW\"),\n    icon: \"o-spreadsheet-Icon.INSERT_ROW\",\n};\nconst rowInsertRowBefore = {\n    name: (env) => {\n        const number = getRowsNumber(env);\n        return number === 1 ? _t(\"Insert row above\") : _t(\"Insert %s rows above\", number.toString());\n    },\n    execute: INSERT_ROWS_BEFORE_ACTION,\n    isVisible: (env) => CAN_INSERT_HEADER(env, \"ROW\"),\n    icon: \"o-spreadsheet-Icon.INSERT_ROW_BEFORE\",\n};\nconst topBarInsertRowsBefore = {\n    ...rowInsertRowBefore,\n    name: (env) => {\n        const number = getRowsNumber(env);\n        if (number === 1) {\n            return _t(\"Row above\");\n        }\n        return _t(\"%s Rows above\", number.toString());\n    },\n};\nconst cellInsertRowsBefore = {\n    ...rowInsertRowBefore,\n    name: (env) => {\n        const number = getRowsNumber(env);\n        if (number === 1) {\n            return _t(\"Insert row\");\n        }\n        return _t(\"Insert %s rows\", number.toString());\n    },\n    isVisible: IS_ONLY_ONE_RANGE,\n    icon: \"o-spreadsheet-Icon.INSERT_ROW_BEFORE\",\n};\nconst rowInsertRowsAfter = {\n    execute: INSERT_ROWS_AFTER_ACTION,\n    name: (env) => {\n        const number = getRowsNumber(env);\n        return number === 1 ? _t(\"Insert row below\") : _t(\"Insert %s rows below\", number.toString());\n    },\n    isVisible: (env) => CAN_INSERT_HEADER(env, \"ROW\"),\n    icon: \"o-spreadsheet-Icon.INSERT_ROW_AFTER\",\n};\nconst topBarInsertRowsAfter = {\n    ...rowInsertRowsAfter,\n    name: (env) => {\n        const number = getRowsNumber(env);\n        if (number === 1) {\n            return _t(\"Row below\");\n        }\n        return _t(\"%s Rows below\", number.toString());\n    },\n};\nconst insertCol = {\n    name: (env) => {\n        const number = getColumnsNumber(env);\n        return number === 1 ? _t(\"Insert column\") : _t(\"Insert %s columns\", number.toString());\n    },\n    isVisible: (env) => CAN_INSERT_HEADER(env, \"COL\"),\n    icon: \"o-spreadsheet-Icon.INSERT_COL\",\n};\nconst colInsertColsBefore = {\n    name: (env) => {\n        const number = getColumnsNumber(env);\n        return number === 1\n            ? _t(\"Insert column left\")\n            : _t(\"Insert %s columns left\", number.toString());\n    },\n    execute: INSERT_COLUMNS_BEFORE_ACTION,\n    isVisible: (env) => CAN_INSERT_HEADER(env, \"COL\"),\n    icon: \"o-spreadsheet-Icon.INSERT_COL_BEFORE\",\n};\nconst topBarInsertColsBefore = {\n    ...colInsertColsBefore,\n    name: (env) => {\n        const number = getColumnsNumber(env);\n        if (number === 1) {\n            return _t(\"Column left\");\n        }\n        return _t(\"%s Columns left\", number.toString());\n    },\n};\nconst cellInsertColsBefore = {\n    ...colInsertColsBefore,\n    name: (env) => {\n        const number = getColumnsNumber(env);\n        if (number === 1) {\n            return _t(\"Insert column\");\n        }\n        return _t(\"Insert %s columns\", number.toString());\n    },\n    isVisible: IS_ONLY_ONE_RANGE,\n    icon: \"o-spreadsheet-Icon.INSERT_COL_BEFORE\",\n};\nconst colInsertColsAfter = {\n    name: (env) => {\n        const number = getColumnsNumber(env);\n        return number === 1\n            ? _t(\"Insert column right\")\n            : _t(\"Insert %s columns right\", number.toString());\n    },\n    execute: INSERT_COLUMNS_AFTER_ACTION,\n    isVisible: (env) => CAN_INSERT_HEADER(env, \"COL\"),\n    icon: \"o-spreadsheet-Icon.INSERT_COL_AFTER\",\n};\nconst topBarInsertColsAfter = {\n    ...colInsertColsAfter,\n    name: (env) => {\n        const number = getColumnsNumber(env);\n        if (number === 1) {\n            return _t(\"Column right\");\n        }\n        return _t(\"%s Columns right\", number.toString());\n    },\n    execute: INSERT_COLUMNS_AFTER_ACTION,\n};\nconst insertCell = {\n    name: _t(\"Insert cells\"),\n    isVisible: (env) => IS_ONLY_ONE_RANGE(env) &&\n        env.model.getters.getActiveCols().size === 0 &&\n        env.model.getters.getActiveRows().size === 0,\n    icon: \"o-spreadsheet-Icon.INSERT_CELL\",\n};\nconst insertCellShiftDown = {\n    name: _t(\"Insert cells and shift down\"),\n    execute: (env) => {\n        const zone = env.model.getters.getSelectedZone();\n        const result = env.model.dispatch(\"INSERT_CELL\", { zone, shiftDimension: \"ROW\" });\n        handlePasteResult(env, result);\n    },\n    isVisible: (env) => env.model.getters.getActiveRows().size === 0 && env.model.getters.getActiveCols().size === 0,\n    icon: \"o-spreadsheet-Icon.INSERT_CELL_SHIFT_DOWN\",\n};\nconst insertCellShiftRight = {\n    name: _t(\"Insert cells and shift right\"),\n    execute: (env) => {\n        const zone = env.model.getters.getSelectedZone();\n        const result = env.model.dispatch(\"INSERT_CELL\", { zone, shiftDimension: \"COL\" });\n        handlePasteResult(env, result);\n    },\n    isVisible: (env) => env.model.getters.getActiveRows().size === 0 && env.model.getters.getActiveCols().size === 0,\n    icon: \"o-spreadsheet-Icon.INSERT_CELL_SHIFT_RIGHT\",\n};\nconst insertChart = {\n    name: _t(\"Chart\"),\n    execute: CREATE_CHART,\n    icon: \"o-spreadsheet-Icon.INSERT_CHART\",\n};\nconst insertPivot = {\n    name: _t(\"Pivot table\"),\n    execute: CREATE_PIVOT,\n    icon: \"o-spreadsheet-Icon.PIVOT\",\n};\nconst insertImage = {\n    name: _t(\"Image\"),\n    description: \"Ctrl+O\",\n    execute: CREATE_IMAGE,\n    isVisible: (env) => env.imageProvider !== undefined,\n    icon: \"o-spreadsheet-Icon.INSERT_IMAGE\",\n};\nconst insertTable = {\n    name: () => _t(\"Table\"),\n    execute: INSERT_TABLE,\n    isVisible: (env) => IS_SELECTION_CONTINUOUS(env) && !env.model.getters.getFirstTableInSelection(),\n    icon: \"o-spreadsheet-Icon.PAINT_TABLE\",\n};\nconst insertFunction = {\n    name: _t(\"Function\"),\n    icon: \"o-spreadsheet-Icon.FORMULA\",\n};\nconst insertFunctionSum = {\n    name: _t(\"SUM\"),\n    execute: (env) => env.startCellEdition(`=SUM(`),\n};\nconst insertFunctionAverage = {\n    name: _t(\"AVERAGE\"),\n    execute: (env) => env.startCellEdition(`=AVERAGE(`),\n};\nconst insertFunctionCount = {\n    name: _t(\"COUNT\"),\n    execute: (env) => env.startCellEdition(`=COUNT(`),\n};\nconst insertFunctionMax = {\n    name: _t(\"MAX\"),\n    execute: (env) => env.startCellEdition(`=MAX(`),\n};\nconst insertFunctionMin = {\n    name: _t(\"MIN\"),\n    execute: (env) => env.startCellEdition(`=MIN(`),\n};\nconst categorieFunctionAll = {\n    name: _t(\"All\"),\n    children: [allFunctionListMenuBuilder],\n};\nfunction allFunctionListMenuBuilder() {\n    const fnNames = functionRegistry.getKeys().filter((key) => !functionRegistry.get(key).hidden);\n    return createFormulaFunctions(fnNames);\n}\nconst categoriesFunctionListMenuBuilder = () => {\n    const functions = functionRegistry.content;\n    const categories = [\n        ...new Set(functionRegistry\n            .getAll()\n            .filter((fn) => !fn.hidden)\n            .map((fn) => fn.category)),\n    ].filter(isDefined);\n    return categories.sort().map((category, i) => {\n        const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category && !functions[key].hidden);\n        return {\n            name: category,\n            children: createFormulaFunctions(functionsInCategory),\n        };\n    });\n};\nconst insertLink = {\n    name: _t(\"Link\"),\n    execute: INSERT_LINK,\n    icon: \"o-spreadsheet-Icon.INSERT_LINK\",\n};\nconst insertCheckbox = {\n    name: _t(\"Checkbox\"),\n    execute: (env) => {\n        const zones = env.model.getters.getSelectedZones();\n        const sheetId = env.model.getters.getActiveSheetId();\n        const ranges = zones.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));\n        env.model.dispatch(\"ADD_DATA_VALIDATION_RULE\", {\n            ranges,\n            sheetId,\n            rule: {\n                id: env.model.uuidGenerator.uuidv4(),\n                criterion: {\n                    type: \"isBoolean\",\n                    values: [],\n                },\n            },\n        });\n    },\n    icon: \"o-spreadsheet-Icon.INSERT_CHECKBOX\",\n};\nconst insertDropdown = {\n    name: _t(\"Dropdown list\"),\n    execute: (env) => {\n        const zones = env.model.getters.getSelectedZones();\n        const sheetId = env.model.getters.getActiveSheetId();\n        const ranges = zones.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));\n        const ruleID = env.model.uuidGenerator.uuidv4();\n        env.model.dispatch(\"ADD_DATA_VALIDATION_RULE\", {\n            ranges,\n            sheetId,\n            rule: {\n                id: ruleID,\n                criterion: {\n                    type: \"isValueInList\",\n                    values: [],\n                    displayStyle: \"arrow\",\n                },\n            },\n        });\n        const rule = env.model.getters.getDataValidationRule(sheetId, ruleID);\n        if (!rule) {\n            return;\n        }\n        env.openSidePanel(\"DataValidationEditor\", {\n            rule: localizeDataValidationRule(rule, env.model.getters.getLocale()),\n            onExit: () => {\n                env.openSidePanel(\"DataValidation\");\n            },\n        });\n    },\n    icon: \"o-spreadsheet-Icon.INSERT_DROPDOWN\",\n};\nconst insertSheet = {\n    name: _t(\"Insert sheet\"),\n    execute: (env) => {\n        const activeSheetId = env.model.getters.getActiveSheetId();\n        const position = env.model.getters.getSheetIds().indexOf(activeSheetId) + 1;\n        const sheetId = env.model.uuidGenerator.uuidv4();\n        env.model.dispatch(\"CREATE_SHEET\", { sheetId, position });\n        env.model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom: activeSheetId, sheetIdTo: sheetId });\n    },\n    icon: \"o-spreadsheet-Icon.INSERT_SHEET\",\n};\nfunction createFormulaFunctions(fnNames) {\n    return fnNames.sort().map((fnName, i) => {\n        return {\n            name: fnName,\n            sequence: i * 10,\n            execute: (env) => env.startCellEdition(`=${fnName}(`),\n        };\n    });\n}\nfunction getRowsNumber(env) {\n    const activeRows = env.model.getters.getActiveRows();\n    if (activeRows.size) {\n        return activeRows.size;\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        return zone.bottom - zone.top + 1;\n    }\n}\nfunction getColumnsNumber(env) {\n    const activeCols = env.model.getters.getActiveCols();\n    if (activeCols.size) {\n        return activeCols.size;\n    }\n    else {\n        const zone = env.model.getters.getSelectedZones()[0];\n        return zone.right - zone.left + 1;\n    }\n}\n\nconst pivotProperties = {\n    name: _t(\"Edit Pivot\"),\n    execute(env) {\n        const position = env.model.getters.getActivePosition();\n        const pivotId = env.model.getters.getPivotIdFromPosition(position);\n        env.openSidePanel(\"PivotSidePanel\", { pivotId });\n    },\n    isVisible: (env) => {\n        const position = env.model.getters.getActivePosition();\n        const pivotId = env.model.getters.getPivotIdFromPosition(position);\n        return (pivotId && env.model.getters.isExistingPivot(pivotId)) || false;\n    },\n    icon: \"o-spreadsheet-Icon.PIVOT\",\n};\nconst FIX_FORMULAS = {\n    name: _t(\"Convert to individual formulas\"),\n    execute(env) {\n        const position = env.model.getters.getActivePosition();\n        const cell = env.model.getters.getCorrespondingFormulaCell(position);\n        const pivotId = env.model.getters.getPivotIdFromPosition(position);\n        if (!cell || !pivotId) {\n            return;\n        }\n        const { sheetId, col, row } = env.model.getters.getCellPosition(cell.id);\n        const pivot = env.model.getters.getPivot(pivotId);\n        pivot.init();\n        if (!pivot.isValid()) {\n            return;\n        }\n        env.model.dispatch(\"SPLIT_PIVOT_FORMULA\", {\n            sheetId,\n            col,\n            row,\n            pivotId,\n        });\n    },\n    isVisible: (env) => {\n        const position = env.model.getters.getActivePosition();\n        const pivotId = env.model.getters.getPivotIdFromPosition(position);\n        if (!pivotId) {\n            return false;\n        }\n        const pivot = env.model.getters.getPivot(pivotId);\n        const cell = env.model.getters.getEvaluatedCell(position);\n        return (pivot.isValid() &&\n            env.model.getters.isSpillPivotFormula(position) &&\n            cell.type !== CellValueType.error);\n    },\n    icon: \"o-spreadsheet-Icon.PIVOT\",\n};\n\n//------------------------------------------------------------------------------\n// Context Menu Registry\n//------------------------------------------------------------------------------\nconst cellMenuRegistry = new MenuItemRegistry();\ncellMenuRegistry\n    .add(\"cut\", {\n    ...cut,\n    sequence: 10,\n})\n    .add(\"copy\", {\n    ...copy,\n    sequence: 20,\n})\n    .add(\"paste\", {\n    ...paste,\n    sequence: 30,\n})\n    .add(\"paste_special\", {\n    ...pasteSpecial,\n    sequence: 40,\n    separator: true,\n})\n    .addChild(\"paste_value_only\", [\"paste_special\"], {\n    ...pasteSpecialValue,\n    sequence: 10,\n})\n    .addChild(\"paste_format_only\", [\"paste_special\"], {\n    ...pasteSpecialFormat,\n    sequence: 20,\n})\n    .add(\"add_row_before\", {\n    ...cellInsertRowsBefore,\n    sequence: 70,\n})\n    .add(\"add_column_before\", {\n    ...cellInsertColsBefore,\n    sequence: 90,\n})\n    .add(\"insert_cell\", {\n    ...insertCell,\n    sequence: 100,\n    separator: true,\n})\n    .addChild(\"insert_cell_down\", [\"insert_cell\"], {\n    ...insertCellShiftDown,\n    name: _t(\"Shift down\"),\n    sequence: 10,\n})\n    .addChild(\"insert_cell_right\", [\"insert_cell\"], {\n    ...insertCellShiftRight,\n    name: _t(\"Shift right\"),\n    sequence: 20,\n})\n    .add(\"delete_row\", {\n    ...deleteRow,\n    sequence: 110,\n    icon: \"o-spreadsheet-Icon.TRASH\",\n})\n    .add(\"delete_column\", {\n    ...deleteCol,\n    sequence: 120,\n    icon: \"o-spreadsheet-Icon.TRASH\",\n})\n    .add(\"delete_cell\", {\n    ...deleteCells,\n    sequence: 130,\n    separator: true,\n    icon: \"o-spreadsheet-Icon.TRASH\",\n})\n    .addChild(\"delete_cell_up\", [\"delete_cell\"], {\n    ...deleteCellShiftUp,\n    name: _t(\"Shift up\"),\n    sequence: 10,\n    icon: \"o-spreadsheet-Icon.DELETE_CELL_SHIFT_UP\",\n})\n    .addChild(\"delete_cell_left\", [\"delete_cell\"], {\n    ...deleteCellShiftLeft,\n    name: _t(\"Shift left\"),\n    sequence: 20,\n    icon: \"o-spreadsheet-Icon.DELETE_CELL_SHIFT_LEFT\",\n})\n    .add(\"edit_table\", {\n    ...editTable,\n    isVisible: SELECTION_CONTAINS_SINGLE_TABLE,\n    sequence: 140,\n})\n    .add(\"delete_table\", {\n    ...deleteTable,\n    isVisible: SELECTION_CONTAINS_SINGLE_TABLE,\n    sequence: 145,\n    separator: true,\n})\n    .add(\"insert_link\", {\n    ...insertLink,\n    name: INSERT_LINK_NAME,\n    sequence: 150,\n    separator: true,\n})\n    .add(\"pivot_fix_formulas\", {\n    ...FIX_FORMULAS,\n    sequence: 155,\n})\n    .add(\"pivot_properties\", {\n    ...pivotProperties,\n    sequence: 160,\n    separator: true,\n});\n\nconst sortRange = {\n    name: _t(\"Sort range\"),\n    isVisible: IS_ONLY_ONE_RANGE,\n    icon: \"o-spreadsheet-Icon.SORT_RANGE\",\n};\nconst sortAscending = {\n    name: _t(\"Ascending (A \u27f6 Z)\"),\n    execute: (env) => {\n        const { anchor, zones } = env.model.getters.getSelection();\n        const sheetId = env.model.getters.getActiveSheetId();\n        interactiveSortSelection(env, sheetId, anchor.cell, zones[0], \"ascending\");\n    },\n    icon: \"o-spreadsheet-Icon.SORT_ASCENDING\",\n};\nconst dataCleanup = {\n    name: _t(\"Data cleanup\"),\n    icon: \"o-spreadsheet-Icon.DATA_CLEANUP\",\n};\nconst removeDuplicates = {\n    name: _t(\"Remove duplicates\"),\n    execute: (env) => {\n        if (getZoneArea(env.model.getters.getSelectedZone()) === 1) {\n            env.model.selection.selectTableAroundSelection();\n        }\n        env.openSidePanel(\"RemoveDuplicates\", {});\n    },\n};\nconst trimWhitespace = {\n    name: _t(\"Trim whitespace\"),\n    execute: (env) => {\n        env.model.dispatch(\"TRIM_WHITESPACE\");\n    },\n};\nconst sortDescending = {\n    name: _t(\"Descending (Z \u27f6 A)\"),\n    execute: (env) => {\n        const { anchor, zones } = env.model.getters.getSelection();\n        const sheetId = env.model.getters.getActiveSheetId();\n        interactiveSortSelection(env, sheetId, anchor.cell, zones[0], \"descending\");\n    },\n    icon: \"o-spreadsheet-Icon.SORT_DESCENDING\",\n};\nconst createRemoveFilter = {\n    ...CREATE_OR_REMOVE_FILTER_ACTION,\n};\nconst createRemoveFilterTool = {\n    ...CREATE_OR_REMOVE_FILTER_ACTION,\n    isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env),\n};\nconst splitToColumns = {\n    name: _t(\"Split text to columns\"),\n    sequence: 1,\n    execute: (env) => env.openSidePanel(\"SplitToColumns\", {}),\n    isEnabled: (env) => env.model.getters.isSingleColSelected(),\n    icon: \"o-spreadsheet-Icon.SPLIT_TEXT\",\n};\nconst reinsertDynamicPivotMenu = {\n    id: \"reinsert_dynamic_pivot\",\n    name: _t(\"Re-insert dynamic pivot\"),\n    sequence: 1020,\n    icon: \"o-spreadsheet-Icon.INSERT_PIVOT\",\n    children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],\n    isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid()),\n};\nconst reinsertStaticPivotMenu = {\n    id: \"reinsert_static_pivot\",\n    name: _t(\"Re-insert static pivot\"),\n    sequence: 1020,\n    icon: \"o-spreadsheet-Icon.INSERT_PIVOT\",\n    children: [REINSERT_STATIC_PIVOT_CHILDREN],\n    isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid()),\n};\n\nvar ACTION_DATA = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    createRemoveFilter: createRemoveFilter,\n    createRemoveFilterTool: createRemoveFilterTool,\n    dataCleanup: dataCleanup,\n    reinsertDynamicPivotMenu: reinsertDynamicPivotMenu,\n    reinsertStaticPivotMenu: reinsertStaticPivotMenu,\n    removeDuplicates: removeDuplicates,\n    sortAscending: sortAscending,\n    sortDescending: sortDescending,\n    sortRange: sortRange,\n    splitToColumns: splitToColumns,\n    trimWhitespace: trimWhitespace\n});\n\n/**\n * Create a format action specification for a given format.\n * The format can be dynamically computed from the environment.\n */\nfunction createFormatActionSpec({ name, format, descriptionValue, }) {\n    const formatCallback = typeof format === \"function\" ? format : () => format;\n    return {\n        name,\n        description: (env) => formatValue(descriptionValue, {\n            format: formatCallback(env),\n            locale: env.model.getters.getLocale(),\n        }),\n        execute: (env) => setFormatter(env, formatCallback(env)),\n        isActive: (env) => isFormatSelected(env, formatCallback(env)),\n        format,\n    };\n}\nconst formatNumberAutomatic = {\n    name: _t(\"Automatic\"),\n    execute: (env) => setFormatter(env, \"\"),\n    isActive: (env) => isAutomaticFormatSelected(env),\n};\nconst formatNumberPlainText = {\n    name: _t(\"Plain text\"),\n    execute: (env) => setFormatter(env, \"@\"),\n    isActive: (env) => isFormatSelected(env, \"@\"),\n};\nconst formatNumberNumber = createFormatActionSpec({\n    name: _t(\"Number\"),\n    descriptionValue: 1000.12,\n    format: \"#,##0.00\",\n});\nconst formatPercent = {\n    name: _t(\"Format as percent\"),\n    execute: FORMAT_PERCENT_ACTION,\n    icon: \"o-spreadsheet-Icon.PERCENT\",\n};\nconst formatNumberPercent = createFormatActionSpec({\n    name: _t(\"Percent\"),\n    descriptionValue: 0.1012,\n    format: \"0.00%\",\n});\nconst formatNumberCurrency = createFormatActionSpec({\n    name: _t(\"Currency\"),\n    descriptionValue: 1000.12,\n    format: (env) => createCurrencyFormat(env.model.config.defaultCurrency || DEFAULT_CURRENCY),\n});\nconst formatNumberCurrencyRounded = {\n    ...createFormatActionSpec({\n        name: _t(\"Currency rounded\"),\n        descriptionValue: 1000,\n        format: (env) => roundFormat(createCurrencyFormat(env.model.config.defaultCurrency || DEFAULT_CURRENCY)),\n    }),\n    isVisible: (env) => {\n        const currencyFormat = createCurrencyFormat(env.model.config.defaultCurrency || DEFAULT_CURRENCY);\n        const roundedFormat = roundFormat(currencyFormat);\n        return currencyFormat !== roundedFormat;\n    },\n};\nconst formatNumberAccounting = createFormatActionSpec({\n    name: _t(\"Accounting\"),\n    descriptionValue: -1000.12,\n    format: (env) => createAccountingFormat(env.model.config.defaultCurrency || DEFAULT_CURRENCY),\n});\nconst EXAMPLE_DATE = parseLiteral(\"2023/09/26 10:43:00 PM\", DEFAULT_LOCALE);\nconst formatCustomCurrency = {\n    name: _t(\"Custom currency\"),\n    isVisible: (env) => env.loadCurrencies !== undefined,\n    execute: (env) => env.openSidePanel(\"CustomCurrency\", {}),\n};\nconst formatNumberDate = createFormatActionSpec({\n    name: _t(\"Date\"),\n    descriptionValue: EXAMPLE_DATE,\n    format: (env) => env.model.getters.getLocale().dateFormat,\n});\nconst formatNumberTime = createFormatActionSpec({\n    name: _t(\"Time\"),\n    descriptionValue: EXAMPLE_DATE,\n    format: (env) => env.model.getters.getLocale().timeFormat,\n});\nconst formatNumberDateTime = createFormatActionSpec({\n    name: _t(\"Date time\"),\n    descriptionValue: EXAMPLE_DATE,\n    format: (env) => {\n        const locale = env.model.getters.getLocale();\n        return getDateTimeFormat(locale);\n    },\n});\nconst formatNumberDuration = createFormatActionSpec({\n    name: _t(\"Duration\"),\n    descriptionValue: \"27:51:38\",\n    format: \"hhhh:mm:ss\",\n});\nconst formatNumberQuarter = createFormatActionSpec({\n    name: _t(\"Quarter\"),\n    descriptionValue: EXAMPLE_DATE,\n    format: \"qq yyyy\",\n});\nconst formatNumberFullQuarter = createFormatActionSpec({\n    name: _t(\"Full quarter\"),\n    descriptionValue: EXAMPLE_DATE,\n    format: \"qqqq yyyy\",\n});\nconst moreFormats = {\n    name: _t(\"More date formats\"),\n    execute: (env) => env.openSidePanel(\"MoreFormats\", {}),\n};\nconst formatNumberFullDateTime = createFormatActionSpec({\n    name: _t(\"Full date time\"),\n    format: \"dddd d mmmm yyyy hh:mm:ss a\",\n    descriptionValue: EXAMPLE_DATE,\n});\nconst formatNumberFullWeekDayAndMonth = createFormatActionSpec({\n    name: _t(\"Full week day and month\"),\n    format: \"dddd d mmmm yyyy\",\n    descriptionValue: EXAMPLE_DATE,\n});\nconst formatNumberDayAndFullMonth = createFormatActionSpec({\n    name: _t(\"Day and full month\"),\n    format: \"d mmmm yyyy\",\n    descriptionValue: EXAMPLE_DATE,\n});\nconst formatNumberShortWeekDay = createFormatActionSpec({\n    name: _t(\"Short week day\"),\n    format: \"ddd d mmm yyyy\",\n    descriptionValue: EXAMPLE_DATE,\n});\nconst formatNumberDayAndShortMonth = createFormatActionSpec({\n    name: _t(\"Day and short month\"),\n    format: \"d mmm yyyy\",\n    descriptionValue: EXAMPLE_DATE,\n});\nconst formatNumberFullMonth = createFormatActionSpec({\n    name: _t(\"Full month\"),\n    format: \"mmmm yyyy\",\n    descriptionValue: EXAMPLE_DATE,\n});\nconst formatNumberShortMonth = createFormatActionSpec({\n    name: _t(\"Short month\"),\n    format: \"mmm yyyy\",\n    descriptionValue: EXAMPLE_DATE,\n});\nconst incraseDecimalPlaces = {\n    name: _t(\"Increase decimal places\"),\n    icon: \"o-spreadsheet-Icon.INCREASE_DECIMAL\",\n    execute: (env) => env.model.dispatch(\"SET_DECIMAL\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        target: env.model.getters.getSelectedZones(),\n        step: 1,\n    }),\n};\nconst decraseDecimalPlaces = {\n    name: _t(\"Decrease decimal places\"),\n    icon: \"o-spreadsheet-Icon.DECRASE_DECIMAL\",\n    execute: (env) => env.model.dispatch(\"SET_DECIMAL\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        target: env.model.getters.getSelectedZones(),\n        step: -1,\n    }),\n};\nconst formatBold = {\n    name: _t(\"Bold\"),\n    description: \"Ctrl+B\",\n    execute: (env) => setStyle(env, { bold: !env.model.getters.getCurrentStyle().bold }),\n    icon: \"o-spreadsheet-Icon.BOLD\",\n    isActive: (env) => !!env.model.getters.getCurrentStyle().bold,\n};\nconst formatItalic = {\n    name: _t(\"Italic\"),\n    description: \"Ctrl+I\",\n    execute: (env) => setStyle(env, { italic: !env.model.getters.getCurrentStyle().italic }),\n    icon: \"o-spreadsheet-Icon.ITALIC\",\n    isActive: (env) => !!env.model.getters.getCurrentStyle().italic,\n};\nconst formatUnderline = {\n    name: _t(\"Underline\"),\n    description: \"Ctrl+U\",\n    execute: (env) => setStyle(env, { underline: !env.model.getters.getCurrentStyle().underline }),\n    icon: \"o-spreadsheet-Icon.UNDERLINE\",\n    isActive: (env) => !!env.model.getters.getCurrentStyle().underline,\n};\nconst formatStrikethrough = {\n    name: _t(\"Strikethrough\"),\n    execute: (env) => setStyle(env, { strikethrough: !env.model.getters.getCurrentStyle().strikethrough }),\n    icon: \"o-spreadsheet-Icon.STRIKE\",\n    isActive: (env) => !!env.model.getters.getCurrentStyle().strikethrough,\n};\nconst formatFontSize = {\n    name: _t(\"Font size\"),\n    children: fontSizeMenuBuilder(),\n    icon: \"o-spreadsheet-Icon.FONT_SIZE\",\n};\nconst formatAlignment = {\n    name: _t(\"Alignment\"),\n    icon: \"o-spreadsheet-Icon.ALIGN_LEFT\",\n};\nconst formatAlignmentHorizontal = {\n    name: _t(\"Horizontal align\"),\n    icon: (env) => getHorizontalAlignmentIcon(env),\n};\nconst formatAlignmentLeft = {\n    name: _t(\"Left\"),\n    description: \"Ctrl+Shift+L\",\n    execute: (env) => setStyle(env, { align: \"left\" }),\n    isActive: (env) => getHorizontalAlign(env) === \"left\",\n    icon: \"o-spreadsheet-Icon.ALIGN_LEFT\",\n};\nconst formatAlignmentCenter = {\n    name: _t(\"Center\"),\n    description: \"Ctrl+Shift+E\",\n    execute: (env) => setStyle(env, { align: \"center\" }),\n    isActive: (env) => getHorizontalAlign(env) === \"center\",\n    icon: \"o-spreadsheet-Icon.ALIGN_CENTER\",\n};\nconst formatAlignmentRight = {\n    name: _t(\"Right\"),\n    description: \"Ctrl+Shift+R\",\n    execute: (env) => setStyle(env, { align: \"right\" }),\n    isActive: (env) => getHorizontalAlign(env) === \"right\",\n    icon: \"o-spreadsheet-Icon.ALIGN_RIGHT\",\n};\nconst formatAlignmentVertical = {\n    name: _t(\"Vertical align\"),\n    icon: (env) => getVerticalAlignmentIcon(env),\n};\nconst formatAlignmentTop = {\n    name: _t(\"Top\"),\n    execute: (env) => setStyle(env, { verticalAlign: \"top\" }),\n    isActive: (env) => getVerticalAlign(env) === \"top\",\n    icon: \"o-spreadsheet-Icon.ALIGN_TOP\",\n};\nconst formatAlignmentMiddle = {\n    name: _t(\"Middle\"),\n    execute: (env) => setStyle(env, { verticalAlign: \"middle\" }),\n    isActive: (env) => getVerticalAlign(env) === \"middle\",\n    icon: \"o-spreadsheet-Icon.ALIGN_MIDDLE\",\n};\nconst formatAlignmentBottom = {\n    name: _t(\"Bottom\"),\n    execute: (env) => setStyle(env, { verticalAlign: \"bottom\" }),\n    isActive: (env) => getVerticalAlign(env) === \"bottom\",\n    icon: \"o-spreadsheet-Icon.ALIGN_BOTTOM\",\n};\nconst formatWrappingIcon = {\n    name: _t(\"Wrapping\"),\n    icon: \"o-spreadsheet-Icon.WRAPPING_OVERFLOW\",\n};\nconst formatWrapping = {\n    name: _t(\"Wrapping\"),\n    icon: (env) => getWrapModeIcon(env),\n};\nconst formatWrappingOverflow = {\n    name: _t(\"Overflow\"),\n    execute: (env) => setStyle(env, { wrapping: \"overflow\" }),\n    isActive: (env) => getWrappingMode(env) === \"overflow\",\n    icon: \"o-spreadsheet-Icon.WRAPPING_OVERFLOW\",\n};\nconst formatWrappingWrap = {\n    name: _t(\"Wrap\"),\n    execute: (env) => setStyle(env, { wrapping: \"wrap\" }),\n    isActive: (env) => getWrappingMode(env) === \"wrap\",\n    icon: \"o-spreadsheet-Icon.WRAPPING_WRAP\",\n};\nconst formatWrappingClip = {\n    name: _t(\"Clip\"),\n    execute: (env) => setStyle(env, { wrapping: \"clip\" }),\n    isActive: (env) => getWrappingMode(env) === \"clip\",\n    icon: \"o-spreadsheet-Icon.WRAPPING_CLIP\",\n};\nconst textColor = {\n    name: _t(\"Text Color\"),\n    icon: \"o-spreadsheet-Icon.TEXT_COLOR\",\n};\nconst fillColor = {\n    name: _t(\"Fill Color\"),\n    icon: \"o-spreadsheet-Icon.FILL_COLOR\",\n};\nconst formatCF = {\n    name: _t(\"Conditional formatting\"),\n    execute: OPEN_CF_SIDEPANEL_ACTION,\n    icon: \"o-spreadsheet-Icon.CONDITIONAL_FORMAT\",\n};\nconst clearFormat = {\n    name: _t(\"Clear formatting\"),\n    description: \"Ctrl+<\",\n    execute: (env) => env.model.dispatch(\"CLEAR_FORMATTING\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n        target: env.model.getters.getSelectedZones(),\n    }),\n    icon: \"o-spreadsheet-Icon.CLEAR_FORMAT\",\n};\nfunction fontSizeMenuBuilder() {\n    return FONT_SIZES.map((fs) => {\n        return {\n            name: fs.toString(),\n            sequence: fs,\n            id: `font_size_${fs}`,\n            execute: (env) => setStyle(env, { fontSize: fs }),\n            isActive: (env) => isFontSizeSelected(env, fs),\n        };\n    });\n}\nfunction isAutomaticFormatSelected(env) {\n    const activeCell = env.model.getters.getCell(env.model.getters.getActivePosition());\n    return !activeCell || !activeCell.format;\n}\nfunction isFormatSelected(env, format) {\n    const activeCell = env.model.getters.getCell(env.model.getters.getActivePosition());\n    return activeCell?.format === format;\n}\nfunction isFontSizeSelected(env, fontSize) {\n    const currentFontSize = env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;\n    return currentFontSize === fontSize;\n}\nfunction getHorizontalAlign(env) {\n    const style = env.model.getters.getCurrentStyle();\n    if (style.align) {\n        return style.align;\n    }\n    const cell = env.model.getters.getActiveCell();\n    return cell.defaultAlign;\n}\nfunction getVerticalAlign(env) {\n    const style = env.model.getters.getCurrentStyle();\n    if (style.verticalAlign) {\n        return style.verticalAlign;\n    }\n    return DEFAULT_VERTICAL_ALIGN;\n}\nfunction getWrappingMode(env) {\n    const style = env.model.getters.getCurrentStyle();\n    if (style.wrapping) {\n        return style.wrapping;\n    }\n    return DEFAULT_WRAPPING_MODE;\n}\nfunction getHorizontalAlignmentIcon(env) {\n    const horizontalAlign = getHorizontalAlign(env);\n    switch (horizontalAlign) {\n        case \"right\":\n            return \"o-spreadsheet-Icon.ALIGN_RIGHT\";\n        case \"center\":\n            return \"o-spreadsheet-Icon.ALIGN_CENTER\";\n        default:\n            return \"o-spreadsheet-Icon.ALIGN_LEFT\";\n    }\n}\nfunction getVerticalAlignmentIcon(env) {\n    const verticalAlign = getVerticalAlign(env);\n    switch (verticalAlign) {\n        case \"top\":\n            return \"o-spreadsheet-Icon.ALIGN_TOP\";\n        case \"middle\":\n            return \"o-spreadsheet-Icon.ALIGN_MIDDLE\";\n        default:\n            return \"o-spreadsheet-Icon.ALIGN_BOTTOM\";\n    }\n}\nfunction getWrapModeIcon(env) {\n    const wrapMode = getWrappingMode(env);\n    switch (wrapMode) {\n        case \"wrap\":\n            return \"o-spreadsheet-Icon.WRAPPING_WRAP\";\n        case \"clip\":\n            return \"o-spreadsheet-Icon.WRAPPING_CLIP\";\n        default:\n            return \"o-spreadsheet-Icon.WRAPPING_OVERFLOW\";\n    }\n}\n\nvar ACTION_FORMAT = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    EXAMPLE_DATE: EXAMPLE_DATE,\n    clearFormat: clearFormat,\n    createFormatActionSpec: createFormatActionSpec,\n    decraseDecimalPlaces: decraseDecimalPlaces,\n    fillColor: fillColor,\n    formatAlignment: formatAlignment,\n    formatAlignmentBottom: formatAlignmentBottom,\n    formatAlignmentCenter: formatAlignmentCenter,\n    formatAlignmentHorizontal: formatAlignmentHorizontal,\n    formatAlignmentLeft: formatAlignmentLeft,\n    formatAlignmentMiddle: formatAlignmentMiddle,\n    formatAlignmentRight: formatAlignmentRight,\n    formatAlignmentTop: formatAlignmentTop,\n    formatAlignmentVertical: formatAlignmentVertical,\n    formatBold: formatBold,\n    formatCF: formatCF,\n    formatCustomCurrency: formatCustomCurrency,\n    formatFontSize: formatFontSize,\n    formatItalic: formatItalic,\n    formatNumberAccounting: formatNumberAccounting,\n    formatNumberAutomatic: formatNumberAutomatic,\n    formatNumberCurrency: formatNumberCurrency,\n    formatNumberCurrencyRounded: formatNumberCurrencyRounded,\n    formatNumberDate: formatNumberDate,\n    formatNumberDateTime: formatNumberDateTime,\n    formatNumberDayAndFullMonth: formatNumberDayAndFullMonth,\n    formatNumberDayAndShortMonth: formatNumberDayAndShortMonth,\n    formatNumberDuration: formatNumberDuration,\n    formatNumberFullDateTime: formatNumberFullDateTime,\n    formatNumberFullMonth: formatNumberFullMonth,\n    formatNumberFullQuarter: formatNumberFullQuarter,\n    formatNumberFullWeekDayAndMonth: formatNumberFullWeekDayAndMonth,\n    formatNumberNumber: formatNumberNumber,\n    formatNumberPercent: formatNumberPercent,\n    formatNumberPlainText: formatNumberPlainText,\n    formatNumberQuarter: formatNumberQuarter,\n    formatNumberShortMonth: formatNumberShortMonth,\n    formatNumberShortWeekDay: formatNumberShortWeekDay,\n    formatNumberTime: formatNumberTime,\n    formatPercent: formatPercent,\n    formatStrikethrough: formatStrikethrough,\n    formatUnderline: formatUnderline,\n    formatWrapping: formatWrapping,\n    formatWrappingClip: formatWrappingClip,\n    formatWrappingIcon: formatWrappingIcon,\n    formatWrappingOverflow: formatWrappingOverflow,\n    formatWrappingWrap: formatWrappingWrap,\n    incraseDecimalPlaces: incraseDecimalPlaces,\n    moreFormats: moreFormats,\n    textColor: textColor\n});\n\nfunction interactiveFreezeColumnsRows(env, dimension, base) {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const cmd = dimension === \"COL\" ? \"FREEZE_COLUMNS\" : \"FREEZE_ROWS\";\n    const result = env.model.dispatch(cmd, { sheetId, quantity: base });\n    if (result.isCancelledBecause(\"MergeOverlap\" /* CommandResult.MergeOverlap */)) {\n        env.raiseError(MergeErrorMessage);\n    }\n}\n\nconst hideCols = {\n    name: HIDE_COLUMNS_NAME,\n    execute: (env) => {\n        const columns = env.model.getters.getElementsFromSelection(\"COL\");\n        env.model.dispatch(\"HIDE_COLUMNS_ROWS\", {\n            sheetId: env.model.getters.getActiveSheetId(),\n            dimension: \"COL\",\n            elements: columns,\n        });\n    },\n    isVisible: NOT_ALL_VISIBLE_COLS_SELECTED,\n    icon: \"o-spreadsheet-Icon.HIDE_COL\",\n};\nconst unhideCols = {\n    name: _t(\"Unhide columns\"),\n    execute: (env) => {\n        const columns = env.model.getters.getElementsFromSelection(\"COL\");\n        env.model.dispatch(\"UNHIDE_COLUMNS_ROWS\", {\n            sheetId: env.model.getters.getActiveSheetId(),\n            dimension: \"COL\",\n            elements: columns,\n        });\n    },\n    isVisible: (env) => {\n        const hiddenCols = env.model.getters\n            .getHiddenColsGroups(env.model.getters.getActiveSheetId())\n            .flat();\n        const currentCols = env.model.getters.getElementsFromSelection(\"COL\");\n        return currentCols.some((col) => hiddenCols.includes(col));\n    },\n    icon: \"o-spreadsheet-Icon.UNHIDE_COL\",\n};\nconst unhideAllCols = {\n    name: _t(\"Unhide all columns\"),\n    execute: (env) => {\n        const sheetId = env.model.getters.getActiveSheetId();\n        env.model.dispatch(\"UNHIDE_COLUMNS_ROWS\", {\n            sheetId,\n            dimension: \"COL\",\n            elements: Array.from(Array(env.model.getters.getNumberCols(sheetId)).keys()),\n        });\n    },\n    isVisible: (env) => env.model.getters.getHiddenColsGroups(env.model.getters.getActiveSheetId()).length > 0,\n    icon: \"o-spreadsheet-Icon.UNHIDE_COL\",\n};\nconst hideRows = {\n    name: HIDE_ROWS_NAME,\n    execute: (env) => {\n        const rows = env.model.getters.getElementsFromSelection(\"ROW\");\n        env.model.dispatch(\"HIDE_COLUMNS_ROWS\", {\n            sheetId: env.model.getters.getActiveSheetId(),\n            dimension: \"ROW\",\n            elements: rows,\n        });\n    },\n    isVisible: NOT_ALL_VISIBLE_ROWS_SELECTED,\n    icon: \"o-spreadsheet-Icon.HIDE_ROW\",\n};\nconst unhideRows = {\n    name: _t(\"Unhide rows\"),\n    execute: (env) => {\n        const columns = env.model.getters.getElementsFromSelection(\"ROW\");\n        env.model.dispatch(\"UNHIDE_COLUMNS_ROWS\", {\n            sheetId: env.model.getters.getActiveSheetId(),\n            dimension: \"ROW\",\n            elements: columns,\n        });\n    },\n    isVisible: (env) => {\n        const hiddenRows = env.model.getters\n            .getHiddenRowsGroups(env.model.getters.getActiveSheetId())\n            .flat();\n        const currentRows = env.model.getters.getElementsFromSelection(\"ROW\");\n        return currentRows.some((col) => hiddenRows.includes(col));\n    },\n    icon: \"o-spreadsheet-Icon.UNHIDE_ROW\",\n};\nconst unhideAllRows = {\n    name: _t(\"Unhide all rows\"),\n    execute: (env) => {\n        const sheetId = env.model.getters.getActiveSheetId();\n        env.model.dispatch(\"UNHIDE_COLUMNS_ROWS\", {\n            sheetId,\n            dimension: \"ROW\",\n            elements: Array.from(Array(env.model.getters.getNumberRows(sheetId)).keys()),\n        });\n    },\n    isVisible: (env) => env.model.getters.getHiddenRowsGroups(env.model.getters.getActiveSheetId()).length > 0,\n    icon: \"o-spreadsheet-Icon.UNHIDE_ROW\",\n};\nconst unFreezePane = {\n    name: _t(\"Unfreeze\"),\n    isVisible: (env) => {\n        const { xSplit, ySplit } = env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId());\n        return xSplit + ySplit > 0;\n    },\n    execute: (env) => env.model.dispatch(\"UNFREEZE_COLUMNS_ROWS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n    }),\n    icon: \"o-spreadsheet-Icon.UNFREEZE\",\n};\nconst freezePane = {\n    name: _t(\"Freeze\"),\n    icon: \"o-spreadsheet-Icon.FREEZE\",\n};\nconst unFreezeRows = {\n    name: _t(\"No rows\"),\n    execute: (env) => env.model.dispatch(\"UNFREEZE_ROWS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n    }),\n    isReadonlyAllowed: true,\n    isVisible: (env) => !!env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId()).ySplit,\n};\nconst freezeFirstRow = {\n    name: _t(\"1 row\"),\n    execute: (env) => interactiveFreezeColumnsRows(env, \"ROW\", 1),\n    isReadonlyAllowed: true,\n};\nconst freezeSecondRow = {\n    name: _t(\"2 rows\"),\n    execute: (env) => interactiveFreezeColumnsRows(env, \"ROW\", 2),\n    isReadonlyAllowed: true,\n};\nconst freezeCurrentRow = {\n    name: _t(\"Up to current row\"),\n    execute: (env) => {\n        const { bottom } = env.model.getters.getSelectedZone();\n        interactiveFreezeColumnsRows(env, \"ROW\", bottom + 1);\n    },\n    isReadonlyAllowed: true,\n};\nconst unFreezeCols = {\n    name: _t(\"No columns\"),\n    execute: (env) => env.model.dispatch(\"UNFREEZE_COLUMNS\", {\n        sheetId: env.model.getters.getActiveSheetId(),\n    }),\n    isReadonlyAllowed: true,\n    isVisible: (env) => !!env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId()).xSplit,\n};\nconst freezeFirstCol = {\n    name: _t(\"1 column\"),\n    execute: (env) => interactiveFreezeColumnsRows(env, \"COL\", 1),\n    isReadonlyAllowed: true,\n};\nconst freezeSecondCol = {\n    name: _t(\"2 columns\"),\n    execute: (env) => interactiveFreezeColumnsRows(env, \"COL\", 2),\n    isReadonlyAllowed: true,\n};\nconst freezeCurrentCol = {\n    name: _t(\"Up to current column\"),\n    execute: (env) => {\n        const { right } = env.model.getters.getSelectedZone();\n        interactiveFreezeColumnsRows(env, \"COL\", right + 1);\n    },\n    isReadonlyAllowed: true,\n};\nconst viewGridlines = {\n    name: _t(\"Gridlines\"),\n    execute: (env) => {\n        const sheetId = env.model.getters.getActiveSheetId();\n        env.model.dispatch(\"SET_GRID_LINES_VISIBILITY\", {\n            sheetId,\n            areGridLinesVisible: !env.model.getters.getGridLinesVisibility(sheetId),\n        });\n    },\n    isActive: (env) => {\n        const sheetId = env.model.getters.getActiveSheetId();\n        return env.model.getters.getGridLinesVisibility(sheetId);\n    },\n};\nconst viewFormulas = {\n    name: _t(\"Formulas\"),\n    isActive: (env) => env.model.getters.shouldShowFormulas(),\n    execute: (env) => env.model.dispatch(\"SET_FORMULA_VISIBILITY\", { show: !env.model.getters.shouldShowFormulas() }),\n    isReadonlyAllowed: true,\n};\nconst groupColumns = {\n    name: (env) => {\n        const selection = env.model.getters.getSelectedZone();\n        if (selection.left === selection.right) {\n            return _t(\"Group column %s\", numberToLetters(selection.left));\n        }\n        return _t(\"Group columns %s - %s\", numberToLetters(selection.left), numberToLetters(selection.right));\n    },\n    execute: (env) => groupHeadersAction(env, \"COL\"),\n    isVisible: (env) => {\n        const sheetId = env.model.getters.getActiveSheetId();\n        const selection = env.model.getters.getSelectedZone();\n        const groups = env.model.getters.getHeaderGroupsInZone(sheetId, \"COL\", selection);\n        return (IS_ONLY_ONE_RANGE(env) &&\n            !groups.some((group) => group.start === selection.left && group.end === selection.right));\n    },\n    icon: \"o-spreadsheet-Icon.GROUP_COLUMNS\",\n};\nconst groupRows = {\n    name: (env) => {\n        const selection = env.model.getters.getSelectedZone();\n        if (selection.top === selection.bottom) {\n            return _t(\"Group row %s\", String(selection.top + 1));\n        }\n        return _t(\"Group rows %s - %s\", String(selection.top + 1), String(selection.bottom + 1));\n    },\n    execute: (env) => groupHeadersAction(env, \"ROW\"),\n    isVisible: (env) => {\n        const sheetId = env.model.getters.getActiveSheetId();\n        const selection = env.model.getters.getSelectedZone();\n        const groups = env.model.getters.getHeaderGroupsInZone(sheetId, \"ROW\", selection);\n        return (IS_ONLY_ONE_RANGE(env) &&\n            !groups.some((group) => group.start === selection.top && group.end === selection.bottom));\n    },\n    icon: \"o-spreadsheet-Icon.GROUP_ROWS\",\n};\nconst ungroupColumns = {\n    name: (env) => {\n        const selection = env.model.getters.getSelectedZone();\n        if (selection.left === selection.right) {\n            return _t(\"Ungroup column %s\", numberToLetters(selection.left));\n        }\n        return _t(\"Ungroup columns %s - %s\", numberToLetters(selection.left), numberToLetters(selection.right));\n    },\n    execute: (env) => ungroupHeaders(env, \"COL\"),\n    icon: \"o-spreadsheet-Icon.UNGROUP_COLUMNS\",\n};\nconst ungroupRows = {\n    name: (env) => {\n        const selection = env.model.getters.getSelectedZone();\n        if (selection.top === selection.bottom) {\n            return _t(\"Ungroup row %s\", String(selection.top + 1));\n        }\n        return _t(\"Ungroup rows %s - %s\", String(selection.top + 1), String(selection.bottom + 1));\n    },\n    execute: (env) => ungroupHeaders(env, \"ROW\"),\n    icon: \"o-spreadsheet-Icon.UNGROUP_ROWS\",\n};\nfunction groupHeadersAction(env, dim) {\n    const selection = env.model.getters.getSelectedZone();\n    const sheetId = env.model.getters.getActiveSheetId();\n    env.model.dispatch(\"GROUP_HEADERS\", {\n        sheetId,\n        dimension: dim,\n        start: dim === \"COL\" ? selection.left : selection.top,\n        end: dim === \"COL\" ? selection.right : selection.bottom,\n    });\n}\nfunction ungroupHeaders(env, dim) {\n    const selection = env.model.getters.getSelectedZone();\n    const sheetId = env.model.getters.getActiveSheetId();\n    env.model.dispatch(\"UNGROUP_HEADERS\", {\n        sheetId,\n        dimension: dim,\n        start: dim === \"COL\" ? selection.left : selection.top,\n        end: dim === \"COL\" ? selection.right : selection.bottom,\n    });\n}\nfunction canUngroupHeaders(env, dimension) {\n    const sheetId = env.model.getters.getActiveSheetId();\n    const selection = env.model.getters.getSelectedZones();\n    return (selection.length === 1 &&\n        env.model.getters.getHeaderGroupsInZone(sheetId, dimension, selection[0]).length > 0);\n}\n\nconst colMenuRegistry = new MenuItemRegistry();\ncolMenuRegistry\n    .add(\"cut\", {\n    ...cut,\n    sequence: 10,\n})\n    .add(\"copy\", {\n    ...copy,\n    sequence: 20,\n})\n    .add(\"paste\", {\n    ...paste,\n    sequence: 30,\n})\n    .add(\"paste_special\", {\n    ...pasteSpecial,\n    sequence: 40,\n    separator: true,\n})\n    .addChild(\"paste_value_only\", [\"paste_special\"], {\n    ...pasteSpecialValue,\n    sequence: 10,\n})\n    .addChild(\"paste_format_only\", [\"paste_special\"], {\n    ...pasteSpecialFormat,\n    sequence: 20,\n})\n    .add(\"sort_columns\", {\n    ...sortRange,\n    name: (env) => env.model.getters.getActiveCols().size > 1 ? _t(\"Sort columns\") : _t(\"Sort column\"),\n    sequence: 50,\n    separator: true,\n})\n    .addChild(\"sort_ascending\", [\"sort_columns\"], {\n    ...sortAscending,\n    sequence: 10,\n})\n    .addChild(\"sort_descending\", [\"sort_columns\"], {\n    ...sortDescending,\n    sequence: 20,\n})\n    .add(\"add_column_before\", {\n    ...colInsertColsBefore,\n    sequence: 70,\n})\n    .add(\"add_column_after\", {\n    ...colInsertColsAfter,\n    sequence: 80,\n})\n    .add(\"delete_column\", {\n    ...deleteCols,\n    sequence: 90,\n    icon: \"o-spreadsheet-Icon.TRASH\",\n})\n    .add(\"clear_column\", {\n    ...clearCols,\n    sequence: 100,\n    icon: \"o-spreadsheet-Icon.CLEAR\",\n})\n    .add(\"hide_columns\", {\n    ...hideCols,\n    sequence: 105,\n    separator: true,\n})\n    .add(\"unhide_columns\", {\n    ...unhideCols,\n    sequence: 106,\n    separator: true,\n})\n    .add(\"conditional_formatting\", {\n    ...formatCF,\n    sequence: 110,\n    separator: true,\n})\n    .add(\"edit_table\", {\n    ...editTable,\n    isVisible: SELECTION_CONTAINS_SINGLE_TABLE,\n    sequence: 120,\n})\n    .add(\"delete_table\", {\n    ...deleteTable,\n    isVisible: SELECTION_CONTAINS_SINGLE_TABLE,\n    sequence: 125,\n    separator: true,\n})\n    .add(\"group_columns\", {\n    sequence: 150,\n    ...groupColumns,\n})\n    .add(\"ungroup_columns\", {\n    sequence: 155,\n    ...ungroupColumns,\n    isVisible: (env) => canUngroupHeaders(env, \"COL\"),\n});\n\nconst numberFormatMenuRegistry = new Registry();\nnumberFormatMenuRegistry\n    .add(\"format_number_automatic\", {\n    ...formatNumberAutomatic,\n    id: \"format_number_automatic\",\n    sequence: 10,\n})\n    .add(\"format_number_plain_text\", {\n    ...formatNumberPlainText,\n    id: \"format_number_plain_text\",\n    sequence: 15,\n    separator: true,\n})\n    .add(\"format_number_number\", {\n    ...formatNumberNumber,\n    id: \"format_number_number\",\n    sequence: 20,\n})\n    .add(\"format_number_percent\", {\n    ...formatNumberPercent,\n    id: \"format_number_percent\",\n    sequence: 30,\n    separator: true,\n})\n    .add(\"format_number_currency\", {\n    ...formatNumberCurrency,\n    id: \"format_number_currency\",\n    sequence: 40,\n})\n    .add(\"format_number_accounting\", {\n    ...formatNumberAccounting,\n    id: \"format_number_accounting\",\n    sequence: 45,\n})\n    .add(\"format_number_currency_rounded\", {\n    ...formatNumberCurrencyRounded,\n    id: \"format_number_currency_rounded\",\n    sequence: 50,\n})\n    .add(\"format_custom_currency\", {\n    ...formatCustomCurrency,\n    id: \"format_custom_currency\",\n    sequence: 60,\n    separator: true,\n})\n    .add(\"format_number_date\", {\n    ...formatNumberDate,\n    id: \"format_number_date\",\n    sequence: 70,\n})\n    .add(\"format_number_time\", {\n    ...formatNumberTime,\n    id: \"format_number_time\",\n    sequence: 80,\n})\n    .add(\"format_number_date_time\", {\n    ...formatNumberDateTime,\n    id: \"format_number_date_time\",\n    sequence: 90,\n})\n    .add(\"format_number_duration\", {\n    ...formatNumberDuration,\n    id: \"format_number_duration\",\n    sequence: 100,\n    separator: true,\n})\n    .add(\"more_formats\", {\n    ...moreFormats,\n    id: \"more_formats\",\n    sequence: 120,\n});\nfunction getCustomNumberFormats(env) {\n    const defaultFormats = new Set(numberFormatMenuRegistry\n        .getAll()\n        .map((f) => (typeof f.format === \"function\" ? f.format(env) : f.format)));\n    const customFormats = new Map();\n    for (const sheetId of env.model.getters.getSheetIds()) {\n        const cells = env.model.getters.getEvaluatedCells(sheetId);\n        for (const cellId in cells) {\n            const cell = cells[cellId];\n            if (cell.format && !customFormats.has(cell.format) && !defaultFormats.has(cell.format)) {\n                const formatType = getNumberFormatType(cell.format);\n                if (formatType === \"date\" || formatType === \"currency\") {\n                    customFormats.set(cell.format, createFormatActionSpec({\n                        descriptionValue: formatType === \"currency\" ? 1000 : EXAMPLE_DATE,\n                        format: cell.format,\n                        name: cell.format,\n                    }));\n                }\n            }\n        }\n    }\n    return [...customFormats.values()];\n}\nconst getNumberFormatType = memoize((format) => {\n    if (isDateTimeFormat(format)) {\n        return \"date\";\n    }\n    else if (format.includes(\"[$\")) {\n        return \"currency\";\n    }\n    return \"number\";\n});\nconst formatNumberMenuItemSpec = {\n    name: _t(\"More formats\"),\n    icon: \"o-spreadsheet-Icon.NUMBER_FORMATS\",\n    children: [\n        (env) => {\n            const customFormats = getCustomNumberFormats(env).map((action) => ({\n                ...action,\n                sequence: 110,\n            }));\n            if (customFormats.length > 0) {\n                customFormats[customFormats.length - 1].separator = true;\n            }\n            return createActions([...numberFormatMenuRegistry.getAll(), ...customFormats]);\n        },\n    ],\n};\n\nconst rowMenuRegistry = new MenuItemRegistry();\nrowMenuRegistry\n    .add(\"cut\", {\n    ...cut,\n    sequence: 10,\n})\n    .add(\"copy\", {\n    ...copy,\n    sequence: 20,\n})\n    .add(\"paste\", {\n    ...paste,\n    sequence: 30,\n})\n    .add(\"paste_special\", {\n    ...pasteSpecial,\n    sequence: 40,\n    separator: true,\n})\n    .addChild(\"paste_value_only\", [\"paste_special\"], {\n    ...pasteSpecialValue,\n    sequence: 10,\n})\n    .addChild(\"paste_format_only\", [\"paste_special\"], {\n    ...pasteSpecialFormat,\n    sequence: 20,\n})\n    .add(\"add_row_before\", {\n    ...rowInsertRowBefore,\n    sequence: 50,\n})\n    .add(\"add_row_after\", {\n    ...rowInsertRowsAfter,\n    sequence: 60,\n})\n    .add(\"delete_row\", {\n    ...deleteRows,\n    sequence: 70,\n    icon: \"o-spreadsheet-Icon.TRASH\",\n})\n    .add(\"clear_row\", {\n    ...clearRows,\n    sequence: 80,\n    icon: \"o-spreadsheet-Icon.CLEAR\",\n})\n    .add(\"hide_rows\", {\n    ...hideRows,\n    sequence: 85,\n    separator: true,\n})\n    .add(\"unhide_rows\", {\n    ...unhideRows,\n    sequence: 86,\n    separator: true,\n})\n    .add(\"conditional_formatting\", {\n    ...formatCF,\n    sequence: 90,\n    separator: true,\n})\n    .add(\"group_rows\", {\n    sequence: 100,\n    ...groupRows,\n})\n    .add(\"ungroup_rows\", {\n    sequence: 110,\n    ...ungroupRows,\n    isVisible: (env) => canUngroupHeaders(env, \"ROW\"),\n});\n\nfunction getSheetMenuRegistry(args) {\n    const sheetMenuRegistry = new MenuItemRegistry();\n    sheetMenuRegistry\n        .add(\"delete\", {\n        ...deleteSheet,\n        sequence: 10,\n    })\n        .add(\"hide_sheet\", {\n        ...hideSheet,\n        sequence: 20,\n    })\n        .add(\"duplicate\", {\n        ...duplicateSheet,\n        sequence: 30,\n        separator: true,\n    })\n        .add(\"rename\", {\n        ...renameSheet(args),\n        sequence: 40,\n    })\n        .add(\"change_color\", {\n        ...changeSheetColor(args),\n        sequence: 50,\n        separator: true,\n    })\n        .add(\"move_right\", {\n        ...sheetMoveRight,\n        sequence: 60,\n    })\n        .add(\"move_left\", {\n        ...sheetMoveLeft,\n        sequence: 70,\n    });\n    return sheetMenuRegistry;\n}\n\nfunction getPivotHighlights(getters, pivotId) {\n    const sheetId = getters.getActiveSheetId();\n    const pivotCellPositions = getVisiblePivotCellPositions(getters, pivotId);\n    const mergedZones = mergeContiguousZones(pivotCellPositions.map(positionToZone));\n    return mergedZones.map((zone) => ({ sheetId, zone, noFill: true, color: HIGHLIGHT_COLOR }));\n}\nfunction getVisiblePivotCellPositions(getters, pivotId) {\n    const positions = [];\n    const sheetId = getters.getActiveSheetId();\n    for (const col of getters.getSheetViewVisibleCols()) {\n        for (const row of getters.getSheetViewVisibleRows()) {\n            const position = { sheetId, col, row };\n            const cellPivotId = getters.getPivotIdFromPosition(position);\n            if (pivotId === cellPivotId) {\n                positions.push(position);\n            }\n        }\n    }\n    return positions;\n}\n\nfunction drawHighlight(renderingContext, highlight, rect) {\n    const { x, y, width, height } = rect;\n    if (width < 0 || height < 0) {\n        return;\n    }\n    const color = highlight.color || HIGHLIGHT_COLOR;\n    const { ctx } = renderingContext;\n    ctx.save();\n    if (!highlight.noBorder) {\n        if (highlight.dashed) {\n            ctx.setLineDash([5, 3]);\n        }\n        ctx.strokeStyle = color;\n        if (highlight.thinLine) {\n            ctx.lineWidth = 1;\n            ctx.strokeRect(x, y, width, height);\n        }\n        else {\n            ctx.lineWidth = 2;\n            /** + 0.5 offset to have sharp lines. See comment in {@link RendererPlugin#drawBorder} for more details */\n            ctx.strokeRect(x + 0.5, y + 0.5, width, height);\n        }\n    }\n    if (!highlight.noFill) {\n        ctx.fillStyle = setColorAlpha(toHex(color), highlight.fillAlpha ?? 0.12);\n        ctx.fillRect(x, y, width, height);\n    }\n    ctx.restore();\n}\n\nclass HighlightStore extends SpreadsheetStore {\n    mutators = [\"register\", \"unRegister\"];\n    providers = [];\n    constructor(get) {\n        super(get);\n        this.onDispose(() => {\n            this.providers = [];\n        });\n    }\n    get renderingLayers() {\n        return [\"Highlights\"];\n    }\n    get highlights() {\n        const activeSheetId = this.getters.getActiveSheetId();\n        return this.providers\n            .flatMap((h) => h.highlights)\n            .filter((h) => h.sheetId === activeSheetId)\n            .map((highlight) => {\n            const { numberOfRows, numberOfCols } = zoneToDimension(highlight.zone);\n            const zone = numberOfRows * numberOfCols === 1\n                ? this.getters.expandZone(highlight.sheetId, highlight.zone)\n                : highlight.zone;\n            return {\n                ...highlight,\n                zone,\n            };\n        });\n    }\n    register(highlightProvider) {\n        this.providers.push(highlightProvider);\n    }\n    unRegister(highlightProvider) {\n        this.providers = this.providers.filter((h) => h !== highlightProvider);\n    }\n    drawLayer(ctx, layer) {\n        if (layer === \"Highlights\") {\n            for (const highlight of this.highlights) {\n                const rect = this.getters.getVisibleRect(highlight.zone);\n                drawHighlight(ctx, highlight, rect);\n            }\n        }\n    }\n}\n\nconst topbarMenuRegistry = new MenuItemRegistry();\ntopbarMenuRegistry\n    // ---------------------------------------------------------------------\n    // FILE MENU ITEMS\n    // ---------------------------------------------------------------------\n    .add(\"file\", {\n    name: _t(\"File\"),\n    sequence: 10,\n})\n    .addChild(\"settings\", [\"file\"], {\n    name: _t(\"Settings\"),\n    sequence: 100,\n    execute: (env) => env.openSidePanel(\"Settings\"),\n    icon: \"o-spreadsheet-Icon.COG\",\n})\n    // ---------------------------------------------------------------------\n    // EDIT MENU ITEMS\n    // ---------------------------------------------------------------------\n    .add(\"edit\", {\n    name: _t(\"Edit\"),\n    sequence: 20,\n})\n    .addChild(\"undo\", [\"edit\"], {\n    ...undo,\n    sequence: 10,\n})\n    .addChild(\"redo\", [\"edit\"], {\n    ...redo,\n    sequence: 20,\n    separator: true,\n})\n    .addChild(\"copy\", [\"edit\"], {\n    ...copy,\n    sequence: 30,\n})\n    .addChild(\"cut\", [\"edit\"], {\n    ...cut,\n    sequence: 40,\n})\n    .addChild(\"paste\", [\"edit\"], {\n    ...paste,\n    sequence: 50,\n})\n    .addChild(\"paste_special\", [\"edit\"], {\n    ...pasteSpecial,\n    sequence: 60,\n    separator: true,\n})\n    .addChild(\"paste_special_value\", [\"edit\", \"paste_special\"], {\n    ...pasteSpecialValue,\n    sequence: 10,\n})\n    .addChild(\"paste_special_format\", [\"edit\", \"paste_special\"], {\n    ...pasteSpecialFormat,\n    sequence: 20,\n})\n    .addChild(\"edit_table\", [\"edit\"], {\n    ...editTable,\n    isVisible: SELECTION_CONTAINS_SINGLE_TABLE,\n    sequence: 60,\n})\n    .addChild(\"find_and_replace\", [\"edit\"], {\n    ...findAndReplace,\n    sequence: 65,\n    separator: true,\n})\n    .addChild(\"delete\", [\"edit\"], {\n    name: _t(\"Delete\"),\n    icon: \"o-spreadsheet-Icon.TRASH\",\n    sequence: 70,\n})\n    .addChild(\"edit_delete_cell_values\", [\"edit\", \"delete\"], {\n    ...deleteValues,\n    sequence: 10,\n})\n    .addChild(\"edit_delete_row\", [\"edit\", \"delete\"], {\n    ...deleteRows,\n    sequence: 20,\n})\n    .addChild(\"edit_delete_column\", [\"edit\", \"delete\"], {\n    ...deleteCols,\n    sequence: 30,\n})\n    .addChild(\"edit_delete_cell_shift_up\", [\"edit\", \"delete\"], {\n    ...deleteCellShiftUp,\n    sequence: 40,\n})\n    .addChild(\"edit_delete_cell_shift_left\", [\"edit\", \"delete\"], {\n    ...deleteCellShiftLeft,\n    sequence: 50,\n})\n    .addChild(\"edit_unhide_columns\", [\"edit\"], {\n    ...unhideAllCols,\n    sequence: 80,\n})\n    .addChild(\"edit_unhide_rows\", [\"edit\"], {\n    ...unhideAllRows,\n    sequence: 80,\n})\n    // ---------------------------------------------------------------------\n    // VIEW MENU ITEMS\n    // ---------------------------------------------------------------------\n    .add(\"view\", {\n    name: _t(\"View\"),\n    sequence: 30,\n})\n    .addChild(\"unfreeze_panes\", [\"view\"], {\n    ...unFreezePane,\n    sequence: 4,\n})\n    .addChild(\"freeze_panes\", [\"view\"], {\n    ...freezePane,\n    sequence: 5,\n})\n    .addChild(\"unfreeze_rows\", [\"view\", \"freeze_panes\"], {\n    ...unFreezeRows,\n    sequence: 5,\n})\n    .addChild(\"freeze_first_row\", [\"view\", \"freeze_panes\"], {\n    ...freezeFirstRow,\n    sequence: 10,\n})\n    .addChild(\"freeze_second_row\", [\"view\", \"freeze_panes\"], {\n    ...freezeSecondRow,\n    sequence: 15,\n})\n    .addChild(\"freeze_current_row\", [\"view\", \"freeze_panes\"], {\n    ...freezeCurrentRow,\n    sequence: 20,\n    separator: true,\n})\n    .addChild(\"unfreeze_columns\", [\"view\", \"freeze_panes\"], {\n    ...unFreezeCols,\n    sequence: 25,\n})\n    .addChild(\"freeze_first_col\", [\"view\", \"freeze_panes\"], {\n    ...freezeFirstCol,\n    sequence: 30,\n})\n    .addChild(\"freeze_second_col\", [\"view\", \"freeze_panes\"], {\n    ...freezeSecondCol,\n    sequence: 35,\n})\n    .addChild(\"freeze_current_col\", [\"view\", \"freeze_panes\"], {\n    ...freezeCurrentCol,\n    sequence: 40,\n})\n    .addChild(\"group_headers\", [\"view\"], {\n    name: _t(\"Group\"),\n    sequence: 15,\n    separator: true,\n    icon: \"o-spreadsheet-Icon.PLUS_IN_BOX\",\n    isVisible: IS_ONLY_ONE_RANGE,\n})\n    .addChild(\"group_columns\", [\"view\", \"group_headers\"], {\n    ...groupColumns,\n    sequence: 5,\n})\n    .addChild(\"ungroup_columns\", [\"view\", \"group_headers\"], {\n    ...ungroupColumns,\n    isVisible: (env) => canUngroupHeaders(env, \"COL\"),\n    sequence: 10,\n})\n    .addChild(\"group_rows\", [\"view\", \"group_headers\"], {\n    ...groupRows,\n    sequence: 15,\n})\n    .addChild(\"ungroup_rows\", [\"view\", \"group_headers\"], {\n    ...ungroupRows,\n    isVisible: (env) => canUngroupHeaders(env, \"ROW\"),\n    sequence: 20,\n})\n    .addChild(\"show\", [\"view\"], {\n    name: _t(\"Show\"),\n    sequence: 1,\n    icon: \"o-spreadsheet-Icon.SHOW\",\n})\n    .addChild(\"view_gridlines\", [\"view\", \"show\"], {\n    ...viewGridlines,\n    sequence: 5,\n})\n    .addChild(\"view_formulas\", [\"view\", \"show\"], {\n    ...viewFormulas,\n    sequence: 10,\n})\n    // ---------------------------------------------------------------------\n    // INSERT MENU ITEMS\n    // ---------------------------------------------------------------------\n    .add(\"insert\", {\n    name: _t(\"Insert\"),\n    sequence: 40,\n})\n    .addChild(\"insert_row\", [\"insert\"], {\n    ...insertRow,\n    sequence: 10,\n})\n    .addChild(\"insert_row_before\", [\"insert\", \"insert_row\"], {\n    ...topBarInsertRowsBefore,\n    sequence: 10,\n})\n    .addChild(\"insert_row_after\", [\"insert\", \"insert_row\"], {\n    ...topBarInsertRowsAfter,\n    sequence: 20,\n})\n    .addChild(\"insert_column\", [\"insert\"], {\n    ...insertCol,\n    sequence: 20,\n})\n    .addChild(\"insert_column_before\", [\"insert\", \"insert_column\"], {\n    ...topBarInsertColsBefore,\n    sequence: 10,\n})\n    .addChild(\"insert_column_after\", [\"insert\", \"insert_column\"], {\n    ...topBarInsertColsAfter,\n    sequence: 20,\n})\n    .addChild(\"insert_cell\", [\"insert\"], {\n    ...insertCell,\n    sequence: 30,\n})\n    .addChild(\"insert_cell_down\", [\"insert\", \"insert_cell\"], {\n    ...insertCellShiftDown,\n    name: _t(\"Shift down\"),\n    sequence: 10,\n})\n    .addChild(\"insert_cell_right\", [\"insert\", \"insert_cell\"], {\n    ...insertCellShiftRight,\n    name: _t(\"Shift right\"),\n    sequence: 20,\n})\n    .addChild(\"insert_sheet\", [\"insert\"], {\n    ...insertSheet,\n    sequence: 40,\n    separator: true,\n})\n    .addChild(\"insert_chart\", [\"insert\"], {\n    ...insertChart,\n    sequence: 50,\n})\n    .addChild(\"insert_pivot\", [\"insert\"], {\n    ...insertPivot,\n    sequence: 52,\n})\n    .addChild(\"insert_image\", [\"insert\"], {\n    ...insertImage,\n    sequence: 55,\n})\n    .addChild(\"insert_table\", [\"insert\"], {\n    ...insertTable,\n    sequence: 57,\n})\n    .addChild(\"insert_function\", [\"insert\"], {\n    ...insertFunction,\n    sequence: 60,\n})\n    .addChild(\"insert_function_sum\", [\"insert\", \"insert_function\"], {\n    ...insertFunctionSum,\n    sequence: 0,\n})\n    .addChild(\"insert_function_average\", [\"insert\", \"insert_function\"], {\n    ...insertFunctionAverage,\n    sequence: 10,\n})\n    .addChild(\"insert_function_count\", [\"insert\", \"insert_function\"], {\n    ...insertFunctionCount,\n    sequence: 20,\n})\n    .addChild(\"insert_function_max\", [\"insert\", \"insert_function\"], {\n    ...insertFunctionMax,\n    sequence: 30,\n})\n    .addChild(\"insert_function_min\", [\"insert\", \"insert_function\"], {\n    ...insertFunctionMin,\n    sequence: 40,\n    separator: true,\n})\n    .addChild(\"categorie_function_all\", [\"insert\", \"insert_function\"], {\n    ...categorieFunctionAll,\n    sequence: 50,\n})\n    .addChild(\"categories_function_list\", [\"insert\", \"insert_function\"], categoriesFunctionListMenuBuilder)\n    .addChild(\"insert_link\", [\"insert\"], {\n    ...insertLink,\n    separator: true,\n    sequence: 70,\n})\n    .addChild(\"insert_checkbox\", [\"insert\"], {\n    ...insertCheckbox,\n    sequence: 80,\n})\n    .addChild(\"insert_dropdown\", [\"insert\"], {\n    ...insertDropdown,\n    separator: true,\n    sequence: 90,\n})\n    // ---------------------------------------------------------------------\n    // FORMAT MENU ITEMS\n    // ---------------------------------------------------------------------\n    .add(\"format\", { name: _t(\"Format\"), sequence: 50 })\n    .addChild(\"format_number\", [\"format\"], {\n    ...formatNumberMenuItemSpec,\n    name: _t(\"Number\"),\n    sequence: 10,\n    separator: true,\n})\n    .addChild(\"format_bold\", [\"format\"], {\n    ...formatBold,\n    sequence: 20,\n})\n    .addChild(\"format_italic\", [\"format\"], {\n    ...formatItalic,\n    sequence: 30,\n})\n    .addChild(\"format_underline\", [\"format\"], {\n    ...formatUnderline,\n    sequence: 40,\n})\n    .addChild(\"format_strikethrough\", [\"format\"], {\n    ...formatStrikethrough,\n    sequence: 50,\n    separator: true,\n})\n    .addChild(\"format_font_size\", [\"format\"], {\n    ...formatFontSize,\n    sequence: 60,\n    separator: true,\n})\n    .addChild(\"format_alignment\", [\"format\"], {\n    ...formatAlignment,\n    sequence: 70,\n})\n    .addChild(\"format_alignment_left\", [\"format\", \"format_alignment\"], {\n    ...formatAlignmentLeft,\n    sequence: 10,\n})\n    .addChild(\"format_alignment_center\", [\"format\", \"format_alignment\"], {\n    ...formatAlignmentCenter,\n    sequence: 20,\n})\n    .addChild(\"format_alignment_right\", [\"format\", \"format_alignment\"], {\n    ...formatAlignmentRight,\n    sequence: 30,\n    separator: true,\n})\n    .addChild(\"format_alignment_top\", [\"format\", \"format_alignment\"], {\n    ...formatAlignmentTop,\n    sequence: 40,\n})\n    .addChild(\"format_alignment_middle\", [\"format\", \"format_alignment\"], {\n    ...formatAlignmentMiddle,\n    sequence: 50,\n})\n    .addChild(\"format_alignment_bottom\", [\"format\", \"format_alignment\"], {\n    ...formatAlignmentBottom,\n    sequence: 60,\n    separator: true,\n})\n    .addChild(\"format_wrapping\", [\"format\"], {\n    ...formatWrappingIcon,\n    sequence: 80,\n    separator: true,\n})\n    .addChild(\"format_wrapping_overflow\", [\"format\", \"format_wrapping\"], {\n    ...formatWrappingOverflow,\n    sequence: 10,\n})\n    .addChild(\"format_wrapping_wrap\", [\"format\", \"format_wrapping\"], {\n    ...formatWrappingWrap,\n    sequence: 20,\n})\n    .addChild(\"format_wrapping_clip\", [\"format\", \"format_wrapping\"], {\n    ...formatWrappingClip,\n    sequence: 30,\n})\n    .addChild(\"format_cf\", [\"format\"], {\n    ...formatCF,\n    sequence: 90,\n    separator: true,\n})\n    .addChild(\"format_clearFormat\", [\"format\"], {\n    ...clearFormat,\n    sequence: 100,\n    separator: true,\n})\n    // ---------------------------------------------------------------------\n    // DATA MENU ITEMS\n    // ---------------------------------------------------------------------\n    .add(\"data\", {\n    name: _t(\"Data\"),\n    sequence: 60,\n})\n    .addChild(\"sort_range\", [\"data\"], {\n    ...sortRange,\n    sequence: 10,\n    separator: true,\n})\n    .addChild(\"sort_ascending\", [\"data\", \"sort_range\"], {\n    ...sortAscending,\n    sequence: 10,\n})\n    .addChild(\"sort_descending\", [\"data\", \"sort_range\"], {\n    ...sortDescending,\n    sequence: 20,\n})\n    .addChild(\"data_cleanup\", [\"data\"], {\n    ...dataCleanup,\n    sequence: 15,\n})\n    .addChild(\"remove_duplicates\", [\"data\", \"data_cleanup\"], {\n    ...removeDuplicates,\n    sequence: 10,\n})\n    .addChild(\"trim_whitespace\", [\"data\", \"data_cleanup\"], {\n    ...trimWhitespace,\n    sequence: 20,\n})\n    .addChild(\"split_to_columns\", [\"data\"], {\n    ...splitToColumns,\n    sequence: 20,\n})\n    .addChild(\"data_validation\", [\"data\"], {\n    name: _t(\"Data Validation\"),\n    execute: (env) => {\n        env.openSidePanel(\"DataValidation\");\n    },\n    icon: \"o-spreadsheet-Icon.DATA_VALIDATION\",\n    sequence: 30,\n    separator: true,\n})\n    .addChild(\"add_remove_data_filter\", [\"data\"], {\n    ...createRemoveFilter,\n    sequence: 40,\n    separator: true,\n})\n    .addChild(\"data_sources_data\", [\"data\"], (env) => {\n    const sequence = 50;\n    return env.model.getters.getPivotIds().map((pivotId, index) => {\n        const highlightProvider = {\n            get highlights() {\n                return getPivotHighlights(env.model.getters, pivotId);\n            },\n        };\n        return {\n            id: `item_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,\n            name: env.model.getters.getPivotDisplayName(pivotId),\n            sequence: sequence + index,\n            execute: (env) => env.openSidePanel(\"PivotSidePanel\", { pivotId }),\n            onStartHover: (env) => env.getStore(HighlightStore).register(highlightProvider),\n            onStopHover: (env) => env.getStore(HighlightStore).unRegister(highlightProvider),\n            icon: \"o-spreadsheet-Icon.PIVOT\",\n            separator: index === env.model.getters.getPivotIds().length - 1,\n            secondaryIcon: (env) => env.model.getters.isPivotUnused(pivotId)\n                ? \"o-spreadsheet-Icon.UNUSED_PIVOT_WARNING\"\n                : undefined,\n        };\n    });\n})\n    .addChild(\"reinsert_dynamic_pivot\", [\"data\"], reinsertDynamicPivotMenu)\n    .addChild(\"reinsert_static_pivot\", [\"data\"], reinsertStaticPivotMenu);\n\nclass OTRegistry extends Registry {\n    /**\n     * Add a transformation function to the registry. When the executed command\n     * happened, all the commands in toTransforms should be transformed using the\n     * transformation function given\n     */\n    addTransformation(executed, toTransforms, fn) {\n        for (let toTransform of toTransforms) {\n            if (!this.content[toTransform]) {\n                this.content[toTransform] = new Map();\n            }\n            this.content[toTransform].set(executed, fn);\n        }\n        return this;\n    }\n    /**\n     * Get the transformation function to transform the command toTransform, after\n     * that the executed command happened.\n     */\n    getTransformation(toTransform, executed) {\n        return this.content[toTransform] && this.content[toTransform].get(executed);\n    }\n}\nconst otRegistry = new OTRegistry();\n\nconst CHECK_SVG = /*xml*/ `\n<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>\n  <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>\n</svg>\n`;\nconst CHECKBOX_WIDTH = 14;\ncss /* scss */ `\n  label.o-checkbox {\n    input {\n      appearance: none;\n      -webkit-appearance: none;\n      -moz-appearance: none;\n      border-radius: 0;\n      width: ${CHECKBOX_WIDTH}px;\n      height: ${CHECKBOX_WIDTH}px;\n      vertical-align: top;\n      box-sizing: border-box;\n      outline: none;\n      border: 1px solid ${GRAY_300};\n\n      &:hover {\n        border-color: ${ACTION_COLOR};\n      }\n\n      &:checked {\n        background: url(\"data:image/svg+xml,${encodeURIComponent(CHECK_SVG)}\");\n        background-color: ${ACTION_COLOR};\n        border-color: ${ACTION_COLOR};\n      }\n\n      &:focus {\n        outline: none;\n        box-shadow: 0 0 0 0.25rem rgba(113, 75, 103, 0.25);\n        border-color: ${ACTION_COLOR};\n      }\n    }\n  }\n`;\nclass Checkbox extends Component {\n    static template = \"o-spreadsheet.Checkbox\";\n    static props = {\n        label: { type: String, optional: true },\n        value: { type: Boolean, optional: true },\n        className: { type: String, optional: true },\n        name: { type: String, optional: true },\n        title: { type: String, optional: true },\n        disabled: { type: Boolean, optional: true },\n        onChange: Function,\n    };\n    static defaultProps = { value: false };\n    onChange(ev) {\n        const value = ev.target.checked;\n        this.props.onChange(value);\n    }\n}\n\nclass Section extends Component {\n    static template = \"o_spreadsheet.Section\";\n    static props = {\n        class: { type: String, optional: true },\n        slots: Object,\n    };\n}\n\n// The name is misleading and can be confused with the DOM focus.\nclass FocusStore {\n    mutators = [\"focus\", \"unfocus\"];\n    focusedElement = null;\n    focus(element) {\n        this.focusedElement = element;\n    }\n    unfocus(element) {\n        if (this.focusedElement && this.focusedElement === element) {\n            this.focusedElement = null;\n        }\n    }\n}\n\n/**\n * Selection input Plugin\n *\n * The SelectionInput component input and output are both arrays of strings, but\n * it requires an intermediary internal state to work.\n * This plugin handles this internal state.\n */\nclass SelectionInputStore extends SpreadsheetStore {\n    initialRanges;\n    inputHasSingleRange;\n    colors;\n    mutators = [\n        \"resetWithRanges\",\n        \"focusById\",\n        \"unfocus\",\n        \"addEmptyRange\",\n        \"removeRange\",\n        \"changeRange\",\n        \"reset\",\n        \"confirm\",\n    ];\n    ranges = [];\n    focusedRangeIndex = null;\n    inputSheetId;\n    focusStore = this.get(FocusStore);\n    highlightStore = this.get(HighlightStore);\n    constructor(get, initialRanges = [], inputHasSingleRange = false, colors = []) {\n        super(get);\n        this.initialRanges = initialRanges;\n        this.inputHasSingleRange = inputHasSingleRange;\n        this.colors = colors;\n        if (inputHasSingleRange && initialRanges.length > 1) {\n            throw new Error(\"Input with a single range cannot be instantiated with several range references.\");\n        }\n        this.inputSheetId = this.getters.getActiveSheetId();\n        this.resetWithRanges(initialRanges);\n        this.highlightStore.register(this);\n        this.onDispose(() => {\n            this.unfocus();\n            this.highlightStore.unRegister(this);\n        });\n    }\n    handleEvent(event) {\n        if (this.focusedRangeIndex === null) {\n            return;\n        }\n        const inputSheetId = this.inputSheetId;\n        const activeSheetId = this.getters.getActiveSheetId();\n        const zone = event.options.unbounded\n            ? this.getters.getUnboundedZone(activeSheetId, event.anchor.zone)\n            : event.anchor.zone;\n        const range = this.getters.getRangeFromZone(activeSheetId, zone);\n        const willAddNewRange = event.mode === \"newAnchor\" &&\n            !this.inputHasSingleRange &&\n            this.ranges[this.focusedRangeIndex].xc.trim() !== \"\";\n        if (willAddNewRange) {\n            const xc = this.getters.getSelectionRangeString(range, inputSheetId);\n            this.insertNewRange(this.ranges.length, [xc]);\n            this.focusLast();\n        }\n        else {\n            let parts = range.parts;\n            const previousXc = this.ranges[this.focusedRangeIndex].xc.trim();\n            if (previousXc) {\n                parts = this.getters.getRangeFromSheetXC(inputSheetId, previousXc).parts;\n            }\n            const newRange = range.clone({ parts });\n            const xc = this.getters.getSelectionRangeString(newRange, inputSheetId);\n            this.setRange(this.focusedRangeIndex, [xc]);\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ACTIVATE_SHEET\": {\n                if (cmd.sheetIdFrom !== cmd.sheetIdTo) {\n                    const { col, row } = this.getters.getNextVisibleCellPosition({\n                        sheetId: cmd.sheetIdTo,\n                        col: 0,\n                        row: 0,\n                    });\n                    const zone = this.getters.expandZone(cmd.sheetIdTo, positionToZone({ col, row }));\n                    this.model.selection.resetAnchor(this, { cell: { col, row }, zone });\n                }\n                break;\n            }\n            case \"START_CHANGE_HIGHLIGHT\":\n                const activeSheetId = this.getters.getActiveSheetId();\n                const newZone = this.getters.expandZone(activeSheetId, cmd.zone);\n                const focusIndex = this.ranges.findIndex((range) => {\n                    const { xc, sheetName: sheet } = splitReference(range.xc);\n                    const sheetName = sheet || this.getters.getSheetName(this.inputSheetId);\n                    if (this.getters.getSheetName(activeSheetId) !== sheetName) {\n                        return false;\n                    }\n                    const refRange = this.getters.getRangeFromSheetXC(activeSheetId, xc);\n                    return isEqual(this.getters.expandZone(activeSheetId, refRange.zone), newZone);\n                });\n                if (focusIndex !== -1) {\n                    this.focus(focusIndex);\n                    const { left, top } = newZone;\n                    this.model.selection.resetAnchor(this, {\n                        cell: { col: left, row: top },\n                        zone: newZone,\n                    });\n                }\n                break;\n        }\n    }\n    changeRange(rangeId, value) {\n        if (this.inputHasSingleRange && value.split(\",\").length > 1) {\n            return;\n        }\n        const index = this.getIndex(rangeId);\n        if (index !== null && this.focusedRangeIndex !== index) {\n            this.focus(index);\n        }\n        if (index !== null) {\n            const valueWithoutLeadingComma = value.replace(/^,+/, \"\");\n            const values = valueWithoutLeadingComma.split(\",\").map((reference) => reference.trim());\n            this.setRange(index, values);\n            this.captureSelection();\n        }\n    }\n    addEmptyRange() {\n        if (this.inputHasSingleRange && this.ranges.length === 1) {\n            return;\n        }\n        this.insertNewRange(this.ranges.length, [\"\"]);\n        this.focusLast();\n    }\n    removeRange(rangeId) {\n        if (this.ranges.length === 1) {\n            return;\n        }\n        const index = this.getIndex(rangeId);\n        if (index !== null) {\n            this.removeRangeByIndex(index);\n        }\n    }\n    confirm() {\n        for (const range of this.selectionInputs) {\n            if (range.xc === \"\") {\n                this.removeRange(range.id);\n            }\n        }\n        const activeSheetId = this.getters.getActiveSheetId();\n        if (this.inputSheetId !== activeSheetId) {\n            this.model.dispatch(\"ACTIVATE_SHEET\", {\n                sheetIdFrom: activeSheetId,\n                sheetIdTo: this.inputSheetId,\n            });\n        }\n        if (this.selectionInputValues.join() !== this.initialRanges.join()) {\n            this.resetWithRanges(this.selectionInputValues);\n        }\n        this.initialRanges = this.selectionInputValues;\n        this.unfocus();\n    }\n    reset() {\n        this.resetWithRanges(this.initialRanges);\n        this.confirm();\n    }\n    get selectionInputValues() {\n        return this.cleanInputs(this.ranges.map((range) => {\n            return range.xc ? range.xc : \"\";\n        }));\n    }\n    /**\n     * Return a list of all valid XCs.\n     * e.g. [\"A1\", \"Sheet2!B3\", \"E12\"]\n     */\n    get selectionInputs() {\n        const generator = new ColorGenerator(this.ranges.length, this.colors);\n        return this.ranges.map((input, index) => Object.assign({}, input, {\n            color: this.hasMainFocus &&\n                this.focusedRangeIndex !== null &&\n                this.getters.isRangeValid(input.xc)\n                ? generator.next()\n                : null,\n            isFocused: this.hasMainFocus && this.focusedRangeIndex === index,\n            isValidRange: input.xc === \"\" || this.getters.isRangeValid(input.xc),\n        }));\n    }\n    get isResettable() {\n        return this.initialRanges.join() !== this.ranges.map((r) => r.xc).join();\n    }\n    get isConfirmable() {\n        return this.selectionInputs.every((range) => range.isValidRange);\n    }\n    get hasFocus() {\n        return this.selectionInputs.some((i) => i.isFocused);\n    }\n    get hasMainFocus() {\n        const focusedElement = this.focusStore.focusedElement;\n        return !!focusedElement && focusedElement === this;\n    }\n    get highlights() {\n        if (!this.hasMainFocus) {\n            return [];\n        }\n        // TODO expand zone globally\n        return this.ranges.map((input) => this.inputToHighlights(input)).flat();\n    }\n    // ---------------------------------------------------------------------------\n    // Other\n    // ---------------------------------------------------------------------------\n    focusById(rangeId) {\n        this.focus(this.getIndex(rangeId));\n    }\n    /**\n     * Focus a given range or remove the focus.\n     */\n    focus(index) {\n        this.focusStore.focus(this);\n        this.focusedRangeIndex = index;\n        this.captureSelection();\n    }\n    focusLast() {\n        this.focus(this.ranges.length - 1);\n    }\n    unfocus() {\n        this.focusedRangeIndex = null;\n        this.focusStore.unfocus(this);\n        this.model.selection.release(this);\n    }\n    captureSelection() {\n        if (this.focusedRangeIndex === null) {\n            return;\n        }\n        const range = this.ranges[this.focusedRangeIndex];\n        const sheetId = this.getters.getActiveSheetId();\n        const zone = this.getters.getRangeFromSheetXC(sheetId, range?.xc || \"A1\").zone;\n        this.model.selection.capture(this, { cell: { col: zone.left, row: zone.top }, zone }, {\n            handleEvent: this.handleEvent.bind(this),\n            release: this.unfocus.bind(this),\n        });\n    }\n    resetWithRanges(ranges) {\n        this.ranges = [];\n        this.insertNewRange(0, ranges);\n        if (this.ranges.length === 0) {\n            this.insertNewRange(this.ranges.length, [\"\"]);\n            this.focusLast();\n        }\n    }\n    setContent(index, xc) {\n        this.ranges[index] = {\n            ...this.ranges[index],\n            xc,\n        };\n    }\n    /**\n     * Insert new inputs after the given index.\n     */\n    insertNewRange(index, values) {\n        const currentMaxId = Math.max(0, ...this.ranges.map((range) => Number(range.id)));\n        const colors = new ColorGenerator(this.ranges.length, this.colors);\n        for (let i = 0; i < index; i++) {\n            colors.next();\n        }\n        this.ranges.splice(index, 0, ...values.map((xc, i) => ({\n            xc,\n            id: currentMaxId + i + 1,\n            color: colors.next(),\n        })));\n    }\n    /**\n     * Set a new value in a given range input. If more than one value is provided,\n     * new inputs will be added.\n     */\n    setRange(index, values) {\n        const [, ...additionalValues] = values;\n        this.setContent(index, values[0]);\n        this.insertNewRange(index + 1, additionalValues);\n        // focus the last newly added range\n        if (additionalValues.length) {\n            this.focus(index + additionalValues.length);\n        }\n    }\n    removeRangeByIndex(index) {\n        this.ranges.splice(index, 1);\n        if (this.focusedRangeIndex !== null) {\n            this.focusLast();\n        }\n    }\n    /**\n     * Converts highlights input format to the command format.\n     * The first xc in the input range will keep its color.\n     * Invalid ranges and ranges from other sheets than the active sheets\n     * are ignored.\n     */\n    inputToHighlights({ xc, color }) {\n        const XCs = this.cleanInputs([xc])\n            .filter((range) => this.getters.isRangeValid(range))\n            .filter((reference) => this.shouldBeHighlighted(this.inputSheetId, reference));\n        return XCs.map((xc) => {\n            const { sheetName } = splitReference(xc);\n            return {\n                zone: this.getters.getRangeFromSheetXC(this.inputSheetId, xc).zone,\n                sheetId: (sheetName && this.getters.getSheetIdByName(sheetName)) || this.inputSheetId,\n                color,\n                interactive: true,\n            };\n        });\n    }\n    cleanInputs(ranges) {\n        return ranges\n            .map((xc) => xc.split(\",\"))\n            .flat()\n            .map((xc) => xc.trim())\n            .filter((xc) => xc !== \"\");\n    }\n    /**\n     * Check if a cell or range reference should be highlighted.\n     * It should be highlighted if it references the current active sheet.\n     * Note that if no sheet name is given in the reference (\"A1\"), it refers to the\n     * active sheet when the selection input was enabled which might be different from\n     * the current active sheet.\n     */\n    shouldBeHighlighted(inputSheetId, reference) {\n        const { sheetName } = splitReference(reference);\n        const sheetId = this.getters.getSheetIdByName(sheetName);\n        const activeSheetId = this.getters.getActiveSheet().id;\n        const valid = this.getters.isRangeValid(reference);\n        return (valid &&\n            (sheetId === activeSheetId || (sheetId === undefined && activeSheetId === inputSheetId)));\n    }\n    /**\n     * Return the index of a range given its id\n     * or `null` if the range is not found.\n     */\n    getIndex(rangeId) {\n        const index = this.ranges.findIndex((range) => range.id === rangeId);\n        return index >= 0 ? index : null;\n    }\n}\n\ncss /* scss */ `\n  .o-selection {\n    .o-selection-input {\n      padding: 2px 0px;\n\n      input.o-invalid {\n        background-color: ${ALERT_DANGER_BG};\n      }\n      .error-icon {\n        right: 7px;\n        top: 4px;\n      }\n    }\n    .o-button {\n      height: 28px;\n      flex-grow: 0;\n    }\n\n    /** Make the character a bit bigger\n    compared to its neighbor INPUT box  */\n    .o-remove-selection {\n      font-size: calc(100% + 4px);\n    }\n  }\n`;\n/**\n * This component can be used when the user needs to input some\n * ranges. He can either input the ranges with the regular DOM `<input/>`\n * displayed or by selecting zones on the grid.\n *\n * onSelectionChanged is called every time the input value\n * changes.\n */\nclass SelectionInput extends Component {\n    static template = \"o-spreadsheet-SelectionInput\";\n    static props = {\n        ranges: Array,\n        hasSingleRange: { type: Boolean, optional: true },\n        required: { type: Boolean, optional: true },\n        isInvalid: { type: Boolean, optional: true },\n        class: { type: String, optional: true },\n        onSelectionChanged: { type: Function, optional: true },\n        onSelectionConfirmed: { type: Function, optional: true },\n        colors: { type: Array, optional: true, default: [] },\n    };\n    state = useState({\n        isMissing: false,\n        mode: \"select-range\",\n    });\n    focusedInput = useRef(\"focusedInput\");\n    store;\n    get ranges() {\n        return this.store.selectionInputs;\n    }\n    get canAddRange() {\n        return !this.props.hasSingleRange;\n    }\n    get isInvalid() {\n        return this.props.isInvalid || this.state.isMissing;\n    }\n    get isConfirmable() {\n        return this.store.isConfirmable;\n    }\n    get isResettable() {\n        return this.store.isResettable;\n    }\n    setup() {\n        useEffect(() => this.focusedInput.el?.focus(), () => [this.focusedInput.el]);\n        this.store = useLocalStore(SelectionInputStore, this.props.ranges, this.props.hasSingleRange || false, this.props.colors);\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.ranges.join() !== this.store.selectionInputValues.join()) {\n                this.triggerChange();\n            }\n            if (nextProps.ranges.join() !== this.props.ranges.join() &&\n                nextProps.ranges.join() !== this.store.selectionInputValues.join()) {\n                this.store.resetWithRanges(nextProps.ranges);\n            }\n        });\n    }\n    getColor(range) {\n        if (!range.color) {\n            return \"\";\n        }\n        return cssPropertiesToCss({ color: range.color });\n    }\n    triggerChange() {\n        const ranges = this.store.selectionInputValues;\n        this.props.onSelectionChanged?.(ranges);\n    }\n    onKeydown(ev) {\n        if (ev.key === \"F2\") {\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.state.mode = this.state.mode === \"select-range\" ? \"text-edit\" : \"select-range\";\n        }\n        else if (ev.key.startsWith(\"Arrow\")) {\n            ev.stopPropagation();\n            if (this.state.mode === \"select-range\") {\n                ev.preventDefault();\n                updateSelectionWithArrowKeys(ev, this.env.model.selection);\n            }\n        }\n        else if (ev.key === \"Enter\") {\n            const target = ev.target;\n            target.blur();\n            this.confirm();\n        }\n    }\n    extractRanges(value) {\n        return this.props.hasSingleRange ? value.split(\",\")[0] : value;\n    }\n    focus(rangeId) {\n        this.state.isMissing = false;\n        this.state.mode = \"select-range\";\n        this.store.focusById(rangeId);\n    }\n    addEmptyInput() {\n        this.store.addEmptyRange();\n    }\n    removeInput(rangeId) {\n        this.store.removeRange(rangeId);\n        this.triggerChange();\n        this.props.onSelectionConfirmed?.();\n    }\n    onInputChanged(rangeId, ev) {\n        const target = ev.target;\n        const value = this.extractRanges(target.value);\n        this.store.changeRange(rangeId, value);\n        this.triggerChange();\n    }\n    reset() {\n        this.store.reset();\n        this.triggerChange();\n    }\n    confirm() {\n        this.store.confirm();\n        const anyValidInput = this.store.selectionInputs.some((range) => this.env.model.getters.isRangeValid(range.xc));\n        if (this.props.required && !anyValidInput) {\n            this.state.isMissing = true;\n        }\n        this.props.onSelectionChanged?.(this.store.selectionInputValues);\n        this.props.onSelectionConfirmed?.();\n    }\n}\n\nclass ChartDataSeries extends Component {\n    static template = \"o-spreadsheet.ChartDataSeries\";\n    static components = { SelectionInput, Section };\n    static props = {\n        ranges: Array,\n        hasSingleRange: { type: Boolean, optional: true },\n        onSelectionChanged: Function,\n        onSelectionConfirmed: Function,\n    };\n    get ranges() {\n        return this.props.ranges.map((r) => r.dataRange);\n    }\n    get colors() {\n        return this.props.ranges.map((r) => r.backgroundColor);\n    }\n    get title() {\n        return this.props.hasSingleRange ? _t(\"Data range\") : _t(\"Data series\");\n    }\n}\n\ncss /* scss */ `\n  .o-validation {\n    border-radius: 4px;\n    border-width: 0 0 0 3px;\n    border-style: solid;\n    gap: 3px;\n\n    .o-icon {\n      margin-right: 5px;\n      height: 1.2em;\n      width: 1.2em;\n    }\n  }\n\n  .o-validation-warning {\n    border-color: ${ALERT_WARNING_BORDER};\n    color: ${ALERT_WARNING_TEXT_COLOR};\n    background-color: ${ALERT_WARNING_BG};\n  }\n\n  .o-validation-error {\n    border-color: ${ALERT_DANGER_BORDER};\n    color: ${ALERT_DANGER_TEXT_COLOR};\n    background-color: ${ALERT_DANGER_BG};\n  }\n\n  .o-validation-info {\n    border-color: ${ALERT_INFO_BORDER};\n    color: ${ALERT_INFO_TEXT_COLOR};\n    background-color: ${ALERT_INFO_BG};\n  }\n`;\nclass ValidationMessages extends Component {\n    static template = \"o-spreadsheet-ValidationMessages\";\n    static props = {\n        messages: Array,\n        msgType: String,\n        singleBox: { type: Boolean, optional: true },\n    };\n    get divClasses() {\n        if (this.props.msgType === \"warning\") {\n            return \"o-validation-warning\";\n        }\n        if (this.props.msgType === \"info\") {\n            return \"o-validation-info\";\n        }\n        return \"o-validation-error\";\n    }\n    get alertBoxes() {\n        return this.props.singleBox ? [this.props.messages] : this.props.messages.map((msg) => [msg]);\n    }\n}\n\nclass ChartErrorSection extends Component {\n    static template = \"o-spreadsheet.ChartErrorSection\";\n    static components = { Section, ValidationMessages };\n    static props = { messages: { type: Array, element: String } };\n}\n\nclass ChartLabelRange extends Component {\n    static template = \"o-spreadsheet.ChartLabelRange\";\n    static components = { SelectionInput, Checkbox, Section };\n    static props = {\n        title: { type: String, optional: true },\n        range: String,\n        isInvalid: Boolean,\n        onSelectionChanged: Function,\n        onSelectionConfirmed: Function,\n        options: { type: Array, optional: true },\n    };\n    static defaultProps = {\n        title: _t(\"Categories / Labels\"),\n        options: [],\n    };\n}\n\nclass GenericChartConfigPanel extends Component {\n    static template = \"o-spreadsheet-GenericChartConfigPanel\";\n    static components = {\n        ChartDataSeries,\n        ChartLabelRange,\n        Section,\n        Checkbox,\n        ChartErrorSection,\n    };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        canUpdateChart: Function,\n    };\n    state = useState({\n        datasetDispatchResult: undefined,\n        labelsDispatchResult: undefined,\n    });\n    dataSeriesRanges = [];\n    labelRange;\n    chartTerms = ChartTerms;\n    setup() {\n        this.dataSeriesRanges = this.props.definition.dataSets;\n        this.labelRange = this.props.definition.labelRange;\n    }\n    get errorMessages() {\n        const cancelledReasons = [\n            ...(this.state.datasetDispatchResult?.reasons || []),\n            ...(this.state.labelsDispatchResult?.reasons || []),\n        ];\n        return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);\n    }\n    get isDatasetInvalid() {\n        return !!this.state.datasetDispatchResult?.isCancelledBecause(\"InvalidDataSet\" /* CommandResult.InvalidDataSet */);\n    }\n    get isLabelInvalid() {\n        return !!this.state.labelsDispatchResult?.isCancelledBecause(\"InvalidLabelRange\" /* CommandResult.InvalidLabelRange */);\n    }\n    get dataSetsHaveTitleLabel() {\n        return _t(\"Use row %s as headers\", this.calculateHeaderPosition() || \"\");\n    }\n    getLabelRangeOptions() {\n        return [\n            {\n                name: \"aggregated\",\n                label: this.chartTerms.AggregatedChart,\n                value: this.props.definition.aggregated ?? false,\n                onChange: this.onUpdateAggregated.bind(this),\n            },\n            {\n                name: \"dataSetsHaveTitle\",\n                label: this.dataSetsHaveTitleLabel,\n                value: this.props.definition.dataSetsHaveTitle,\n                onChange: this.onUpdateDataSetsHaveTitle.bind(this),\n            },\n        ];\n    }\n    onUpdateDataSetsHaveTitle(dataSetsHaveTitle) {\n        this.props.updateChart(this.props.figureId, {\n            dataSetsHaveTitle,\n        });\n    }\n    /**\n     * Change the local dataSeriesRanges. The model should be updated when the\n     * button \"confirm\" is clicked\n     */\n    onDataSeriesRangesChanged(ranges) {\n        this.dataSeriesRanges = ranges.map((dataRange, i) => ({\n            ...this.dataSeriesRanges?.[i],\n            dataRange,\n        }));\n        this.state.datasetDispatchResult = this.props.canUpdateChart(this.props.figureId, {\n            dataSets: this.dataSeriesRanges,\n        });\n    }\n    onDataSeriesConfirmed() {\n        this.dataSeriesRanges = spreadRange(this.env.model.getters, this.dataSeriesRanges);\n        this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {\n            dataSets: this.dataSeriesRanges,\n        });\n    }\n    getDataSeriesRanges() {\n        return this.dataSeriesRanges;\n    }\n    /**\n     * Change the local labelRange. The model should be updated when the\n     * button \"confirm\" is clicked\n     */\n    onLabelRangeChanged(ranges) {\n        this.labelRange = ranges[0];\n        this.state.labelsDispatchResult = this.props.canUpdateChart(this.props.figureId, {\n            labelRange: this.labelRange,\n        });\n    }\n    onLabelRangeConfirmed() {\n        this.state.labelsDispatchResult = this.props.updateChart(this.props.figureId, {\n            labelRange: this.labelRange,\n        });\n    }\n    getLabelRange() {\n        return this.labelRange || \"\";\n    }\n    onUpdateAggregated(aggregated) {\n        this.props.updateChart(this.props.figureId, {\n            aggregated,\n        });\n    }\n    calculateHeaderPosition() {\n        if (this.isDatasetInvalid || this.isLabelInvalid) {\n            return undefined;\n        }\n        const getters = this.env.model.getters;\n        const sheetId = getters.getActiveSheetId();\n        const labelRange = createValidRange(getters, sheetId, this.labelRange);\n        const dataSets = createDataSets(getters, this.dataSeriesRanges, sheetId, this.props.definition.dataSetsHaveTitle);\n        if (dataSets.length) {\n            return dataSets[0].dataRange.zone.top + 1;\n        }\n        else if (labelRange) {\n            return labelRange.zone.top + 1;\n        }\n        return undefined;\n    }\n}\n\nclass BarConfigPanel extends GenericChartConfigPanel {\n    static template = \"o-spreadsheet-BarConfigPanel\";\n    get stackedLabel() {\n        const definition = this.props.definition;\n        return definition.horizontal\n            ? this.chartTerms.StackedBarChart\n            : this.chartTerms.StackedColumnChart;\n    }\n    onUpdateStacked(stacked) {\n        this.props.updateChart(this.props.figureId, {\n            stacked,\n        });\n    }\n    onUpdateAggregated(aggregated) {\n        this.props.updateChart(this.props.figureId, {\n            aggregated,\n        });\n    }\n}\n\ncss /* scss */ `\n  .o_side_panel_collapsible_title {\n    font-size: 16px;\n    cursor: pointer;\n    padding: 6px 0px 6px 6px !important;\n\n    .collapsor-arrow {\n      transform: rotate(-90deg);\n      display: inline-block;\n      transform-origin: 8px 11px;\n      transition: transform 0.2s ease-in-out;\n\n      .o-icon {\n        width: 16px;\n        height: 22px;\n      }\n    }\n    .collapsor:not(.collapsed) .collapsor-arrow {\n      transform: rotate(0);\n    }\n\n    .collapsor {\n      width: 100%;\n      transition-delay: 0.35s;\n      transition-property: all;\n    }\n  }\n\n  .collapsible_section {\n    &.collapsing {\n      transition: height 0.35s, background-color 0.35s !important;\n    }\n  }\n`;\nlet CURRENT_COLLAPSIBLE_ID = 0;\nclass SidePanelCollapsible extends Component {\n    static template = \"o-spreadsheet-SidePanelCollapsible\";\n    static props = {\n        slots: Object,\n        collapsedAtInit: { type: Boolean, optional: true },\n        class: { type: String, optional: true },\n    };\n    currentId = (CURRENT_COLLAPSIBLE_ID++).toString();\n}\n\nconst CIRCLE_SVG = /*xml*/ `\n<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'>\n  <circle r=\"2\" fill=\"#FFF\"/>\n</svg>\n`;\ncss /* scss */ `\n  .o-radio {\n    input {\n      appearance: none;\n      -webkit-appearance: none;\n      -moz-appearance: none;\n      width: 14px;\n      height: 14px;\n      border: 1px solid ${GRAY_300};\n      box-sizing: border-box;\n      outline: none;\n      border-radius: 8px;\n\n      &:checked {\n        background: url(\"data:image/svg+xml,${encodeURIComponent(CIRCLE_SVG)}\");\n        background-color: ${ACTION_COLOR};\n        border-color: ${ACTION_COLOR};\n      }\n    }\n  }\n`;\nclass RadioSelection extends Component {\n    static template = \"o-spreadsheet.RadioSelection\";\n    static props = {\n        choices: Array,\n        onChange: Function,\n        selectedValue: { optional: false },\n        name: String,\n        direction: { type: String, optional: true },\n    };\n    static defaultProps = {\n        direction: \"horizontal\",\n    };\n}\n\n/**\n * Start listening to pointer events and apply the given callbacks.\n *\n * @returns A function to remove the listeners.\n */\nfunction startDnd(onMouseMove, onMouseUp, onMouseDown = () => { }) {\n    const removeListeners = () => {\n        window.removeEventListener(\"pointerdown\", onMouseDown);\n        window.removeEventListener(\"pointerup\", _onMouseUp);\n        window.removeEventListener(\"dragstart\", _onDragStart);\n        window.removeEventListener(\"pointermove\", onMouseMove);\n        window.removeEventListener(\"wheel\", onMouseMove);\n    };\n    const _onMouseUp = (ev) => {\n        onMouseUp(ev);\n        removeListeners();\n    };\n    function _onDragStart(ev) {\n        ev.preventDefault();\n    }\n    window.addEventListener(\"pointerdown\", onMouseDown);\n    window.addEventListener(\"pointerup\", _onMouseUp);\n    window.addEventListener(\"dragstart\", _onDragStart);\n    window.addEventListener(\"pointermove\", onMouseMove);\n    // mouse wheel on window is by default a passive event.\n    // preventDefault() is not allowed in passive event handler.\n    // https://chromestatus.com/feature/6662647093133312\n    window.addEventListener(\"wheel\", onMouseMove, { passive: false });\n    return removeListeners;\n}\n/**\n * Function to be used during a pointerdown event, this function allows to\n * perform actions related to the pointermove and pointerup events and adjusts the viewport\n * when the new position related to the pointermove event is outside of it.\n * Among inputs are two callback functions. First intended for actions performed during\n * the pointermove event, it receives as parameters the current position of the pointermove\n * (occurrence of the current column and the current row). Second intended for actions\n * performed during the pointerup event.\n */\nfunction dragAndDropBeyondTheViewport(env, cbMouseMove, cbMouseUp, only = false) {\n    let timeOutId = null;\n    let currentEv;\n    let previousEv;\n    let startingEv;\n    let startingX;\n    let startingY;\n    const getters = env.model.getters;\n    const sheetId = getters.getActiveSheetId();\n    const position = gridOverlayPosition();\n    let colIndex;\n    let rowIndex;\n    const onMouseDown = (ev) => {\n        previousEv = ev;\n        startingEv = ev;\n        startingX = startingEv.clientX - position.left;\n        startingY = startingEv.clientY - position.top;\n    };\n    const onMouseMove = (ev) => {\n        currentEv = ev;\n        if (timeOutId) {\n            return;\n        }\n        const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();\n        let { top, left, bottom, right } = getters.getActiveMainViewport();\n        let { scrollX, scrollY } = getters.getActiveSheetDOMScrollInfo();\n        const { xSplit, ySplit } = getters.getPaneDivisions(sheetId);\n        let canEdgeScroll = false;\n        let timeoutDelay = MAX_DELAY;\n        const x = currentEv.clientX - position.left;\n        colIndex = getters.getColIndex(x);\n        if (only !== \"vertical\") {\n            const previousX = previousEv.clientX - position.left;\n            const edgeScrollInfoX = getters.getEdgeScrollCol(x, previousX, startingX);\n            if (edgeScrollInfoX.canEdgeScroll) {\n                canEdgeScroll = true;\n                timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoX.delay);\n                let newTarget;\n                switch (edgeScrollInfoX.direction) {\n                    case \"reset\":\n                        colIndex = xSplit;\n                        newTarget = xSplit;\n                        break;\n                    case 1:\n                        colIndex = right;\n                        newTarget = left + 1;\n                        break;\n                    case -1:\n                        colIndex = left - 1;\n                        while (env.model.getters.isColHidden(sheetId, colIndex)) {\n                            colIndex--;\n                        }\n                        newTarget = colIndex;\n                        break;\n                }\n                scrollX = getters.getColDimensions(sheetId, newTarget).start - offsetCorrectionX;\n            }\n        }\n        const y = currentEv.clientY - position.top;\n        rowIndex = getters.getRowIndex(y);\n        if (only !== \"horizontal\") {\n            const previousY = previousEv.clientY - position.top;\n            const edgeScrollInfoY = getters.getEdgeScrollRow(y, previousY, startingY);\n            if (edgeScrollInfoY.canEdgeScroll) {\n                canEdgeScroll = true;\n                timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoY.delay);\n                let newTarget;\n                switch (edgeScrollInfoY.direction) {\n                    case \"reset\":\n                        rowIndex = ySplit;\n                        newTarget = ySplit;\n                        break;\n                    case 1:\n                        rowIndex = bottom;\n                        newTarget = top + edgeScrollInfoY.direction;\n                        break;\n                    case -1:\n                        rowIndex = top - 1;\n                        while (env.model.getters.isRowHidden(sheetId, rowIndex)) {\n                            rowIndex--;\n                        }\n                        newTarget = rowIndex;\n                        break;\n                }\n                scrollY = env.model.getters.getRowDimensions(sheetId, newTarget).start - offsetCorrectionY;\n            }\n        }\n        if (!canEdgeScroll) {\n            if (rowIndex === -1) {\n                rowIndex = y < 0 ? 0 : getters.getNumberRows(sheetId) - 1;\n            }\n            if (colIndex === -1 && x < 0) {\n                colIndex = x < 0 ? 0 : getters.getNumberCols(sheetId) - 1;\n            }\n        }\n        cbMouseMove(colIndex, rowIndex, currentEv);\n        if (canEdgeScroll) {\n            env.model.dispatch(\"SET_VIEWPORT_OFFSET\", { offsetX: scrollX, offsetY: scrollY });\n            timeOutId = setTimeout(() => {\n                timeOutId = null;\n                onMouseMove(currentEv);\n            }, Math.round(timeoutDelay));\n        }\n        previousEv = currentEv;\n    };\n    const onMouseUp = () => {\n        clearTimeout(timeOutId);\n        cbMouseUp();\n    };\n    startDnd(onMouseMove, onMouseUp, onMouseDown);\n}\n\nconst LINE_VERTICAL_PADDING = 1;\nconst PICKER_PADDING = 8;\nconst ITEM_BORDER_WIDTH = 1;\nconst ITEM_EDGE_LENGTH = 18;\nconst ITEMS_PER_LINE = 10;\nconst MAGNIFIER_EDGE = 16;\nconst ITEM_GAP = 2;\nconst CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;\nconst INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;\nconst INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;\nconst CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;\ncss /* scss */ `\n  .o-color-picker {\n    padding: ${PICKER_PADDING}px 0;\n    /** FIXME: this is useless, overiden by the popover container */\n    box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);\n    background-color: white;\n    line-height: 1.2;\n    overflow-y: auto;\n    overflow-x: hidden;\n    width: ${CONTAINER_WIDTH}px;\n\n    .o-color-picker-section-name {\n      margin: 0px ${ITEM_BORDER_WIDTH}px;\n      padding: 4px ${PICKER_PADDING}px;\n    }\n    .colors-grid {\n      display: grid;\n      padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;\n      grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);\n      grid-gap: ${ITEM_GAP}px;\n    }\n    .o-color-picker-toggler-button {\n      display: flex;\n      .o-color-picker-toggler-sign {\n        display: flex;\n        margin: auto auto;\n        width: 55%;\n        height: 55%;\n        .o-icon {\n          width: 100%;\n          height: 100%;\n        }\n      }\n    }\n    .o-color-picker-line-item {\n      width: ${ITEM_EDGE_LENGTH}px;\n      height: ${ITEM_EDGE_LENGTH}px;\n      margin: 0px;\n      border-radius: 50px;\n      border: ${ITEM_BORDER_WIDTH}px solid #666666;\n      padding: 0px;\n      font-size: 16px;\n      background: white;\n      &:hover {\n        background-color: rgba(0, 0, 0, 0.08);\n        outline: 1px solid gray;\n        cursor: pointer;\n      }\n    }\n    .o-buttons {\n      padding: ${PICKER_PADDING}px;\n      display: flex;\n      .o-cancel {\n        border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;\n        width: 100%;\n        padding: 5px;\n        font-size: 14px;\n        background: white;\n        border-radius: 4px;\n        box-sizing: border-box;\n        &:hover:enabled {\n          background-color: rgba(0, 0, 0, 0.08);\n        }\n      }\n    }\n    .o-add-button {\n      border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;\n      padding: 4px;\n      background: white;\n      border-radius: 4px;\n      &:hover:enabled {\n        background-color: rgba(0, 0, 0, 0.08);\n      }\n    }\n    .o-separator {\n      border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};\n      margin-top: ${MENU_SEPARATOR_PADDING}px;\n      margin-bottom: ${MENU_SEPARATOR_PADDING}px;\n    }\n\n    .o-custom-selector {\n      padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;\n      position: relative;\n      .o-gradient {\n        margin-bottom: ${MAGNIFIER_EDGE / 2}px;\n        border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;\n        box-sizing: border-box;\n        width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;\n        height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;\n        position: relative;\n      }\n\n      .magnifier {\n        height: ${MAGNIFIER_EDGE}px;\n        width: ${MAGNIFIER_EDGE}px;\n        box-sizing: border-box;\n        border-radius: 50%;\n        border: 2px solid #fff;\n        box-shadow: 0px 0px 3px #c0c0c0;\n        position: absolute;\n        z-index: 2;\n      }\n      .saturation {\n        background: linear-gradient(to right, #fff 0%, transparent 100%);\n      }\n      .lightness {\n        background: linear-gradient(to top, #000 0%, transparent 100%);\n      }\n      .o-hue-picker {\n        border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;\n        box-sizing: border-box;\n        width: 100%;\n        height: 12px;\n        border-radius: 4px;\n        background: linear-gradient(\n          to right,\n          hsl(0 100% 50%) 0%,\n          hsl(0.2turn 100% 50%) 20%,\n          hsl(0.3turn 100% 50%) 30%,\n          hsl(0.4turn 100% 50%) 40%,\n          hsl(0.5turn 100% 50%) 50%,\n          hsl(0.6turn 100% 50%) 60%,\n          hsl(0.7turn 100% 50%) 70%,\n          hsl(0.8turn 100% 50%) 80%,\n          hsl(0.9turn 100% 50%) 90%,\n          hsl(1turn 100% 50%) 100%\n        );\n        position: relative;\n        cursor: crosshair;\n      }\n      .o-hue-slider {\n        margin-top: -3px;\n      }\n      .o-custom-input-preview {\n        padding: 2px 0px;\n        display: flex;\n        input {\n          box-sizing: border-box;\n          width: 50%;\n          border-radius: 4px;\n          padding: 4px 23px 4px 10px;\n          height: 24px;\n          border: 1px solid #c0c0c0;\n          margin-right: 2px;\n        }\n        .o-wrong-color {\n          /** FIXME bootstrap class instead? */\n          outline-color: red;\n          border-color: red;\n          &:focus {\n            outline-style: solid;\n            outline-width: 1px;\n          }\n        }\n      }\n      .o-custom-input-buttons {\n        padding: 2px 0px;\n        display: flex;\n        justify-content: end;\n      }\n      .o-color-preview {\n        border: 1px solid #c0c0c0;\n        border-radius: 4px;\n        width: 50%;\n      }\n    }\n  }\n`;\nclass ColorPicker extends Component {\n    static template = \"o-spreadsheet-ColorPicker\";\n    static props = {\n        onColorPicked: Function,\n        currentColor: { type: String, optional: true },\n        maxHeight: { type: Number, optional: true },\n        anchorRect: Object,\n        disableNoColor: { type: Boolean, optional: true },\n    };\n    static defaultProps = { currentColor: \"\" };\n    static components = { Popover };\n    COLORS = COLOR_PICKER_DEFAULTS;\n    state = useState({\n        showGradient: false,\n        currentHslaColor: isColorValid(this.props.currentColor)\n            ? { ...hexToHSLA(this.props.currentColor), a: 1 }\n            : { h: 0, s: 100, l: 100, a: 1 },\n        customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : \"\",\n    });\n    get colorPickerStyle() {\n        if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {\n            return cssPropertiesToCss({ display: \"none\" });\n        }\n        return \"\";\n    }\n    get popoverProps() {\n        return {\n            anchorRect: this.props.anchorRect,\n            maxHeight: this.props.maxHeight,\n            positioning: \"BottomLeft\",\n            verticalOffset: 0,\n        };\n    }\n    get gradientHueStyle() {\n        const hue = this.state.currentHslaColor?.h || 0;\n        return cssPropertiesToCss({\n            background: `hsl(${hue} 100% 50%)`,\n        });\n    }\n    get sliderStyle() {\n        const hue = this.state.currentHslaColor?.h || 0;\n        const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);\n        const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;\n        return cssPropertiesToCss({\n            \"margin-left\": `${left}px`,\n        });\n    }\n    get pointerStyle() {\n        const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };\n        const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));\n        const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));\n        return cssPropertiesToCss({\n            left: `${-MAGNIFIER_EDGE / 2 + left}px`,\n            top: `${-MAGNIFIER_EDGE / 2 + top}px`,\n            background: hslaToHex(this.state.currentHslaColor),\n        });\n    }\n    get colorPreviewStyle() {\n        return cssPropertiesToCss({\n            \"background-color\": hslaToHex(this.state.currentHslaColor),\n        });\n    }\n    get checkmarkColor() {\n        return chartFontColor(this.props.currentColor);\n    }\n    get isHexColorInputValid() {\n        return !this.state.customHexColor || isColorValid(this.state.customHexColor);\n    }\n    setCustomGradient({ x, y }) {\n        const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);\n        const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);\n        const deltaX = offsetX / INNER_GRADIENT_WIDTH;\n        const deltaY = offsetY / INNER_GRADIENT_HEIGHT;\n        const s = 100 * deltaX;\n        const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);\n        this.updateColor({ s, l });\n    }\n    setCustomHue(x) {\n        // needs to be capped such that h is in [0\u00b0, 359\u00b0]\n        const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));\n        this.updateColor({ h });\n    }\n    updateColor(newHsl) {\n        this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };\n        this.state.customHexColor = hslaToHex(this.state.currentHslaColor);\n    }\n    onColorClick(color) {\n        if (color) {\n            this.props.onColorPicked(toHex(color));\n        }\n    }\n    resetColor() {\n        this.props.onColorPicked(\"\");\n    }\n    toggleColorPicker() {\n        this.state.showGradient = !this.state.showGradient;\n    }\n    dragGradientPointer(ev) {\n        const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };\n        this.setCustomGradient(initialGradientCoordinates);\n        const initialMousePosition = { x: ev.clientX, y: ev.clientY };\n        const onMouseMove = (ev) => {\n            const currentMousePosition = { x: ev.clientX, y: ev.clientY };\n            const deltaX = currentMousePosition.x - initialMousePosition.x;\n            const deltaY = currentMousePosition.y - initialMousePosition.y;\n            const currentGradientCoordinates = {\n                x: initialGradientCoordinates.x + deltaX,\n                y: initialGradientCoordinates.y + deltaY,\n            };\n            this.setCustomGradient(currentGradientCoordinates);\n        };\n        startDnd(onMouseMove, () => { });\n    }\n    dragHuePointer(ev) {\n        const initialX = ev.offsetX;\n        const initialMouseX = ev.clientX;\n        this.setCustomHue(initialX);\n        const onMouseMove = (ev) => {\n            const currentMouseX = ev.clientX;\n            const deltaX = currentMouseX - initialMouseX;\n            const x = initialX + deltaX;\n            this.setCustomHue(x);\n        };\n        startDnd(onMouseMove, () => { });\n    }\n    setHexColor(ev) {\n        // only support HEX code input\n        const val = ev.target.value.slice(0, 7);\n        this.state.customHexColor = val;\n        if (!isColorValid(val)) ;\n        else {\n            this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };\n        }\n    }\n    addCustomColor(ev) {\n        if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {\n            return;\n        }\n        this.props.onColorPicked(toHex(this.state.customHexColor));\n    }\n    isSameColor(color1, color2) {\n        return isSameColor(color1, color2);\n    }\n}\n\ncss /* scss */ `\n  .o-color-picker-widget {\n    display: flex;\n    position: relative;\n    align-items: center;\n    height: 30px;\n\n    .o-color-picker-button-style {\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      margin: 2px;\n      padding: 3px;\n      border-radius: 2px;\n      cursor: pointer;\n      &:not([disabled]):hover {\n        background-color: rgba(0, 0, 0, 0.08);\n      }\n    }\n\n    .o-color-picker-button {\n      > span {\n        border-bottom: 4px solid;\n        height: 16px;\n        margin-top: 2px;\n        display: block;\n      }\n\n      &[disabled] {\n        pointer-events: none;\n        opacity: 0.3;\n      }\n    }\n  }\n`;\nclass ColorPickerWidget extends Component {\n    static template = \"o-spreadsheet-ColorPickerWidget\";\n    static props = {\n        currentColor: { type: String, optional: true },\n        toggleColorPicker: Function,\n        showColorPicker: Boolean,\n        onColorPicked: Function,\n        icon: String,\n        title: { type: String, optional: true },\n        disabled: { type: Boolean, optional: true },\n        dropdownMaxHeight: { type: Number, optional: true },\n        class: { type: String, optional: true },\n    };\n    static components = { ColorPicker };\n    colorPickerButtonRef = useRef(\"colorPickerButton\");\n    get iconStyle() {\n        return this.props.currentColor\n            ? `border-color: ${this.props.currentColor}`\n            : \"border-bottom-style: hidden\";\n    }\n    get colorPickerAnchorRect() {\n        const button = this.colorPickerButtonRef.el;\n        const buttonRect = button.getBoundingClientRect();\n        return {\n            x: buttonRect.x,\n            y: buttonRect.y,\n            width: buttonRect.width,\n            height: buttonRect.height,\n        };\n    }\n}\n\nconst TRANSPARENT_BACKGROUND_SVG = /*xml*/ `\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"10\" height=\"10\">\n  <path fill=\"#d9d9d9\" d=\"M5 5h5v5H5zH0V0h5\"/>\n</svg>\n`;\ncss /* scss */ `\n  .o-round-color-picker-button {\n    width: 18px;\n    height: 18px;\n    cursor: pointer;\n    border: 1px solid ${GRAY_300};\n    background-position: 1px 1px;\n    background-image: url(\"data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}\");\n  }\n`;\nclass RoundColorPicker extends Component {\n    static template = \"o-spreadsheet.RoundColorPicker\";\n    static components = { ColorPickerWidget, Section, ColorPicker };\n    static props = {\n        currentColor: { type: String, optional: true },\n        title: { type: String, optional: true },\n        onColorPicked: Function,\n        disableNoColor: { type: Boolean, optional: true },\n    };\n    colorPickerButtonRef = useRef(\"colorPickerButton\");\n    state;\n    setup() {\n        this.state = useState({ pickerOpened: false });\n        useExternalListener(window, \"click\", this.closePicker);\n    }\n    closePicker() {\n        this.state.pickerOpened = false;\n    }\n    togglePicker() {\n        this.state.pickerOpened = !this.state.pickerOpened;\n    }\n    onColorPicked(color) {\n        this.props.onColorPicked(color);\n        this.state.pickerOpened = false;\n    }\n    get colorPickerAnchorRect() {\n        const button = this.colorPickerButtonRef.el;\n        return getBoundingRectAsPOJO(button);\n    }\n    get buttonStyle() {\n        return cssPropertiesToCss({\n            background: this.props.currentColor,\n        });\n    }\n}\n\ncss /* scss */ `\n  .o-badge-selection {\n    gap: 1px;\n    button.o-button {\n      border-radius: 0;\n      &.selected {\n        color: ${GRAY_900};\n        border-color: ${ACTION_COLOR};\n        background: ${BADGE_SELECTED_COLOR};\n        font-weight: 600;\n      }\n\n      &:first-child {\n        border-radius: 4px 0 0 4px;\n      }\n      &:last-child {\n        border-radius: 0 4px 4px 0;\n      }\n    }\n  }\n`;\nclass BadgeSelection extends Component {\n    static template = \"o-spreadsheet.BadgeSelection\";\n    static props = {\n        choices: Array,\n        onChange: Function,\n        selectedValue: String,\n    };\n}\n\ncss /* scss */ `\n  .o-chart-title-designer {\n    > span {\n      height: 30px;\n    }\n\n    .o-divider {\n      border-right: 1px solid ${GRAY_300};\n      margin: 0px 4px;\n      height: 14px;\n    }\n\n    .o-menu-item-button.active {\n      background-color: #e6f4ea;\n      color: #188038;\n    }\n\n    .o-dropdown-content {\n      overflow-y: auto;\n      overflow-x: hidden;\n      padding: 2px;\n      z-index: 100;\n      box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);\n\n      .o-dropdown-line {\n        > span {\n          padding: 4px;\n        }\n      }\n    }\n  }\n`;\nclass ChartTitle extends Component {\n    static template = \"o-spreadsheet.ChartTitle\";\n    static components = { Section, ColorPickerWidget };\n    static props = {\n        title: { type: String, optional: true },\n        updateTitle: Function,\n        name: { type: String, optional: true },\n        toggleItalic: { type: Function, optional: true },\n        toggleBold: { type: Function, optional: true },\n        updateAlignment: { type: Function, optional: true },\n        updateColor: { type: Function, optional: true },\n        style: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        title: \"\",\n    };\n    openedEl = null;\n    setup() {\n        useExternalListener(window, \"click\", this.onExternalClick);\n    }\n    state = useState({\n        activeTool: \"\",\n    });\n    updateTitle(ev) {\n        this.props.updateTitle(ev.target.value);\n    }\n    toggleDropdownTool(tool, ev) {\n        const isOpen = this.state.activeTool === tool;\n        this.closeMenus();\n        this.state.activeTool = isOpen ? \"\" : tool;\n        this.openedEl = isOpen ? null : ev.target;\n    }\n    /**\n     * TODO: This is clearly not a goot way to handle external click, but\n     * we currently have no other way to do it ... Should be done in\n     * another task to handle the fact we want only one menu opened at a\n     * time with something like a menuStore ?\n     */\n    onExternalClick(ev) {\n        if (this.openedEl === ev.target) {\n            return;\n        }\n        this.closeMenus();\n    }\n    onColorPicked(color) {\n        this.props.updateColor?.(color);\n        this.closeMenus();\n    }\n    updateAlignment(aligment) {\n        this.props.updateAlignment?.(aligment);\n        this.closeMenus();\n    }\n    closeMenus() {\n        this.state.activeTool = \"\";\n        this.openedEl = null;\n    }\n}\n\nclass AxisDesignEditor extends Component {\n    static template = \"o-spreadsheet-AxisDesignEditor\";\n    static components = { Section, ChartTitle, BadgeSelection };\n    static props = { figureId: String, definition: Object, updateChart: Function, axesList: Array };\n    state = useState({ currentAxis: \"x\" });\n    get axisTitleStyle() {\n        const axisDesign = this.props.definition.axesDesign?.[this.state.currentAxis] ?? {};\n        return {\n            color: \"\",\n            align: \"center\",\n            ...axisDesign.title,\n        };\n    }\n    get badgeAxes() {\n        return this.props.axesList.map((axis) => ({ value: axis.id, label: axis.name }));\n    }\n    updateAxisTitleColor(color) {\n        const axesDesign = this.props.definition.axesDesign ?? {};\n        axesDesign[this.state.currentAxis] = {\n            ...axesDesign[this.state.currentAxis],\n            title: {\n                ...(axesDesign[this.state.currentAxis]?.title ?? {}),\n                color,\n            },\n        };\n        this.props.updateChart(this.props.figureId, { axesDesign });\n    }\n    toggleBoldAxisTitle() {\n        const axesDesign = this.props.definition.axesDesign ?? {};\n        const title = axesDesign[this.state.currentAxis]?.title ?? {};\n        axesDesign[this.state.currentAxis] = {\n            ...axesDesign[this.state.currentAxis],\n            title: {\n                ...title,\n                bold: !title?.bold,\n            },\n        };\n        this.props.updateChart(this.props.figureId, { axesDesign });\n    }\n    toggleItalicAxisTitle() {\n        const axesDesign = this.props.definition.axesDesign ?? {};\n        const title = axesDesign[this.state.currentAxis]?.title ?? {};\n        axesDesign[this.state.currentAxis] = {\n            ...axesDesign[this.state.currentAxis],\n            title: {\n                ...title,\n                italic: !title?.italic,\n            },\n        };\n        this.props.updateChart(this.props.figureId, { axesDesign });\n    }\n    updateAxisTitleAlignment(align) {\n        const axesDesign = this.props.definition.axesDesign ?? {};\n        const title = axesDesign[this.state.currentAxis]?.title ?? {};\n        axesDesign[this.state.currentAxis] = {\n            ...axesDesign[this.state.currentAxis],\n            title: {\n                ...title,\n                align,\n            },\n        };\n        this.props.updateChart(this.props.figureId, { axesDesign });\n    }\n    updateAxisEditor(ev) {\n        const axis = ev.target.value;\n        this.state.currentAxis = axis;\n    }\n    getAxisTitle() {\n        const axesDesign = this.props.definition.axesDesign ?? {};\n        return axesDesign[this.state.currentAxis]?.title.text || \"\";\n    }\n    updateAxisTitle(text) {\n        const axesDesign = this.props.definition.axesDesign ?? {};\n        axesDesign[this.state.currentAxis] = {\n            ...axesDesign[this.state.currentAxis],\n            title: {\n                ...axesDesign?.[this.state.currentAxis]?.title,\n                text,\n            },\n        };\n        this.props.updateChart(this.props.figureId, { axesDesign });\n    }\n}\n\nclass GeneralDesignEditor extends Component {\n    static template = \"o-spreadsheet-GeneralDesignEditor\";\n    static components = {\n        RoundColorPicker,\n        ChartTitle,\n        Section,\n        SidePanelCollapsible,\n    };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        slots: { type: Object, optional: true },\n    };\n    state;\n    setup() {\n        this.state = useState({\n            activeTool: \"\",\n        });\n    }\n    get title() {\n        return this.props.definition.title;\n    }\n    toggleDropdownTool(tool, ev) {\n        const isOpen = this.state.activeTool === tool;\n        this.state.activeTool = isOpen ? \"\" : tool;\n    }\n    updateBackgroundColor(color) {\n        this.props.updateChart(this.props.figureId, {\n            background: color,\n        });\n    }\n    updateTitle(newTitle) {\n        const title = { ...this.title, text: newTitle };\n        this.props.updateChart(this.props.figureId, { title });\n    }\n    get titleStyle() {\n        return {\n            align: \"left\",\n            ...this.title,\n        };\n    }\n    updateChartTitleColor(color) {\n        const title = { ...this.title, color };\n        this.props.updateChart(this.props.figureId, { title });\n        this.state.activeTool = \"\";\n    }\n    toggleBoldChartTitle() {\n        let title = this.title;\n        title = { ...title, bold: !title.bold };\n        this.props.updateChart(this.props.figureId, { title });\n    }\n    toggleItalicChartTitle() {\n        let title = this.title;\n        title = { ...title, italic: !title.italic };\n        this.props.updateChart(this.props.figureId, { title });\n    }\n    updateChartTitleAlignment(align) {\n        const title = { ...this.title, align };\n        this.props.updateChart(this.props.figureId, { title });\n        this.state.activeTool = \"\";\n    }\n}\n\nclass ChartWithAxisDesignPanel extends Component {\n    static template = \"o-spreadsheet-ChartWithAxisDesignPanel\";\n    static components = {\n        GeneralDesignEditor,\n        SidePanelCollapsible,\n        Section,\n        AxisDesignEditor,\n        RoundColorPicker,\n        Checkbox,\n        RadioSelection,\n    };\n    static props = {\n        figureId: String,\n        definition: Object,\n        canUpdateChart: Function,\n        updateChart: Function,\n    };\n    axisChoices = CHART_AXIS_CHOICES;\n    state = useState({ index: 0 });\n    get axesList() {\n        const { useLeftAxis, useRightAxis } = getDefinedAxis(this.props.definition);\n        let axes = [{ id: \"x\", name: _t(\"Horizontal axis\") }];\n        if (useLeftAxis) {\n            axes.push({ id: \"y\", name: useRightAxis ? _t(\"Left axis\") : _t(\"Vertical axis\") });\n        }\n        if (useRightAxis) {\n            axes.push({ id: \"y1\", name: useLeftAxis ? _t(\"Right axis\") : _t(\"Vertical axis\") });\n        }\n        return axes;\n    }\n    updateLegendPosition(ev) {\n        this.props.updateChart(this.props.figureId, {\n            legendPosition: ev.target.value,\n        });\n    }\n    getDataSeries() {\n        return this.props.definition.dataSets.map((d, i) => d.label ?? `${ChartTerms.Series} ${i + 1}`);\n    }\n    getPolynomialDegrees() {\n        return range(1, this.getMaxPolynomialDegree() + 1);\n    }\n    updateSerieEditor(ev) {\n        const chartId = this.props.figureId;\n        const selectedIndex = ev.target.selectedIndex;\n        const runtime = this.env.model.getters.getChartRuntime(chartId);\n        if (!runtime) {\n            return;\n        }\n        this.state.index = selectedIndex;\n    }\n    updateDataSeriesColor(color) {\n        const dataSets = [...this.props.definition.dataSets];\n        if (!dataSets?.[this.state.index]) {\n            return;\n        }\n        dataSets[this.state.index] = {\n            ...dataSets[this.state.index],\n            backgroundColor: color,\n        };\n        this.props.updateChart(this.props.figureId, { dataSets });\n    }\n    getDataSerieColor() {\n        const dataSets = this.props.definition.dataSets;\n        if (!dataSets?.[this.state.index]) {\n            return \"\";\n        }\n        const color = dataSets[this.state.index].backgroundColor;\n        return color ? toHex(color) : getNthColor(this.state.index, getColorsPalette(dataSets.length));\n    }\n    updateDataSeriesAxis(axis) {\n        const dataSets = [...this.props.definition.dataSets];\n        if (!dataSets?.[this.state.index]) {\n            return;\n        }\n        dataSets[this.state.index] = {\n            ...dataSets[this.state.index],\n            yAxisId: axis === \"left\" ? \"y\" : \"y1\",\n        };\n        this.props.updateChart(this.props.figureId, { dataSets });\n    }\n    getDataSerieAxis() {\n        const dataSets = this.props.definition.dataSets;\n        if (!dataSets?.[this.state.index]) {\n            return \"left\";\n        }\n        return dataSets[this.state.index].yAxisId === \"y1\" ? \"right\" : \"left\";\n    }\n    get canHaveTwoVerticalAxis() {\n        return \"horizontal\" in this.props.definition ? !this.props.definition.horizontal : true;\n    }\n    updateDataSeriesLabel(ev) {\n        const label = ev.target.value;\n        const dataSets = [...this.props.definition.dataSets];\n        if (!dataSets?.[this.state.index]) {\n            return;\n        }\n        dataSets[this.state.index] = {\n            ...dataSets[this.state.index],\n            label,\n        };\n        this.props.updateChart(this.props.figureId, { dataSets });\n    }\n    getDataSerieLabel() {\n        const dataSets = this.props.definition.dataSets;\n        return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];\n    }\n    updateShowValues(showValues) {\n        this.props.updateChart(this.props.figureId, { showValues });\n    }\n    toggleDataTrend(display) {\n        const dataSets = [...this.props.definition.dataSets];\n        if (!dataSets?.[this.state.index]) {\n            return;\n        }\n        dataSets[this.state.index] = {\n            ...dataSets[this.state.index],\n            trend: {\n                type: \"polynomial\",\n                order: 1,\n                ...dataSets[this.state.index].trend,\n                display,\n            },\n        };\n        this.props.updateChart(this.props.figureId, { dataSets });\n    }\n    getTrendLineConfiguration() {\n        const dataSets = this.props.definition.dataSets;\n        return dataSets?.[this.state.index]?.trend;\n    }\n    getTrendType(config) {\n        if (!config) {\n            return \"\";\n        }\n        return config.type === \"polynomial\" && config.order === 1 ? \"linear\" : config.type;\n    }\n    onChangeTrendType(ev) {\n        const type = ev.target.value;\n        let config;\n        switch (type) {\n            case \"linear\":\n            case \"polynomial\":\n                config = {\n                    type: \"polynomial\",\n                    order: type === \"linear\" ? 1 : this.getMaxPolynomialDegree(),\n                };\n                break;\n            case \"exponential\":\n            case \"logarithmic\":\n                config = { type };\n                break;\n            default:\n                return;\n        }\n        this.updateTrendLineValue(config);\n    }\n    onChangePolynomialDegree(ev) {\n        const element = ev.target;\n        this.updateTrendLineValue({ order: parseInt(element.value) });\n    }\n    getTrendLineColor() {\n        return this.getTrendLineConfiguration()?.color ?? setColorAlpha(this.getDataSerieColor(), 0.5);\n    }\n    updateTrendLineColor(color) {\n        this.updateTrendLineValue({ color });\n    }\n    updateTrendLineValue(config) {\n        const dataSets = [...this.props.definition.dataSets];\n        if (!dataSets?.[this.state.index]) {\n            return;\n        }\n        dataSets[this.state.index] = {\n            ...dataSets[this.state.index],\n            trend: {\n                ...dataSets[this.state.index].trend,\n                ...config,\n            },\n        };\n        this.props.updateChart(this.props.figureId, { dataSets });\n    }\n    getMaxPolynomialDegree() {\n        const runtime = this.env.model.getters.getChartRuntime(this.props.figureId);\n        return Math.min(10, runtime.chartJsConfig.data.datasets[this.state.index].data.length - 1);\n    }\n}\n\nclass ComboChartDesignPanel extends ChartWithAxisDesignPanel {\n    static template = \"o-spreadsheet-ComboChartDesignPanel\";\n    seriesTypeChoices = [\n        { value: \"bar\", label: _t(\"Bar\") },\n        { value: \"line\", label: _t(\"Line\") },\n    ];\n    updateDataSeriesType(type) {\n        const dataSets = [...this.props.definition.dataSets];\n        if (!dataSets?.[this.state.index]) {\n            return;\n        }\n        dataSets[this.state.index] = {\n            ...dataSets[this.state.index],\n            type,\n        };\n        this.props.updateChart(this.props.figureId, { dataSets });\n    }\n    getDataSeriesType() {\n        const dataSets = this.props.definition.dataSets;\n        if (!dataSets?.[this.state.index]) {\n            return \"bar\";\n        }\n        return dataSets[this.state.index].type ?? \"line\";\n    }\n}\n\nclass GaugeChartConfigPanel extends Component {\n    static template = \"o-spreadsheet-GaugeChartConfigPanel\";\n    static components = { ChartErrorSection, ChartDataSeries };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        canUpdateChart: Function,\n    };\n    state = useState({\n        dataRangeDispatchResult: undefined,\n    });\n    dataRange = this.props.definition.dataRange;\n    get configurationErrorMessages() {\n        const cancelledReasons = [...(this.state.dataRangeDispatchResult?.reasons || [])];\n        return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);\n    }\n    get isDataRangeInvalid() {\n        return !!this.state.dataRangeDispatchResult?.isCancelledBecause(\"InvalidGaugeDataRange\" /* CommandResult.InvalidGaugeDataRange */);\n    }\n    onDataRangeChanged(ranges) {\n        this.dataRange = ranges[0];\n        this.state.dataRangeDispatchResult = this.props.canUpdateChart(this.props.figureId, {\n            dataRange: this.dataRange,\n        });\n    }\n    updateDataRange() {\n        this.state.dataRangeDispatchResult = this.props.updateChart(this.props.figureId, {\n            dataRange: this.dataRange,\n        });\n    }\n    getDataRange() {\n        return { dataRange: this.dataRange || \"\" };\n    }\n}\n\ncss /* scss */ `\n  .o-gauge-color-set {\n    table {\n      table-layout: fixed;\n      margin-top: 2%;\n      display: table;\n      text-align: left;\n      font-size: 12px;\n      line-height: 18px;\n      width: 100%;\n      font-size: 12px;\n    }\n\n    td {\n      box-sizing: border-box;\n      height: 30px;\n      padding: 6px 0;\n    }\n    th.o-gauge-color-set-colorPicker {\n      width: 8%;\n    }\n    th.o-gauge-color-set-text {\n      width: 25%;\n    }\n    th.o-gauge-color-set-operator {\n      width: 10%;\n    }\n    th.o-gauge-color-set-value {\n      width: 22%;\n    }\n    th.o-gauge-color-set-type {\n      width: 30%;\n    }\n    input,\n    select {\n      width: 100%;\n      height: 100%;\n      box-sizing: border-box;\n    }\n  }\n`;\nclass GaugeChartDesignPanel extends Component {\n    static template = \"o-spreadsheet-GaugeChartDesignPanel\";\n    static components = {\n        SidePanelCollapsible,\n        Section,\n        RoundColorPicker,\n        GeneralDesignEditor,\n        ChartErrorSection,\n    };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        canUpdateChart: { type: Function, optional: true },\n    };\n    state;\n    setup() {\n        this.state = useState({\n            sectionRuleDispatchResult: undefined,\n            sectionRule: deepCopy(this.props.definition.sectionRule),\n        });\n    }\n    get designErrorMessages() {\n        const cancelledReasons = [...(this.state.sectionRuleDispatchResult?.reasons || [])];\n        return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);\n    }\n    isRangeMinInvalid() {\n        return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause(\"EmptyGaugeRangeMin\" /* CommandResult.EmptyGaugeRangeMin */) ||\n            this.state.sectionRuleDispatchResult?.isCancelledBecause(\"GaugeRangeMinNaN\" /* CommandResult.GaugeRangeMinNaN */) ||\n            this.state.sectionRuleDispatchResult?.isCancelledBecause(\"GaugeRangeMinBiggerThanRangeMax\" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */));\n    }\n    isRangeMaxInvalid() {\n        return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause(\"EmptyGaugeRangeMax\" /* CommandResult.EmptyGaugeRangeMax */) ||\n            this.state.sectionRuleDispatchResult?.isCancelledBecause(\"GaugeRangeMaxNaN\" /* CommandResult.GaugeRangeMaxNaN */) ||\n            this.state.sectionRuleDispatchResult?.isCancelledBecause(\"GaugeRangeMinBiggerThanRangeMax\" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */));\n    }\n    // ---------------------------------------------------------------------------\n    // COLOR_SECTION_TEMPLATE\n    // ---------------------------------------------------------------------------\n    get isLowerInflectionPointInvalid() {\n        return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause(\"GaugeLowerInflectionPointNaN\" /* CommandResult.GaugeLowerInflectionPointNaN */) ||\n            this.state.sectionRuleDispatchResult?.isCancelledBecause(\"GaugeLowerBiggerThanUpper\" /* CommandResult.GaugeLowerBiggerThanUpper */));\n    }\n    get isUpperInflectionPointInvalid() {\n        return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause(\"GaugeUpperInflectionPointNaN\" /* CommandResult.GaugeUpperInflectionPointNaN */) ||\n            this.state.sectionRuleDispatchResult?.isCancelledBecause(\"GaugeLowerBiggerThanUpper\" /* CommandResult.GaugeLowerBiggerThanUpper */));\n    }\n    updateSectionColor(target, color) {\n        const sectionRule = deepCopy(this.state.sectionRule);\n        sectionRule.colors[target] = color;\n        this.updateSectionRule(sectionRule);\n    }\n    updateSectionRule(sectionRule) {\n        this.state.sectionRuleDispatchResult = this.props.updateChart(this.props.figureId, {\n            sectionRule,\n        });\n        if (this.state.sectionRuleDispatchResult.isSuccessful) {\n            this.state.sectionRule = deepCopy(sectionRule);\n        }\n    }\n    canUpdateSectionRule(sectionRule) {\n        this.state.sectionRuleDispatchResult = this.props.canUpdateChart(this.props.figureId, {\n            sectionRule,\n        });\n    }\n}\n\nclass LineConfigPanel extends GenericChartConfigPanel {\n    static template = \"o-spreadsheet-LineConfigPanel\";\n    get canTreatLabelsAsText() {\n        const chart = this.env.model.getters.getChart(this.props.figureId);\n        if (chart && chart instanceof LineChart) {\n            return canChartParseLabels(chart.labelRange, this.env.model.getters);\n        }\n        return false;\n    }\n    get stackedLabel() {\n        const definition = this.props.definition;\n        return definition.fillArea\n            ? this.chartTerms.StackedAreaChart\n            : this.chartTerms.StackedLineChart;\n    }\n    getLabelRangeOptions() {\n        const options = super.getLabelRangeOptions();\n        if (this.canTreatLabelsAsText) {\n            options.push({\n                name: \"labelsAsText\",\n                value: this.props.definition.labelsAsText,\n                label: this.chartTerms.TreatLabelsAsText,\n                onChange: this.onUpdateLabelsAsText.bind(this),\n            });\n        }\n        return options;\n    }\n    onUpdateLabelsAsText(labelsAsText) {\n        this.props.updateChart(this.props.figureId, {\n            labelsAsText,\n        });\n    }\n    onUpdateStacked(stacked) {\n        this.props.updateChart(this.props.figureId, {\n            stacked,\n        });\n    }\n    onUpdateAggregated(aggregated) {\n        this.props.updateChart(this.props.figureId, {\n            aggregated,\n        });\n    }\n    onUpdateCumulative(cumulative) {\n        this.props.updateChart(this.props.figureId, {\n            cumulative,\n        });\n    }\n}\n\nclass PieChartDesignPanel extends Component {\n    static template = \"o-spreadsheet-PieChartDesignPanel\";\n    static components = {\n        GeneralDesignEditor,\n        Section,\n        Checkbox,\n    };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        canUpdateChart: { type: Function, optional: true },\n    };\n    updateLegendPosition(ev) {\n        this.props.updateChart(this.props.figureId, {\n            legendPosition: ev.target.value,\n        });\n    }\n}\n\nclass ScatterConfigPanel extends GenericChartConfigPanel {\n    static template = \"o-spreadsheet-ScatterConfigPanel\";\n    get canTreatLabelsAsText() {\n        const chart = this.env.model.getters.getChart(this.props.figureId);\n        if (chart && chart instanceof ScatterChart) {\n            return canChartParseLabels(chart.labelRange, this.env.model.getters);\n        }\n        return false;\n    }\n    onUpdateLabelsAsText(labelsAsText) {\n        this.props.updateChart(this.props.figureId, {\n            labelsAsText,\n        });\n    }\n    getLabelRangeOptions() {\n        const options = super.getLabelRangeOptions();\n        if (this.canTreatLabelsAsText) {\n            options.push({\n                name: \"labelsAsText\",\n                value: this.props.definition.labelsAsText,\n                label: this.chartTerms.TreatLabelsAsText,\n                onChange: this.onUpdateLabelsAsText.bind(this),\n            });\n        }\n        return options;\n    }\n}\n\nclass ScorecardChartConfigPanel extends Component {\n    static template = \"o-spreadsheet-ScorecardChartConfigPanel\";\n    static components = { SelectionInput, ChartErrorSection, Section };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        canUpdateChart: Function,\n    };\n    state = useState({\n        keyValueDispatchResult: undefined,\n        baselineDispatchResult: undefined,\n    });\n    keyValue = this.props.definition.keyValue;\n    baseline = this.props.definition.baseline;\n    get errorMessages() {\n        const cancelledReasons = [\n            ...(this.state.keyValueDispatchResult?.reasons || []),\n            ...(this.state.baselineDispatchResult?.reasons || []),\n        ];\n        return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);\n    }\n    get isKeyValueInvalid() {\n        return !!this.state.keyValueDispatchResult?.isCancelledBecause(\"InvalidScorecardKeyValue\" /* CommandResult.InvalidScorecardKeyValue */);\n    }\n    get isBaselineInvalid() {\n        return !!this.state.keyValueDispatchResult?.isCancelledBecause(\"InvalidScorecardBaseline\" /* CommandResult.InvalidScorecardBaseline */);\n    }\n    onKeyValueRangeChanged(ranges) {\n        this.keyValue = ranges[0];\n        this.state.keyValueDispatchResult = this.props.canUpdateChart(this.props.figureId, {\n            keyValue: this.keyValue,\n        });\n    }\n    updateKeyValueRange() {\n        this.state.keyValueDispatchResult = this.props.updateChart(this.props.figureId, {\n            keyValue: this.keyValue,\n        });\n    }\n    getKeyValueRange() {\n        return this.keyValue || \"\";\n    }\n    onBaselineRangeChanged(ranges) {\n        this.baseline = ranges[0];\n        this.state.baselineDispatchResult = this.props.canUpdateChart(this.props.figureId, {\n            baseline: this.baseline,\n        });\n    }\n    updateBaselineRange() {\n        this.state.baselineDispatchResult = this.props.updateChart(this.props.figureId, {\n            baseline: this.baseline,\n        });\n    }\n    getBaselineRange() {\n        return this.baseline || \"\";\n    }\n    updateBaselineMode(ev) {\n        this.props.updateChart(this.props.figureId, { baselineMode: ev.target.value });\n    }\n}\n\nclass ScorecardChartDesignPanel extends Component {\n    static template = \"o-spreadsheet-ScorecardChartDesignPanel\";\n    static components = {\n        GeneralDesignEditor,\n        RoundColorPicker,\n        SidePanelCollapsible,\n        Section,\n        Checkbox,\n    };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        canUpdateChart: { type: Function, optional: true },\n    };\n    get colorsSectionTitle() {\n        return this.props.definition.baselineMode === \"progress\"\n            ? _t(\"Progress bar colors\")\n            : _t(\"Baseline colors\");\n    }\n    get humanizeNumbersLabel() {\n        return _t(\"Humanize numbers\");\n    }\n    updateHumanizeNumbers(humanize) {\n        this.props.updateChart(this.props.figureId, { humanize });\n    }\n    translate(term) {\n        return _t(term);\n    }\n    updateBaselineDescr(ev) {\n        this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });\n    }\n    setColor(color, colorPickerId) {\n        switch (colorPickerId) {\n            case \"backgroundColor\":\n                this.props.updateChart(this.props.figureId, { background: color });\n                break;\n            case \"baselineColorDown\":\n                this.props.updateChart(this.props.figureId, { baselineColorDown: color });\n                break;\n            case \"baselineColorUp\":\n                this.props.updateChart(this.props.figureId, { baselineColorUp: color });\n                break;\n        }\n    }\n}\n\nclass WaterfallChartDesignPanel extends Component {\n    static template = \"o-spreadsheet-WaterfallChartDesignPanel\";\n    static components = {\n        GeneralDesignEditor,\n        Checkbox,\n        SidePanelCollapsible,\n        Section,\n        RoundColorPicker,\n        AxisDesignEditor,\n        RadioSelection,\n    };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        canUpdateChart: { type: Function, optional: true },\n    };\n    axisChoices = CHART_AXIS_CHOICES;\n    onUpdateShowSubTotals(showSubTotals) {\n        this.props.updateChart(this.props.figureId, { showSubTotals });\n    }\n    onUpdateShowConnectorLines(showConnectorLines) {\n        this.props.updateChart(this.props.figureId, { showConnectorLines });\n    }\n    onUpdateFirstValueAsSubtotal(firstValueAsSubtotal) {\n        this.props.updateChart(this.props.figureId, { firstValueAsSubtotal });\n    }\n    updateColor(colorName, color) {\n        this.props.updateChart(this.props.figureId, { [colorName]: color });\n    }\n    get axesList() {\n        return [\n            { id: \"x\", name: _t(\"Horizontal axis\") },\n            { id: \"y\", name: _t(\"Vertical axis\") },\n        ];\n    }\n    get positiveValuesColor() {\n        return (this.props.definition.positiveValuesColor ||\n            CHART_WATERFALL_POSITIVE_COLOR);\n    }\n    get negativeValuesColor() {\n        return (this.props.definition.negativeValuesColor ||\n            CHART_WATERFALL_NEGATIVE_COLOR);\n    }\n    get subTotalValuesColor() {\n        return (this.props.definition.subTotalValuesColor ||\n            CHART_WATERFALL_SUBTOTAL_COLOR);\n    }\n    updateLegendPosition(ev) {\n        this.props.updateChart(this.props.figureId, {\n            legendPosition: ev.target.value,\n        });\n    }\n    updateVerticalAxisPosition(value) {\n        this.props.updateChart(this.props.figureId, {\n            verticalAxisPosition: value,\n        });\n    }\n    updateShowValues(showValues) {\n        this.props.updateChart(this.props.figureId, { showValues });\n    }\n}\n\nconst chartSidePanelComponentRegistry = new Registry();\nchartSidePanelComponentRegistry\n    .add(\"line\", {\n    configuration: LineConfigPanel,\n    design: ChartWithAxisDesignPanel,\n})\n    .add(\"scatter\", {\n    configuration: ScatterConfigPanel,\n    design: ChartWithAxisDesignPanel,\n})\n    .add(\"bar\", {\n    configuration: BarConfigPanel,\n    design: ChartWithAxisDesignPanel,\n})\n    .add(\"combo\", {\n    configuration: GenericChartConfigPanel,\n    design: ComboChartDesignPanel,\n})\n    .add(\"pie\", {\n    configuration: GenericChartConfigPanel,\n    design: PieChartDesignPanel,\n})\n    .add(\"gauge\", {\n    configuration: GaugeChartConfigPanel,\n    design: GaugeChartDesignPanel,\n})\n    .add(\"scorecard\", {\n    configuration: ScorecardChartConfigPanel,\n    design: ScorecardChartDesignPanel,\n})\n    .add(\"waterfall\", {\n    configuration: GenericChartConfigPanel,\n    design: WaterfallChartDesignPanel,\n})\n    .add(\"pyramid\", {\n    configuration: GenericChartConfigPanel,\n    design: ChartWithAxisDesignPanel,\n});\n\ncss /* scss */ `\n  .o-section .o-input.o-type-selector {\n    height: 30px;\n    padding-left: 35px;\n    padding-top: 5px;\n  }\n  .o-type-selector-preview {\n    left: 5px;\n    top: 3px;\n    .o-chart-preview {\n      width: 24px;\n      height: 24px;\n    }\n  }\n\n  .o-popover .o-chart-select-popover {\n    box-sizing: border-box;\n    background: #fff;\n    .o-chart-type-item {\n      cursor: pointer;\n      padding: 3px 6px;\n      margin: 1px 2px;\n      &.selected,\n      &:hover {\n        border: 1px solid ${ACTION_COLOR};\n        background: ${BADGE_SELECTED_COLOR};\n        padding: 2px 5px;\n      }\n      .o-chart-preview {\n        width: 48px;\n        height: 48px;\n      }\n    }\n  }\n`;\nclass ChartTypePicker extends Component {\n    static template = \"o-spreadsheet-ChartTypePicker\";\n    static components = { Section, Popover };\n    static props = { figureId: String, chartPanelStore: Object };\n    categories = chartCategories;\n    chartTypeByCategories = {};\n    popoverRef = useRef(\"popoverRef\");\n    selectRef = useRef(\"selectRef\");\n    state = useState({ popoverProps: undefined, popoverStyle: \"\" });\n    setup() {\n        useExternalListener(window, \"pointerdown\", this.onExternalClick, { capture: true });\n        for (const subtypeProperties of chartSubtypeRegistry.getAll()) {\n            if (this.chartTypeByCategories[subtypeProperties.category]) {\n                this.chartTypeByCategories[subtypeProperties.category].push(subtypeProperties);\n            }\n            else {\n                this.chartTypeByCategories[subtypeProperties.category] = [subtypeProperties];\n            }\n        }\n    }\n    onExternalClick(ev) {\n        if (isChildEvent(this.popoverRef.el?.parentElement, ev) ||\n            isChildEvent(this.selectRef.el, ev)) {\n            return;\n        }\n        this.closePopover();\n    }\n    onTypeChange(type) {\n        this.props.chartPanelStore.changeChartType(this.props.figureId, type);\n        this.closePopover();\n    }\n    getChartDefinition(figureId) {\n        return this.env.model.getters.getChartDefinition(figureId);\n    }\n    getSelectedChartSubtypeProperties() {\n        const definition = this.getChartDefinition(this.props.figureId);\n        const matchedChart = chartSubtypeRegistry\n            .getAll()\n            .find((c) => c.matcher?.(definition) || false);\n        return matchedChart || chartSubtypeRegistry.get(definition.type);\n    }\n    onPointerDown(ev) {\n        if (this.state.popoverProps) {\n            this.closePopover();\n            return;\n        }\n        const target = ev.currentTarget;\n        const { bottom, right, width } = target.getBoundingClientRect();\n        this.state.popoverProps = {\n            anchorRect: { x: right, y: bottom, width: 0, height: 0 },\n            positioning: \"TopRight\",\n            verticalOffset: 0,\n        };\n        this.state.popoverStyle = cssPropertiesToCss({ width: `${width}px` });\n    }\n    closePopover() {\n        this.state.popoverProps = undefined;\n    }\n}\n\nclass MainChartPanelStore extends SpreadsheetStore {\n    mutators = [\"activatePanel\", \"changeChartType\"];\n    panel = \"configuration\";\n    creationContext = {};\n    activatePanel(panel) {\n        this.panel = panel;\n    }\n    changeChartType(figureId, newDisplayType) {\n        this.creationContext = {\n            ...this.creationContext,\n            ...this.getters.getContextCreationChart(figureId),\n        };\n        const sheetId = this.getters.getFigureSheetId(figureId);\n        if (!sheetId) {\n            return;\n        }\n        const definition = this.getChartDefinitionFromContextCreation(figureId, newDisplayType);\n        this.model.dispatch(\"UPDATE_CHART\", {\n            definition,\n            id: figureId,\n            sheetId,\n        });\n    }\n    getChartDefinitionFromContextCreation(figureId, newDisplayType) {\n        const newChartInfo = chartSubtypeRegistry.get(newDisplayType);\n        const ChartClass = chartRegistry.get(newChartInfo.chartType);\n        const contextCreation = {\n            ...this.creationContext,\n            ...this.getters.getContextCreationChart(figureId),\n        };\n        return {\n            ...ChartClass.getChartDefinitionFromContextCreation(contextCreation),\n            ...newChartInfo.subtypeDefinition,\n        };\n    }\n}\n\ncss /* scss */ `\n  .o-chart {\n    .o-panel {\n      display: flex;\n      .o-panel-element {\n        flex: 1 0 auto;\n        padding: 8px 0px;\n        text-align: center;\n        cursor: pointer;\n        border-right: 1px solid ${GRAY_300};\n\n        &.inactive {\n          color: ${TEXT_BODY};\n          background-color: ${GRAY_100};\n          border-bottom: 1px solid ${GRAY_300};\n        }\n\n        &:not(.inactive) {\n          color: ${TEXT_HEADING};\n          border-bottom: 1px solid #fff;\n        }\n\n        .fa {\n          margin-right: 4px;\n        }\n      }\n      .o-panel-element:last-child {\n        border-right: none;\n      }\n    }\n  }\n`;\nclass ChartPanel extends Component {\n    static template = \"o-spreadsheet-ChartPanel\";\n    static components = { Section, ChartTypePicker };\n    static props = { onCloseSidePanel: Function, figureId: String };\n    store;\n    get figureId() {\n        return this.props.figureId;\n    }\n    setup() {\n        this.store = useLocalStore(MainChartPanelStore);\n    }\n    updateChart(figureId, updateDefinition) {\n        if (figureId !== this.figureId) {\n            return;\n        }\n        const definition = {\n            ...this.getChartDefinition(this.figureId),\n            ...updateDefinition,\n        };\n        return this.env.model.dispatch(\"UPDATE_CHART\", {\n            definition,\n            id: figureId,\n            sheetId: this.env.model.getters.getFigureSheetId(figureId),\n        });\n    }\n    canUpdateChart(figureId, updateDefinition) {\n        if (figureId !== this.figureId || !this.env.model.getters.isChartDefined(figureId)) {\n            return;\n        }\n        const definition = {\n            ...this.getChartDefinition(this.figureId),\n            ...updateDefinition,\n        };\n        return this.env.model.canDispatch(\"UPDATE_CHART\", {\n            definition,\n            id: figureId,\n            sheetId: this.env.model.getters.getFigureSheetId(figureId),\n        });\n    }\n    onTypeChange(type) {\n        if (!this.figureId) {\n            return;\n        }\n        this.store.changeChartType(this.figureId, type);\n    }\n    get chartPanel() {\n        if (!this.figureId) {\n            throw new Error(\"Chart not defined.\");\n        }\n        const type = this.env.model.getters.getChartType(this.figureId);\n        if (!type) {\n            throw new Error(\"Chart not defined.\");\n        }\n        const chartPanel = chartSidePanelComponentRegistry.get(type);\n        if (!chartPanel) {\n            throw new Error(`Component is not defined for type ${type}`);\n        }\n        return chartPanel;\n    }\n    getChartDefinition(figureId) {\n        return this.env.model.getters.getChartDefinition(figureId);\n    }\n}\n\nclass NotificationStore {\n    mutators = [\n        \"notifyUser\",\n        \"raiseError\",\n        \"askConfirmation\",\n        \"updateNotificationCallbacks\",\n    ];\n    notifyUser = (notification) => window.alert(notification.text);\n    askConfirmation = (content, confirm, cancel) => {\n        if (window.confirm(content)) {\n            confirm();\n        }\n        else {\n            cancel?.();\n        }\n    };\n    raiseError = (text, callback) => {\n        window.alert(text);\n        callback?.();\n    };\n    updateNotificationCallbacks(methods) {\n        this.notifyUser = methods.notifyUser || this.notifyUser;\n        this.raiseError = methods.raiseError || this.raiseError;\n        this.askConfirmation = methods.askConfirmation || this.askConfirmation;\n    }\n}\n\nclass AbstractComposerStore extends SpreadsheetStore {\n    mutators = [\n        \"startEdition\",\n        \"setCurrentContent\",\n        \"stopEdition\",\n        \"stopComposerRangeSelection\",\n        \"cancelEdition\",\n        \"cycleReferences\",\n        \"changeComposerCursorSelection\",\n        \"replaceComposerCursorSelection\",\n    ];\n    col = 0;\n    row = 0;\n    editionMode = \"inactive\";\n    sheetId = \"\";\n    _currentContent = \"\";\n    currentTokens = [];\n    selectionStart = 0;\n    selectionEnd = 0;\n    initialContent = \"\";\n    colorIndexByRange = {};\n    notificationStore = this.get(NotificationStore);\n    highlightStore = this.get(HighlightStore);\n    constructor(get) {\n        super(get);\n        this.highlightStore.register(this);\n        this.onDispose(() => {\n            this.highlightStore.unRegister(this);\n        });\n    }\n    handleEvent(event) {\n        const sheetId = this.getters.getActiveSheetId();\n        let unboundedZone;\n        if (event.options.unbounded) {\n            unboundedZone = this.getters.getUnboundedZone(sheetId, event.anchor.zone);\n        }\n        else {\n            unboundedZone = event.anchor.zone;\n        }\n        switch (event.mode) {\n            case \"newAnchor\":\n                if (this.editionMode === \"selecting\") {\n                    this.insertSelectedRange(unboundedZone);\n                }\n                break;\n            default:\n                if (this.editionMode === \"selecting\") {\n                    this.replaceSelectedRange(unboundedZone);\n                }\n                else {\n                    this.updateComposerRange(event.previousAnchor.zone, unboundedZone);\n                }\n                break;\n        }\n    }\n    changeComposerCursorSelection(start, end) {\n        if (!this.isSelectionValid(this._currentContent.length, start, end)) {\n            return;\n        }\n        this.selectionStart = start;\n        this.selectionEnd = end;\n    }\n    stopComposerRangeSelection() {\n        if (this.isSelectingRange) {\n            this.editionMode = \"editing\";\n        }\n    }\n    startEdition(text, selection) {\n        if (selection) {\n            const content = text || this.getComposerContent(this.getters.getActivePosition());\n            const validSelection = this.isSelectionValid(content.length, selection.start, selection.end);\n            if (!validSelection) {\n                return;\n            }\n        }\n        const { col, row } = this.getters.getActivePosition();\n        this.model.dispatch(\"SELECT_FIGURE\", { id: null });\n        this.model.dispatch(\"SCROLL_TO_CELL\", { col, row });\n        if (this.editionMode !== \"inactive\" && text) {\n            this.setContent(text, selection);\n        }\n        else {\n            this._startEdition(text, selection);\n        }\n        this.updateRangeColor();\n    }\n    cancelEdition() {\n        this.cancelEditionAndActivateSheet();\n        this.resetContent();\n    }\n    setCurrentContent(content, selection) {\n        if (selection && !this.isSelectionValid(content.length, selection.start, selection.end)) {\n            return;\n        }\n        this.setContent(content, selection, true);\n        this.updateRangeColor();\n    }\n    replaceComposerCursorSelection(text) {\n        this.replaceSelection(text);\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SELECT_FIGURE\":\n                if (cmd.id) {\n                    this.cancelEditionAndActivateSheet();\n                    this.resetContent();\n                }\n                break;\n            case \"START_CHANGE_HIGHLIGHT\":\n                const { left, top } = cmd.zone;\n                // changing the highlight can conflit with the 'selecting' mode\n                if (this.isSelectingRange) {\n                    this.editionMode = \"editing\";\n                }\n                this.model.selection.resetAnchor(this, {\n                    cell: { col: left, row: top },\n                    zone: cmd.zone,\n                });\n                break;\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    get currentContent() {\n        if (this.editionMode === \"inactive\") {\n            return this.getComposerContent(this.getters.getActivePosition());\n        }\n        return this._currentContent;\n    }\n    get composerSelection() {\n        return {\n            start: this.selectionStart,\n            end: this.selectionEnd,\n        };\n    }\n    get isSelectingRange() {\n        return this.editionMode === \"selecting\";\n    }\n    get showSelectionIndicator() {\n        return this.isSelectingRange && this.canStartComposerRangeSelection();\n    }\n    /**\n     * Return the (enriched) token just before the cursor.\n     */\n    get tokenAtCursor() {\n        const start = Math.min(this.selectionStart, this.selectionEnd);\n        const end = Math.max(this.selectionStart, this.selectionEnd);\n        if (start === end && end === 0) {\n            return undefined;\n        }\n        else {\n            return this.currentTokens.find((t) => t.start <= start && t.end >= end);\n        }\n    }\n    cycleReferences() {\n        const locale = this.getters.getLocale();\n        const updated = cycleFixedReference(this.composerSelection, this._currentContent, locale);\n        if (updated === undefined) {\n            return;\n        }\n        this.setCurrentContent(updated.content, updated.selection);\n    }\n    isSelectionValid(length, start, end) {\n        return start >= 0 && start <= length && end >= 0 && end <= length;\n    }\n    /**\n     * Enable the selecting mode\n     */\n    startComposerRangeSelection() {\n        if (this.sheetId === this.getters.getActiveSheetId()) {\n            const zone = positionToZone({ col: this.col, row: this.row });\n            this.model.selection.resetAnchor(this, {\n                cell: { col: this.col, row: this.row },\n                zone,\n            });\n        }\n        this.editionMode = \"selecting\";\n    }\n    /**\n     * start the edition of a cell\n     * @param str the key that is used to start the edition if it is a \"content\" key like a letter or number\n     * @param selection\n     * @private\n     */\n    _startEdition(str, selection) {\n        const evaluatedCell = this.getters.getActiveCell();\n        const locale = this.getters.getLocale();\n        if (str && evaluatedCell.format?.includes(\"%\") && isNumber(str, locale)) {\n            selection = selection || { start: str.length, end: str.length };\n            str = `${str}%`;\n        }\n        const { col, row, sheetId } = this.getters.getActivePosition();\n        this.col = col;\n        this.sheetId = sheetId;\n        this.row = row;\n        this.initialContent = this.getComposerContent({ sheetId, col, row });\n        this.editionMode = \"editing\";\n        this.setContent(str || this.initialContent, selection);\n        this.colorIndexByRange = {};\n        const zone = positionToZone({ col: this.col, row: this.row });\n        this.model.selection.capture(this, { cell: { col: this.col, row: this.row }, zone }, {\n            handleEvent: this.handleEvent.bind(this),\n            release: () => {\n                this._stopEdition();\n            },\n        });\n    }\n    _stopEdition() {\n        if (this.editionMode !== \"inactive\") {\n            this.cancelEditionAndActivateSheet();\n            let content = this.getCurrentCanonicalContent();\n            const didChange = this.initialContent !== content;\n            if (!didChange) {\n                return;\n            }\n            if (content) {\n                if (content.startsWith(\"=\")) {\n                    const left = this.currentTokens.filter((t) => t.type === \"LEFT_PAREN\").length;\n                    const right = this.currentTokens.filter((t) => t.type === \"RIGHT_PAREN\").length;\n                    const missing = left - right;\n                    if (missing > 0) {\n                        content += concat(new Array(missing).fill(\")\"));\n                    }\n                }\n            }\n            this.confirmEdition(content);\n        }\n    }\n    getCurrentCanonicalContent() {\n        return canonicalizeNumberContent(this._currentContent, this.getters.getLocale());\n    }\n    cancelEditionAndActivateSheet() {\n        if (this.editionMode === \"inactive\") {\n            return;\n        }\n        this._cancelEdition();\n        const sheetId = this.getters.getActiveSheetId();\n        if (sheetId !== this.sheetId) {\n            this.model.dispatch(\"ACTIVATE_SHEET\", {\n                sheetIdFrom: this.getters.getActiveSheetId(),\n                sheetIdTo: this.sheetId,\n            });\n        }\n    }\n    _cancelEdition() {\n        if (this.editionMode === \"inactive\") {\n            return;\n        }\n        this.editionMode = \"inactive\";\n        this.model.selection.release(this);\n        this.colorIndexByRange = {};\n    }\n    /**\n     * Reset the current content to the active cell content\n     */\n    resetContent() {\n        this.setContent(this.initialContent || \"\");\n    }\n    setContent(text, selection, raise) {\n        const isNewCurrentContent = this._currentContent !== text;\n        this._currentContent = text;\n        if (selection) {\n            this.selectionStart = selection.start;\n            this.selectionEnd = selection.end;\n        }\n        else {\n            this.selectionStart = this.selectionEnd = text.length;\n        }\n        if (isNewCurrentContent || this.editionMode !== \"inactive\") {\n            const locale = this.getters.getLocale();\n            this.currentTokens = text.startsWith(\"=\") ? composerTokenize(text, locale) : [];\n            if (this.currentTokens.length > 100) {\n                if (raise) {\n                    this.notificationStore.raiseError(_t(\"This formula has over 100 parts. It can't be processed properly, consider splitting it into multiple cells\"));\n                }\n            }\n        }\n        if (this.canStartComposerRangeSelection()) {\n            this.startComposerRangeSelection();\n        }\n    }\n    getAutoCompleteProviders() {\n        return autoCompleteProviders.getAll();\n    }\n    insertSelectedRange(zone) {\n        // infer if range selected or selecting range from cursor position\n        const start = Math.min(this.selectionStart, this.selectionEnd);\n        const ref = this.getZoneReference(zone);\n        if (this.canStartComposerRangeSelection()) {\n            this.insertText(ref, start);\n        }\n        else {\n            this.insertText(\",\" + ref, start);\n        }\n    }\n    /**\n     * Replace the current reference selected by the new one.\n     * */\n    replaceSelectedRange(zone) {\n        const ref = this.getZoneReference(zone);\n        const currentToken = this.tokenAtCursor;\n        const start = currentToken?.type === \"REFERENCE\" ? currentToken.start : this.selectionStart;\n        this.replaceText(ref, start, this.selectionEnd);\n    }\n    /**\n     * Replace the reference of the old zone by the new one.\n     */\n    updateComposerRange(oldZone, newZone) {\n        const activeSheetId = this.getters.getActiveSheetId();\n        const tokentAtCursor = this.tokenAtCursor;\n        const tokens = tokentAtCursor ? [tokentAtCursor, ...this.currentTokens] : this.currentTokens;\n        const previousRefToken = tokens\n            .filter((token) => token.type === \"REFERENCE\")\n            .find((token) => {\n            const { xc, sheetName: sheet } = splitReference(token.value);\n            const sheetName = sheet || this.getters.getSheetName(this.sheetId);\n            if (this.getters.getSheetName(activeSheetId) !== sheetName) {\n                return false;\n            }\n            const refRange = this.getters.getRangeFromSheetXC(activeSheetId, xc);\n            return isEqual(this.getters.expandZone(activeSheetId, refRange.zone), oldZone);\n        });\n        if (!previousRefToken) {\n            return;\n        }\n        const previousRange = this.getters.getRangeFromSheetXC(activeSheetId, previousRefToken.value);\n        this.selectionStart = previousRefToken.start;\n        this.selectionEnd = this.selectionStart + previousRefToken.value.length;\n        const newRange = this.getters.getRangeFromZone(activeSheetId, newZone);\n        const newRef = this.getRangeReference(newRange, previousRange.parts);\n        this.replaceSelection(newRef);\n    }\n    getZoneReference(zone) {\n        const inputSheetId = this.sheetId;\n        const sheetId = this.getters.getActiveSheetId();\n        const range = this.getters.getRangeFromZone(sheetId, zone);\n        return this.getters.getSelectionRangeString(range, inputSheetId);\n    }\n    getRangeReference(range, fixedParts) {\n        let _fixedParts = [...fixedParts];\n        const newRange = range.clone({ parts: _fixedParts });\n        return this.getters.getSelectionRangeString(newRange, this.sheetId);\n    }\n    /**\n     * Replace the current selection by a new text.\n     * The cursor is then set at the end of the text.\n     */\n    replaceSelection(text) {\n        const start = Math.min(this.selectionStart, this.selectionEnd);\n        const end = Math.max(this.selectionStart, this.selectionEnd);\n        this.replaceText(text, start, end);\n    }\n    replaceText(text, start, end) {\n        this._currentContent =\n            this._currentContent.slice(0, start) +\n                this._currentContent.slice(end, this._currentContent.length);\n        this.insertText(text, start);\n    }\n    /**\n     * Insert a text at the given position.\n     * The cursor is then set at the end of the text.\n     */\n    insertText(text, start) {\n        const content = this._currentContent.slice(0, start) + text + this._currentContent.slice(start);\n        const end = start + text.length;\n        this.setCurrentContent(content, { start: end, end });\n    }\n    updateRangeColor() {\n        if (!this._currentContent.startsWith(\"=\") || this.editionMode === \"inactive\") {\n            return;\n        }\n        const editionSheetId = this.sheetId;\n        const XCs = this.getReferencedRanges().map((range) => this.getters.getRangeString(range, editionSheetId));\n        const colorsToKeep = {};\n        for (const xc of XCs) {\n            if (this.colorIndexByRange[xc] !== undefined) {\n                colorsToKeep[xc] = this.colorIndexByRange[xc];\n            }\n        }\n        const usedIndexes = new Set(Object.values(colorsToKeep));\n        let currentIndex = 0;\n        const nextIndex = () => {\n            while (usedIndexes.has(currentIndex))\n                currentIndex++;\n            usedIndexes.add(currentIndex);\n            return currentIndex;\n        };\n        for (const xc of XCs) {\n            const colorIndex = xc in colorsToKeep ? colorsToKeep[xc] : nextIndex();\n            colorsToKeep[xc] = colorIndex;\n        }\n        this.colorIndexByRange = colorsToKeep;\n    }\n    /**\n     * Highlight all ranges that can be found in the composer content.\n     */\n    get highlights() {\n        if (!this.currentContent.startsWith(\"=\") || this.editionMode === \"inactive\") {\n            return [];\n        }\n        const editionSheetId = this.sheetId;\n        const rangeColor = (rangeString) => {\n            const colorIndex = this.colorIndexByRange[rangeString];\n            return colors$1[colorIndex % colors$1.length];\n        };\n        return this.getReferencedRanges().map((range) => {\n            const rangeString = this.getters.getRangeString(range, editionSheetId);\n            const { numberOfRows, numberOfCols } = zoneToDimension(range.zone);\n            const zone = numberOfRows * numberOfCols === 1\n                ? this.getters.expandZone(range.sheetId, range.zone)\n                : range.zone;\n            return {\n                zone,\n                color: rangeColor(rangeString),\n                sheetId: range.sheetId,\n                interactive: true,\n            };\n        });\n    }\n    /**\n     * Return ranges currently referenced in the composer\n     */\n    getReferencedRanges() {\n        const editionSheetId = this.sheetId;\n        const referenceRanges = this.currentTokens\n            .filter((token) => token.type === \"REFERENCE\")\n            .map((token) => this.getters.getRangeFromSheetXC(editionSheetId, token.value));\n        return referenceRanges.filter((range) => !range.invalidSheetName && !range.invalidXc);\n    }\n    get autocompleteProvider() {\n        const content = this.currentContent;\n        const tokenAtCursor = content.startsWith(\"=\")\n            ? this.tokenAtCursor\n            : { type: \"STRING\", value: content };\n        if (this.editionMode === \"inactive\" ||\n            !tokenAtCursor ||\n            [\"TRUE\", \"FALSE\"].includes(tokenAtCursor.value.toUpperCase()) ||\n            !(this.canStartComposerRangeSelection() ||\n                [\"SYMBOL\", \"STRING\", \"UNKNOWN\"].includes(tokenAtCursor.type))) {\n            return;\n        }\n        const thisCtx = { composer: this, getters: this.getters };\n        const providersDefinitions = this.getAutoCompleteProviders();\n        const providers = providersDefinitions\n            .sort((a, b) => (a.sequence ?? Infinity) - (b.sequence ?? Infinity))\n            .map((provider) => ({\n            ...provider,\n            getProposals: provider.getProposals.bind(thisCtx, tokenAtCursor, content),\n            selectProposal: provider.selectProposal.bind(thisCtx, tokenAtCursor),\n        }));\n        for (const provider of providers) {\n            let proposals = provider.getProposals();\n            const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);\n            // remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string\n            const searchTerm = tokenAtCursor.value.replace(/[ ,\\(\\)]/g, \"\");\n            if (exactMatch && this._currentContent !== this.initialContent) {\n                // this means the user has chosen a proposal\n                return;\n            }\n            if (searchTerm &&\n                proposals &&\n                ![\"ARG_SEPARATOR\", \"LEFT_PAREN\", \"OPERATOR\"].includes(tokenAtCursor.type)) {\n                const filteredProposals = fuzzyLookup(searchTerm, proposals, (p) => p.fuzzySearchKey || p.text);\n                if (!exactMatch || filteredProposals.length > 1) {\n                    proposals = filteredProposals;\n                }\n            }\n            if (provider.maxDisplayedProposals) {\n                proposals = proposals?.slice(0, provider.maxDisplayedProposals);\n            }\n            if (proposals?.length) {\n                return {\n                    proposals,\n                    selectProposal: provider.selectProposal,\n                    autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,\n                };\n            }\n        }\n        return;\n    }\n    /**\n     * Function used to determine when composer selection can start.\n     * Three conditions are necessary:\n     * - the previous token is among [\"ARG_SEPARATOR\", \"LEFT_PAREN\", \"OPERATOR\"], and is not a postfix unary operator\n     * - the next token is missing or is among [\"ARG_SEPARATOR\", \"RIGHT_PAREN\", \"OPERATOR\"]\n     * - Previous and next tokens can be separated by spaces\n     */\n    canStartComposerRangeSelection() {\n        if (this._currentContent.startsWith(\"=\")) {\n            const tokenAtCursor = this.tokenAtCursor;\n            if (!tokenAtCursor) {\n                return false;\n            }\n            const tokenIdex = this.currentTokens.map((token) => token.start).indexOf(tokenAtCursor.start);\n            let count = tokenIdex;\n            let currentToken = tokenAtCursor;\n            // check previous token\n            while (![\"ARG_SEPARATOR\", \"LEFT_PAREN\", \"OPERATOR\"].includes(currentToken.type) ||\n                POSTFIX_UNARY_OPERATORS.includes(currentToken.value)) {\n                if (currentToken.type !== \"SPACE\" || count < 1) {\n                    return false;\n                }\n                count--;\n                currentToken = this.currentTokens[count];\n            }\n            count = tokenIdex + 1;\n            currentToken = this.currentTokens[count];\n            // check next token\n            while (currentToken &&\n                ![\"ARG_SEPARATOR\", \"RIGHT_PAREN\", \"OPERATOR\"].includes(currentToken.type)) {\n                if (currentToken.type !== \"SPACE\") {\n                    return false;\n                }\n                count++;\n                currentToken = this.currentTokens[count];\n            }\n            return true;\n        }\n        return false;\n    }\n}\n\nclass StandaloneComposerStore extends AbstractComposerStore {\n    args;\n    constructor(get, args) {\n        super(get);\n        this.args = args;\n        this._currentContent = this.getComposerContent();\n    }\n    getAutoCompleteProviders() {\n        const providersDefinitions = super.getAutoCompleteProviders();\n        const contextualAutocomplete = this.args().contextualAutocomplete;\n        if (contextualAutocomplete) {\n            providersDefinitions.push(contextualAutocomplete);\n        }\n        return providersDefinitions;\n    }\n    getComposerContent() {\n        if (this.editionMode === \"inactive\") {\n            // References in the content might not be linked to the current active sheet\n            // We here force the sheet name prefix for all references that are not in\n            // the current active sheet\n            const defaultRangeSheetId = this.args().defaultRangeSheetId;\n            return rangeTokenize(this.args().content)\n                .map((token) => {\n                if (token.type === \"REFERENCE\") {\n                    const range = this.getters.getRangeFromSheetXC(defaultRangeSheetId, token.value);\n                    return this.getters.getRangeString(range, this.getters.getActiveSheetId());\n                }\n                return token.value;\n            })\n                .join(\"\");\n        }\n        return this._currentContent;\n    }\n    stopEdition() {\n        this._stopEdition();\n    }\n    confirmEdition(content) {\n        this.args().onConfirm(content);\n    }\n}\n\ncss /* scss */ `\n  .o-spreadsheet {\n    .o-standalone-composer {\n      min-height: 24px;\n      box-sizing: border-box;\n\n      border-bottom: 1px solid;\n      border-color: ${GRAY_300};\n\n      &.active {\n        border-color: ${SELECTION_BORDER_COLOR};\n      }\n\n      &.o-invalid {\n        border-bottom: 2px solid red;\n      }\n\n      /* As the standalone composer is potentially very small (eg. in a side panel), we remove the scrollbar display */\n      scrollbar-width: none; /* Firefox */\n      &::-webkit-scrollbar {\n        display: none;\n      }\n    }\n  }\n`;\nclass StandaloneComposer extends Component {\n    static template = \"o-spreadsheet-StandaloneComposer\";\n    static props = {\n        composerContent: { type: String, optional: true },\n        defaultRangeSheetId: { type: String, optional: true },\n        onConfirm: Function,\n        contextualAutocomplete: { type: Object, optional: true },\n        placeholder: { type: String, optional: true },\n        class: { type: String, optional: true },\n        invalid: { type: Boolean, optional: true },\n    };\n    static components = { Composer };\n    static defaultProps = {\n        composerContent: \"\",\n    };\n    composerFocusStore;\n    standaloneComposerStore;\n    composerInterface;\n    spreadsheetRect = useSpreadsheetRect();\n    setup() {\n        this.composerFocusStore = useStore(ComposerFocusStore);\n        const standaloneComposerStore = useLocalStore(StandaloneComposerStore, () => ({\n            onConfirm: this.props.onConfirm,\n            content: this.props.composerContent,\n            contextualAutocomplete: this.props.contextualAutocomplete,\n            defaultRangeSheetId: this.props.defaultRangeSheetId,\n        }));\n        this.standaloneComposerStore = standaloneComposerStore;\n        this.composerInterface = {\n            id: \"standaloneComposer\",\n            get editionMode() {\n                return standaloneComposerStore.editionMode;\n            },\n            startEdition: this.standaloneComposerStore.startEdition,\n            setCurrentContent: this.standaloneComposerStore.setCurrentContent,\n            stopEdition: this.standaloneComposerStore.stopEdition,\n        };\n    }\n    get focus() {\n        return this.composerFocusStore.activeComposer === this.composerInterface\n            ? this.composerFocusStore.focusMode\n            : \"inactive\";\n    }\n    get composerStyle() {\n        return this.props.invalid\n            ? cssPropertiesToCss({ padding: \"1px 0px 0px 0px\" })\n            : cssPropertiesToCss({ padding: \"1px 0px\" });\n    }\n    get containerClass() {\n        const classes = [\n            this.focus === \"inactive\" ? \"\" : \"active\",\n            this.props.invalid ? \"o-invalid\" : \"\",\n            this.props.class || \"\",\n        ];\n        return classes.join(\" \");\n    }\n    onFocus(selection) {\n        this.composerFocusStore.focusComposer(this.composerInterface, { selection });\n    }\n}\n\ncss /* scss */ `\n  .o-icon-picker {\n    position: absolute;\n    z-index: ${ComponentsImportance.IconPicker};\n    box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);\n    background-color: white;\n    padding: 2px 1px;\n  }\n  .o-cf-icon-line {\n    display: flex;\n    padding: 0 6px;\n  }\n  .o-icon-picker-item {\n    cursor: pointer;\n    &:hover {\n      background-color: ${BADGE_SELECTED_COLOR};\n      outline: ${ACTION_COLOR} solid 1px;\n    }\n  }\n`;\nclass IconPicker extends Component {\n    static template = \"o-spreadsheet-IconPicker\";\n    static props = {\n        onIconPicked: Function,\n    };\n    icons = ICONS;\n    iconSets = ICON_SETS;\n    onIconClick(icon) {\n        if (icon) {\n            this.props.onIconPicked(icon);\n        }\n    }\n}\n\nfunction useDragAndDropListItems() {\n    let dndHelper;\n    const previousCursor = document.body.style.cursor;\n    let cleanupFns = [];\n    const cleanUp = () => {\n        dndHelper = undefined;\n        document.body.style.cursor = previousCursor;\n        cleanupFns.forEach((fn) => fn());\n        cleanupFns = [];\n    };\n    const start = (direction, args) => {\n        const onChange = () => {\n            document.body.style.cursor = \"move\";\n            if (!dndHelper)\n                return;\n            Object.assign(state.itemsStyle, dndHelper.getItemStyles());\n            args.onChange?.();\n        };\n        state.cancel = () => {\n            state.draggedItemId = undefined;\n            state.itemsStyle = {};\n            document.body.style.cursor = previousCursor;\n            args.onCancel?.();\n            cleanUp();\n        };\n        const onDragEnd = (itemId, indexAtEnd) => {\n            state.draggedItemId = undefined;\n            state.itemsStyle = {};\n            document.body.style.cursor = previousCursor;\n            args.onDragEnd?.(itemId, indexAtEnd);\n            cleanUp();\n        };\n        document.body.style.cursor = \"move\";\n        state.draggedItemId = args.draggedItemId;\n        const container = direction === \"horizontal\"\n            ? new HorizontalContainer(args.containerEl)\n            : new VerticalContainer(args.containerEl);\n        dndHelper = new DOMDndHelper({\n            ...args,\n            container,\n            onChange,\n            onDragEnd,\n            onCancel: state.cancel,\n        });\n        const stopListening = startDnd(dndHelper.onMouseMove.bind(dndHelper), dndHelper.onMouseUp.bind(dndHelper));\n        cleanupFns.push(stopListening);\n        const onScroll = dndHelper.onScroll.bind(dndHelper);\n        args.containerEl.addEventListener(\"scroll\", onScroll);\n        cleanupFns.push(() => args.containerEl.removeEventListener(\"scroll\", onScroll));\n        cleanupFns.push(dndHelper.destroy.bind(dndHelper));\n    };\n    onWillUnmount(() => {\n        cleanUp();\n    });\n    const state = useState({\n        itemsStyle: {},\n        draggedItemId: undefined,\n        start,\n        cancel: () => { },\n    });\n    return state;\n}\nclass DOMDndHelper {\n    draggedItemId;\n    items;\n    container;\n    initialMousePosition;\n    currentMousePosition;\n    initialScroll;\n    minPosition;\n    maxPosition;\n    edgeScrollIntervalId;\n    onChange;\n    onCancel;\n    onDragEnd;\n    /**\n     * The dead zone is an area in which the pointermove events are ignored.\n     *\n     * This is useful when swapping the dragged item with a larger item. After the swap,\n     * the mouse is still hovering on the item  we just swapped with. In this case, we don't want\n     * a mouse move to trigger another swap the other way around, so we create a dead zone. We will clear\n     * the dead zone when the mouse leaves the swapped item.\n     */\n    deadZone;\n    constructor(args) {\n        this.items = args.items.map((item) => ({ ...item, positionAtStart: item.position }));\n        this.draggedItemId = args.draggedItemId;\n        this.container = args.container;\n        this.onChange = args.onChange;\n        this.onCancel = args.onCancel;\n        this.onDragEnd = args.onDragEnd;\n        this.initialMousePosition = args.initialMousePosition;\n        this.currentMousePosition = args.initialMousePosition;\n        this.initialScroll = this.container.scroll;\n        this.minPosition = this.items[0].position;\n        this.maxPosition =\n            this.items[this.items.length - 1].position + this.items[this.items.length - 1].size;\n    }\n    getItemStyles() {\n        const styles = {};\n        for (let item of this.items) {\n            styles[item.id] = this.getItemStyle(item.id);\n        }\n        return styles;\n    }\n    getItemStyle(itemId) {\n        const position = this.container.cssPositionProperty;\n        const style = {};\n        style.position = \"relative\";\n        style[position] = (this.getItemsPositions()[itemId] || 0) + \"px\";\n        style.transition = `${position} 0.5s`;\n        style[\"pointer-events\"] = \"none\";\n        if (this.draggedItemId === itemId) {\n            style.transition = `${position} 0s`;\n            style[\"z-index\"] = \"1000\";\n        }\n        return cssPropertiesToCss(style);\n    }\n    onScroll() {\n        this.moveDraggedItemToPosition(this.currentMousePosition + this.scrollOffset);\n    }\n    onMouseMove(ev) {\n        if (ev.button > 1) {\n            this.onCancel();\n            return;\n        }\n        const mousePosition = this.container.getMousePosition(ev);\n        this.currentMousePosition = mousePosition;\n        if (mousePosition < this.container.start || mousePosition > this.container.end) {\n            this.startEdgeScroll(mousePosition < this.container.start ? -1 : 1);\n            return;\n        }\n        else {\n            this.stopEdgeScroll();\n        }\n        this.moveDraggedItemToPosition(mousePosition + this.scrollOffset);\n    }\n    moveDraggedItemToPosition(position) {\n        const hoveredItemIndex = this.getHoveredItemIndex(position, this.items);\n        const draggedItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);\n        const draggedItem = this.items[draggedItemIndex];\n        if (this.deadZone && this.isInZone(position, this.deadZone)) {\n            this.onChange(this.getItemsPositions());\n            return;\n        }\n        else if (this.isInZone(position, {\n            start: draggedItem.position,\n            end: draggedItem.position + draggedItem.size,\n        })) {\n            this.deadZone = undefined;\n        }\n        if (draggedItemIndex === hoveredItemIndex) {\n            this.onChange(this.getItemsPositions());\n            return;\n        }\n        const startIndex = Math.min(draggedItemIndex, hoveredItemIndex);\n        const endIndex = Math.max(draggedItemIndex, hoveredItemIndex);\n        const direction = Math.sign(hoveredItemIndex - draggedItemIndex);\n        let draggedItemMoveSize = 0;\n        for (let i = startIndex; i <= endIndex; i++) {\n            if (i === draggedItemIndex) {\n                continue;\n            }\n            this.items[i].position -= direction * draggedItem.size;\n            draggedItemMoveSize += this.items[i].size;\n        }\n        draggedItem.position += direction * draggedItemMoveSize;\n        this.items.sort((item1, item2) => item1.position - item2.position);\n        this.deadZone =\n            direction > 0\n                ? { start: position, end: draggedItem.position }\n                : { start: draggedItem.position + draggedItem.size, end: position };\n        this.onChange(this.getItemsPositions());\n    }\n    onMouseUp(ev) {\n        if (ev.button !== 0) {\n            this.onCancel();\n        }\n        ev.stopPropagation();\n        ev.preventDefault();\n        const targetItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);\n        this.onDragEnd(this.draggedItemId, targetItemIndex);\n        this.stopEdgeScroll();\n        return false;\n    }\n    startEdgeScroll(direction) {\n        if (this.edgeScrollIntervalId)\n            return;\n        this.edgeScrollIntervalId = window.setInterval(() => {\n            const offset = direction * 3;\n            let newPosition = this.currentMousePosition + offset;\n            if (newPosition < Math.min(this.container.start, this.minPosition)) {\n                newPosition = Math.min(this.container.start, this.minPosition);\n            }\n            else if (newPosition > Math.max(this.container.end, this.maxPosition)) {\n                newPosition = Math.max(this.container.end, this.maxPosition);\n            }\n            this.container.scroll += offset;\n        }, 5);\n    }\n    stopEdgeScroll() {\n        window.clearInterval(this.edgeScrollIntervalId);\n        this.edgeScrollIntervalId = undefined;\n    }\n    /**\n     * Get the index of the item the given mouse position is inside.\n     * If the mouse is outside the container, return the first or last item index.\n     */\n    getHoveredItemIndex(mousePosition, items) {\n        if (mousePosition <= this.minPosition)\n            return 0;\n        if (mousePosition >= this.maxPosition)\n            return items.length - 1;\n        return items.findIndex((item) => item.position + item.size >= mousePosition);\n    }\n    getItemsPositions() {\n        const positions = {};\n        for (let item of this.items) {\n            if (item.id !== this.draggedItemId) {\n                positions[item.id] = item.position - item.positionAtStart;\n                continue;\n            }\n            const mouseOffset = this.currentMousePosition - this.initialMousePosition;\n            let start = mouseOffset + this.scrollOffset;\n            start = Math.max(this.minPosition - item.positionAtStart, start);\n            start = Math.min(this.maxPosition - item.positionAtStart - item.size, start);\n            positions[item.id] = start;\n        }\n        return positions;\n    }\n    isInZone(position, zone) {\n        return position >= zone.start && position <= zone.end;\n    }\n    get scrollOffset() {\n        return this.container.scroll - this.initialScroll;\n    }\n    destroy() {\n        this.stopEdgeScroll();\n    }\n}\nclass ContainerWrapper {\n    el;\n    constructor(el) {\n        this.el = el;\n    }\n    get containerRect() {\n        return this.el.getBoundingClientRect();\n    }\n}\nclass VerticalContainer extends ContainerWrapper {\n    get start() {\n        return this.containerRect.top;\n    }\n    get end() {\n        return this.containerRect.bottom;\n    }\n    get cssPositionProperty() {\n        return \"top\";\n    }\n    get scroll() {\n        return this.el.scrollTop;\n    }\n    set scroll(scroll) {\n        this.el.scrollTop = scroll;\n    }\n    getMousePosition(ev) {\n        return ev.clientY;\n    }\n}\nclass HorizontalContainer extends ContainerWrapper {\n    get start() {\n        return this.containerRect.left;\n    }\n    get end() {\n        return this.containerRect.right;\n    }\n    get cssPositionProperty() {\n        return \"left\";\n    }\n    get scroll() {\n        return this.el.scrollLeft;\n    }\n    set scroll(scroll) {\n        this.el.scrollLeft = scroll;\n    }\n    getMousePosition(ev) {\n        return ev.clientX;\n    }\n}\n\n/**\n * Manages an event listener on a ref. Useful for hooks that want to manage\n * event listeners, especially more than one. Prefer using t-on directly in\n * components. If your hook only needs a single event listener, consider simply\n * returning it from the hook and letting the user attach it with t-on.\n *\n * Adapted from Odoo Community - See https://github.com/odoo/odoo/blob/saas-16.2/addons/web/static/src/core/utils/hooks.js\n */\nfunction useRefListener(ref, ...listener) {\n    useEffect((el) => {\n        el?.addEventListener(...listener);\n        return () => el?.removeEventListener(...listener);\n    }, () => [ref.el]);\n}\nfunction useHoveredElement(ref) {\n    const state = useState({ hovered: false });\n    useRefListener(ref, \"mouseenter\", () => (state.hovered = true));\n    useRefListener(ref, \"mouseleave\", () => (state.hovered = false));\n    return state;\n}\n\nfunction useHighlightsOnHover(ref, highlightProvider) {\n    const hoverState = useHoveredElement(ref);\n    useHighlights({\n        get highlights() {\n            return hoverState.hovered ? highlightProvider.highlights : [];\n        },\n    });\n}\nfunction useHighlights(highlightProvider) {\n    const stores = useStoreProvider();\n    const store = useLocalStore(HighlightStore);\n    onMounted(() => {\n        store.register(highlightProvider);\n    });\n    let currentHighlights = highlightProvider.highlights;\n    useEffect((highlights) => {\n        if (!deepEquals(highlights, currentHighlights)) {\n            currentHighlights = highlights;\n            stores.trigger(\"store-updated\");\n        }\n    }, () => [highlightProvider.highlights]);\n}\n\ncss /* scss */ `\n  .o-cf-preview {\n    &.o-cf-cursor-ptr {\n      cursor: pointer;\n    }\n\n    border-bottom: 1px solid ${GRAY_300};\n    height: 60px;\n    padding: 10px;\n    position: relative;\n    cursor: pointer;\n    &:hover,\n    &.o-cf-dragging {\n      background-color: ${GRAY_200};\n    }\n\n    &:not(:hover) .o-cf-delete-button {\n      display: none;\n    }\n    .o-cf-preview-icon {\n      border: 1px solid ${GRAY_300};\n      background-color: #fff;\n      position: absolute;\n      height: 50px;\n      width: 50px;\n      .o-icon {\n        width: ${CF_ICON_EDGE_LENGTH}px;\n        height: ${CF_ICON_EDGE_LENGTH}px;\n      }\n    }\n    .o-cf-preview-description {\n      left: 65px;\n      margin-bottom: auto;\n      margin-right: 8px;\n      margin-top: auto;\n      position: relative;\n      width: 142px;\n      .o-cf-preview-description-rule {\n        margin-bottom: 4px;\n        max-height: 2.8em;\n        line-height: 1.4em;\n      }\n      .o-cf-preview-range {\n        font-size: 12px;\n      }\n    }\n    .o-cf-delete {\n      left: 90%;\n      top: 39%;\n      position: absolute;\n    }\n    &:not(:hover):not(.o-cf-dragging) .o-cf-drag-handle {\n      display: none !important;\n    }\n    .o-cf-drag-handle {\n      left: -8px;\n      cursor: move;\n      .o-icon {\n        width: 6px;\n        height: 30px;\n      }\n    }\n\n    .o-icon.arrow-down {\n      color: #e06666;\n    }\n    .o-icon.arrow-up {\n      color: #6aa84f;\n    }\n  }\n`;\nclass ConditionalFormatPreview extends Component {\n    static template = \"o-spreadsheet-ConditionalFormatPreview\";\n    icons = ICONS;\n    ref = useRef(\"cfPreview\");\n    setup() {\n        useHighlightsOnHover(this.ref, this);\n    }\n    getPreviewImageStyle() {\n        const rule = this.props.conditionalFormat.rule;\n        if (rule.type === \"CellIsRule\") {\n            return cssPropertiesToCss(cellStyleToCss(rule.style));\n        }\n        else if (rule.type === \"ColorScaleRule\") {\n            const minColor = colorNumberString(rule.minimum.color);\n            const midColor = rule.midpoint ? colorNumberString(rule.midpoint.color) : null;\n            const maxColor = colorNumberString(rule.maximum.color);\n            const baseString = \"background-image: linear-gradient(to right, \";\n            return midColor\n                ? baseString + minColor + \", \" + midColor + \", \" + maxColor + \")\"\n                : baseString + minColor + \", \" + maxColor + \")\";\n        }\n        else if (rule.type === \"DataBarRule\") {\n            const color = colorNumberString(rule.color);\n            return `background-image: linear-gradient(to right, ${color} 50%, white 50%)`;\n        }\n        return \"\";\n    }\n    getDescription() {\n        const cf = this.props.conditionalFormat;\n        switch (cf.rule.type) {\n            case \"CellIsRule\":\n                const description = CellIsOperators[cf.rule.operator];\n                if (cf.rule.values.length === 1) {\n                    return `${description} ${cf.rule.values[0]}`;\n                }\n                if (cf.rule.values.length === 2) {\n                    return _t(\"%s %s and %s\", description, cf.rule.values[0], cf.rule.values[1]);\n                }\n                return description;\n            case \"ColorScaleRule\":\n                return CfTerms.ColorScale;\n            case \"IconSetRule\":\n                return CfTerms.IconSet;\n            case \"DataBarRule\":\n                return CfTerms.DataBar;\n        }\n    }\n    deleteConditionalFormat() {\n        this.env.model.dispatch(\"REMOVE_CONDITIONAL_FORMAT\", {\n            id: this.props.conditionalFormat.id,\n            sheetId: this.env.model.getters.getActiveSheetId(),\n        });\n    }\n    onMouseDown(event) {\n        this.props.onMouseDown(event);\n    }\n    get highlights() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.props.conditionalFormat.ranges.map((range) => ({\n            sheetId,\n            zone: this.env.model.getters.getRangeFromSheetXC(sheetId, range).zone,\n            color: HIGHLIGHT_COLOR,\n            fillAlpha: 0.06,\n        }));\n    }\n}\nConditionalFormatPreview.props = {\n    conditionalFormat: Object,\n    onPreviewClick: Function,\n    onMouseDown: Function,\n    class: String,\n};\n\nclass ConditionalFormatPreviewList extends Component {\n    static template = \"o-spreadsheet-ConditionalFormatPreviewList\";\n    static props = {\n        conditionalFormats: Array,\n        onPreviewClick: Function,\n        onAddConditionalFormat: Function,\n    };\n    static components = { ConditionalFormatPreview };\n    icons = ICONS;\n    dragAndDrop = useDragAndDropListItems();\n    cfListRef = useRef(\"cfList\");\n    setup() {\n        onWillUpdateProps((nextProps) => {\n            if (!deepEquals(this.props.conditionalFormats, nextProps.conditionalFormats)) {\n                this.dragAndDrop.cancel();\n            }\n        });\n    }\n    getPreviewDivStyle(cf) {\n        return this.dragAndDrop.itemsStyle[cf.id] || \"\";\n    }\n    onPreviewMouseDown(cf, event) {\n        if (event.button !== 0)\n            return;\n        const previewRects = Array.from(this.cfListRef.el.children).map((previewEl) => getBoundingRectAsPOJO(previewEl));\n        const items = this.props.conditionalFormats.map((cf, index) => ({\n            id: cf.id,\n            size: previewRects[index].height,\n            position: previewRects[index].y,\n        }));\n        this.dragAndDrop.start(\"vertical\", {\n            draggedItemId: cf.id,\n            initialMousePosition: event.clientY,\n            items: items,\n            containerEl: this.cfListRef.el,\n            onDragEnd: (cfId, finalIndex) => this.onDragEnd(cfId, finalIndex),\n        });\n    }\n    onDragEnd(cfId, finalIndex) {\n        const originalIndex = this.props.conditionalFormats.findIndex((sheet) => sheet.id === cfId);\n        const delta = originalIndex - finalIndex;\n        if (delta !== 0) {\n            this.env.model.dispatch(\"CHANGE_CONDITIONAL_FORMAT_PRIORITY\", {\n                cfId,\n                delta,\n                sheetId: this.env.model.getters.getActiveSheetId(),\n            });\n        }\n    }\n}\n\ncss /* scss */ `\n  .o-cf-ruleEditor {\n    .o-cf-preview-display {\n      border: 1px solid ${GRAY_300};\n      padding: 10px;\n    }\n\n    .o-cf-cell-is-rule {\n      .o-divider {\n        border-right: 1px solid ${GRAY_300};\n        margin: 4px 6px;\n      }\n    }\n    .o-cf-color-scale-editor {\n      .o-threshold {\n        .o-select-with-input {\n          max-width: 150px;\n        }\n        .o-threshold-value {\n          flex-grow: 1;\n          flex-basis: 60%;\n          min-width: 0px; // input overflows in Firefox otherwise\n        }\n        .o-threshold-value input:disabled {\n          background-color: #edebed;\n        }\n      }\n    }\n    .o-cf-iconset-rule {\n      .o-cf-clickable-icon {\n        border: 1px solid ${GRAY_200};\n        border-radius: 4px;\n        cursor: pointer;\n        &:hover {\n          border-color: ${ACTION_COLOR};\n          background-color: ${BADGE_SELECTED_COLOR};\n        }\n        .o-icon {\n          width: ${CF_ICON_EDGE_LENGTH}px;\n          height: ${CF_ICON_EDGE_LENGTH}px;\n        }\n      }\n      .o-cf-iconsets {\n        gap: 11px;\n        .o-cf-iconset {\n          padding: 7px 8px;\n          width: 95px;\n          .o-icon {\n            margin: 0 3px;\n          }\n          svg {\n            vertical-align: baseline;\n          }\n        }\n      }\n      .o-inflection {\n        .o-cf-icon-button {\n          padding: 4px 10px;\n        }\n        table {\n          font-size: 13px;\n          td {\n            padding: 6px 0;\n          }\n\n          th.o-cf-iconset-icons {\n            width: 25px;\n          }\n          th.o-cf-iconset-text {\n            width: 82px;\n          }\n          th.o-cf-iconset-operator {\n            width: 20px;\n          }\n          .o-cf-iconset-type {\n            min-width: 80px;\n          }\n        }\n      }\n    }\n\n    .o-icon.arrow-down {\n      color: #e06666;\n    }\n    .o-icon.arrow-up {\n      color: #6aa84f;\n    }\n  }\n`;\nclass ConditionalFormattingEditor extends Component {\n    static template = \"o-spreadsheet-ConditionalFormattingEditor\";\n    static props = {\n        editedCf: { type: Object, optional: true },\n        onExitEdition: Function,\n    };\n    static components = {\n        SelectionInput,\n        IconPicker,\n        ColorPickerWidget,\n        ConditionalFormatPreviewList,\n        Section,\n        RoundColorPicker,\n        StandaloneComposer: StandaloneComposer,\n        BadgeSelection,\n        ValidationMessages,\n    };\n    icons = ICONS;\n    cellIsOperators = CellIsOperators;\n    iconSets = ICON_SETS;\n    getTextDecoration = getTextDecoration;\n    colorNumberString = colorNumberString;\n    state;\n    setup() {\n        const cf = this.props.editedCf || {\n            id: this.env.model.uuidGenerator.uuidv4(),\n            ranges: this.env.model.getters\n                .getSelectedZones()\n                .map((zone) => this.env.model.getters.zoneToXC(this.env.model.getters.getActiveSheetId(), zone)),\n        };\n        this.state = useState({\n            currentCF: cf,\n            currentCFType: this.props.editedCf?.rule.type || \"CellIsRule\",\n            errors: [],\n            rules: this.getDefaultRules(),\n        });\n        if (this.props.editedCf) {\n            switch (this.props.editedCf.rule.type) {\n                case \"CellIsRule\":\n                    this.state.rules.cellIs = this.props.editedCf.rule;\n                    break;\n                case \"ColorScaleRule\":\n                    this.state.rules.colorScale = this.props.editedCf.rule;\n                    break;\n                case \"IconSetRule\":\n                    this.state.rules.iconSet = this.props.editedCf.rule;\n                    break;\n                case \"DataBarRule\":\n                    this.state.rules.dataBar = this.props.editedCf.rule;\n                    break;\n            }\n        }\n        useExternalListener(window, \"click\", this.closeMenus);\n    }\n    get isRangeValid() {\n        return this.state.errors.includes(\"EmptyRange\" /* CommandResult.EmptyRange */);\n    }\n    get errorMessages() {\n        return this.state.errors.map((error) => CfTerms.Errors[error] || CfTerms.Errors.Unexpected);\n    }\n    get cfTypesValues() {\n        return [\n            { value: \"CellIsRule\", label: _t(\"Single color\") },\n            { value: \"ColorScaleRule\", label: _t(\"Color scale\") },\n            { value: \"IconSetRule\", label: _t(\"Icon set\") },\n            { value: \"DataBarRule\", label: _t(\"Data bar\") },\n        ];\n    }\n    saveConditionalFormat() {\n        if (this.state.currentCF) {\n            const invalidRanges = this.state.currentCF.ranges.some((xc) => !xc.match(rangeReference));\n            if (invalidRanges) {\n                this.state.errors = [\"InvalidRange\" /* CommandResult.InvalidRange */];\n                return;\n            }\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            const locale = this.env.model.getters.getLocale();\n            const result = this.env.model.dispatch(\"ADD_CONDITIONAL_FORMAT\", {\n                cf: {\n                    rule: canonicalizeCFRule(this.getEditorRule(), locale),\n                    id: this.state.currentCF.id,\n                },\n                ranges: this.state.currentCF.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),\n                sheetId,\n            });\n            if (!result.isSuccessful) {\n                this.state.errors = result.reasons;\n            }\n            else {\n                this.props.onExitEdition();\n            }\n        }\n    }\n    /**\n     * Get the rule currently edited with the editor\n     */\n    getEditorRule() {\n        switch (this.state.currentCFType) {\n            case \"CellIsRule\":\n                return this.state.rules.cellIs;\n            case \"ColorScaleRule\":\n                return this.state.rules.colorScale;\n            case \"IconSetRule\":\n                return this.state.rules.iconSet;\n            case \"DataBarRule\":\n                return this.state.rules.dataBar;\n        }\n    }\n    getDefaultRules() {\n        return {\n            cellIs: {\n                type: \"CellIsRule\",\n                operator: \"IsNotEmpty\",\n                values: [],\n                style: { fillColor: \"#b6d7a8\" },\n            },\n            colorScale: {\n                type: \"ColorScaleRule\",\n                minimum: { type: \"value\", color: 0xffffff },\n                midpoint: undefined,\n                maximum: { type: \"value\", color: 0x6aa84f },\n            },\n            iconSet: {\n                type: \"IconSetRule\",\n                icons: {\n                    upper: \"arrowGood\",\n                    middle: \"arrowNeutral\",\n                    lower: \"arrowBad\",\n                },\n                upperInflectionPoint: {\n                    type: \"percentage\",\n                    value: \"66\",\n                    operator: \"gt\",\n                },\n                lowerInflectionPoint: {\n                    type: \"percentage\",\n                    value: \"33\",\n                    operator: \"gt\",\n                },\n            },\n            dataBar: {\n                type: \"DataBarRule\",\n                color: 0xd9ead3,\n            },\n        };\n    }\n    changeRuleType(ruleType) {\n        if (this.state.currentCFType === ruleType || !this.state.rules) {\n            return;\n        }\n        this.state.errors = [];\n        this.state.currentCFType = ruleType;\n    }\n    onRangesChanged(ranges) {\n        if (this.state.currentCF) {\n            this.state.currentCF.ranges = ranges;\n        }\n    }\n    /*****************************************************************************\n     * Common\n     ****************************************************************************/\n    toggleMenu(menu) {\n        const isSelected = this.state.openedMenu === menu;\n        this.closeMenus();\n        if (!isSelected) {\n            this.state.openedMenu = menu;\n        }\n    }\n    closeMenus() {\n        this.state.openedMenu = undefined;\n    }\n    /*****************************************************************************\n     * Cell Is Rule\n     ****************************************************************************/\n    get isValue1Invalid() {\n        return (this.state.errors.includes(\"FirstArgMissing\" /* CommandResult.FirstArgMissing */) ||\n            this.state.errors.includes(\"ValueCellIsInvalidFormula\" /* CommandResult.ValueCellIsInvalidFormula */));\n    }\n    get isValue2Invalid() {\n        return this.state.errors.includes(\"SecondArgMissing\" /* CommandResult.SecondArgMissing */);\n    }\n    toggleStyle(tool) {\n        const style = this.state.rules.cellIs.style;\n        style[tool] = !style[tool];\n        this.closeMenus();\n    }\n    onKeydown(event) {\n        if (event.key === \"F4\") {\n            const target = event.target;\n            const update = cycleFixedReference({ start: target.selectionStart ?? 0, end: target.selectionEnd ?? 0 }, target.value, this.env.model.getters.getLocale());\n            if (!update) {\n                return;\n            }\n            target.value = update.content;\n            target.setSelectionRange(update.selection.start, update.selection.end);\n            target.dispatchEvent(new Event(\"input\"));\n        }\n    }\n    setColor(target, color) {\n        this.state.rules.cellIs.style[target] = color;\n        this.closeMenus();\n    }\n    /*****************************************************************************\n     * Color Scale Rule\n     ****************************************************************************/\n    isValueInvalid(threshold) {\n        switch (threshold) {\n            case \"minimum\":\n                return (this.state.errors.includes(\"MinInvalidFormula\" /* CommandResult.MinInvalidFormula */) ||\n                    this.state.errors.includes(\"MinBiggerThanMid\" /* CommandResult.MinBiggerThanMid */) ||\n                    this.state.errors.includes(\"MinBiggerThanMax\" /* CommandResult.MinBiggerThanMax */) ||\n                    this.state.errors.includes(\"MinNaN\" /* CommandResult.MinNaN */));\n            case \"midpoint\":\n                return (this.state.errors.includes(\"MidInvalidFormula\" /* CommandResult.MidInvalidFormula */) ||\n                    this.state.errors.includes(\"MidNaN\" /* CommandResult.MidNaN */) ||\n                    this.state.errors.includes(\"MidBiggerThanMax\" /* CommandResult.MidBiggerThanMax */));\n            case \"maximum\":\n                return (this.state.errors.includes(\"MaxInvalidFormula\" /* CommandResult.MaxInvalidFormula */) ||\n                    this.state.errors.includes(\"MaxNaN\" /* CommandResult.MaxNaN */));\n            default:\n                return false;\n        }\n    }\n    setColorScaleColor(target, color) {\n        if (!isColorValid(color)) {\n            return;\n        }\n        const point = this.state.rules.colorScale[target];\n        if (point) {\n            point.color = Number.parseInt(color.substr(1), 16);\n        }\n        this.closeMenus();\n    }\n    getPreviewGradient() {\n        const rule = this.state.rules.colorScale;\n        const minColor = colorNumberString(rule.minimum.color);\n        const midColor = colorNumberString(rule.midpoint?.color || DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);\n        const maxColor = colorNumberString(rule.maximum.color);\n        const baseString = \"background-image: linear-gradient(to right, \";\n        return rule.midpoint === undefined\n            ? baseString + minColor + \", \" + maxColor + \")\"\n            : baseString + minColor + \", \" + midColor + \", \" + maxColor + \")\";\n    }\n    getThresholdColor(threshold) {\n        return threshold\n            ? colorNumberString(threshold.color)\n            : colorNumberString(DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);\n    }\n    onMidpointChange(ev) {\n        const type = ev.target.value;\n        const rule = this.state.rules.colorScale;\n        if (type === \"none\") {\n            rule.midpoint = undefined;\n        }\n        else {\n            rule.midpoint = {\n                color: DEFAULT_COLOR_SCALE_MIDPOINT_COLOR,\n                value: \"\",\n                ...rule.midpoint,\n                type,\n            };\n        }\n    }\n    /*****************************************************************************\n     * Icon Set\n     ****************************************************************************/\n    isInflectionPointInvalid(inflectionPoint) {\n        switch (inflectionPoint) {\n            case \"lowerInflectionPoint\":\n                return (this.state.errors.includes(\"ValueLowerInflectionNaN\" /* CommandResult.ValueLowerInflectionNaN */) ||\n                    this.state.errors.includes(\"ValueLowerInvalidFormula\" /* CommandResult.ValueLowerInvalidFormula */) ||\n                    this.state.errors.includes(\"LowerBiggerThanUpper\" /* CommandResult.LowerBiggerThanUpper */));\n            case \"upperInflectionPoint\":\n                return (this.state.errors.includes(\"ValueUpperInflectionNaN\" /* CommandResult.ValueUpperInflectionNaN */) ||\n                    this.state.errors.includes(\"ValueUpperInvalidFormula\" /* CommandResult.ValueUpperInvalidFormula */) ||\n                    this.state.errors.includes(\"LowerBiggerThanUpper\" /* CommandResult.LowerBiggerThanUpper */));\n            default:\n                return true;\n        }\n    }\n    reverseIcons() {\n        const icons = this.state.rules.iconSet.icons;\n        const upper = icons.upper;\n        icons.upper = icons.lower;\n        icons.lower = upper;\n    }\n    setIconSet(iconSet) {\n        const icons = this.state.rules.iconSet.icons;\n        icons.upper = this.iconSets[iconSet].good;\n        icons.middle = this.iconSets[iconSet].neutral;\n        icons.lower = this.iconSets[iconSet].bad;\n    }\n    setIcon(target, icon) {\n        this.state.rules.iconSet.icons[target] = icon;\n    }\n    getCellIsRuleComposerProps(valueIndex) {\n        const isInvalid = valueIndex === 0 ? this.isValue1Invalid : this.isValue2Invalid;\n        return {\n            onConfirm: (str) => (this.state.rules.cellIs.values[valueIndex] = str),\n            composerContent: this.state.rules.cellIs.values[valueIndex],\n            placeholder: _t(\"Value or formula\"),\n            invalid: isInvalid,\n            class: \"o-sidePanel-composer\",\n            defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),\n        };\n    }\n    getColorScaleComposerProps(thresholdType) {\n        const threshold = this.state.rules.colorScale[thresholdType];\n        if (!threshold) {\n            throw new Error(\"Threshold not found\");\n        }\n        const isInvalid = this.isValueInvalid(thresholdType);\n        return {\n            onConfirm: (str) => (threshold.value = str),\n            composerContent: threshold.value || \"\",\n            placeholder: _t(\"Formula\"),\n            invalid: isInvalid,\n            class: \"o-sidePanel-composer\",\n            defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),\n        };\n    }\n    getColorIconSetComposerProps(inflectionPoint) {\n        const inflection = this.state.rules.iconSet[inflectionPoint];\n        const isInvalid = this.isInflectionPointInvalid(inflectionPoint);\n        return {\n            onConfirm: (str) => (inflection.value = str),\n            composerContent: inflection.value || \"\",\n            placeholder: _t(\"Formula\"),\n            invalid: isInvalid,\n            class: \"o-sidePanel-composer\",\n            defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),\n        };\n    }\n    /*****************************************************************************\n     * DataBar\n     ****************************************************************************/\n    getRangeValues() {\n        return [this.state.rules.dataBar.rangeValues || \"\"];\n    }\n    updateDataBarColor(color) {\n        if (!isColorValid(color)) {\n            return;\n        }\n        this.state.rules.dataBar.color = Number.parseInt(color.substr(1), 16);\n    }\n    onDataBarRangeUpdate(ranges) {\n        this.state.rules.dataBar.rangeValues = ranges[0];\n    }\n}\n\nclass ConditionalFormattingPanel extends Component {\n    static template = \"o-spreadsheet-ConditionalFormattingPanel\";\n    static props = {\n        selection: { type: Object, optional: true },\n        onCloseSidePanel: Function,\n    };\n    static components = {\n        ConditionalFormatPreviewList,\n        ConditionalFormattingEditor,\n    };\n    activeSheetId;\n    state = useState({\n        mode: \"list\",\n    });\n    setup() {\n        this.activeSheetId = this.env.model.getters.getActiveSheetId();\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const rules = this.env.model.getters.getRulesSelection(sheetId, this.props.selection || []);\n        if (rules.length === 1) {\n            const cf = this.conditionalFormats.find((c) => c.id === rules[0]);\n            if (cf) {\n                this.editConditionalFormat(cf);\n            }\n        }\n        onWillUpdateProps((nextProps) => {\n            const newActiveSheetId = this.env.model.getters.getActiveSheetId();\n            if (newActiveSheetId !== this.activeSheetId) {\n                this.activeSheetId = newActiveSheetId;\n                this.switchToList();\n            }\n            else if (nextProps.selection !== this.props.selection) {\n                const sheetId = this.env.model.getters.getActiveSheetId();\n                const rules = this.env.model.getters.getRulesSelection(sheetId, nextProps.selection || []);\n                if (rules.length === 1) {\n                    const cf = this.conditionalFormats.find((c) => c.id === rules[0]);\n                    if (cf) {\n                        this.editConditionalFormat(cf);\n                    }\n                }\n                else {\n                    this.switchToList();\n                }\n            }\n        });\n    }\n    get conditionalFormats() {\n        const cfs = this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());\n        return cfs.map((cf) => ({\n            ...cf,\n            rule: localizeCFRule(cf.rule, this.env.model.getters.getLocale()),\n        }));\n    }\n    switchToList() {\n        this.state.mode = \"list\";\n        this.state.editedCf = undefined;\n    }\n    addConditionalFormat() {\n        this.state.mode = \"edit\";\n    }\n    editConditionalFormat(cf) {\n        this.state.mode = \"edit\";\n        this.state.editedCf = cf;\n    }\n}\n\ncss /* scss */ `\n  .o-custom-currency {\n    .o-format-proposals {\n      color: black;\n    }\n\n    .o-format-examples {\n      background: #f9fafb;\n      padding: 8px;\n      border-radius: 4px;\n      border: 1px solid #d8dadd;\n      color: #374151;\n    }\n  }\n`;\nclass CustomCurrencyPanel extends Component {\n    static template = \"o-spreadsheet-CustomCurrencyPanel\";\n    static components = { Section, Checkbox };\n    static props = { onCloseSidePanel: Function };\n    availableCurrencies;\n    state;\n    setup() {\n        this.availableCurrencies = [];\n        this.state = useState({\n            selectedCurrencyIndex: 0,\n            currencyCode: \"\",\n            currencySymbol: \"\",\n            selectedFormatIndex: 0,\n            isAccountingFormat: false,\n        });\n        onWillStart(() => this.updateAvailableCurrencies());\n    }\n    get formatProposals() {\n        const baseCurrency = this.availableCurrencies[this.state.selectedCurrencyIndex];\n        const position = baseCurrency.position;\n        const opposite = baseCurrency.position === \"before\" ? \"after\" : \"before\";\n        const symbol = this.state.currencySymbol.trim() ? this.state.currencySymbol : \"\";\n        const code = this.state.currencyCode.trim() ? this.state.currencyCode : \"\";\n        const decimalPlaces = baseCurrency.decimalPlaces;\n        if (!symbol && !code) {\n            return [];\n        }\n        const simple = { symbol, position, decimalPlaces };\n        const rounded = { symbol, position, decimalPlaces: 0 };\n        const simpleWithCode = { symbol, position, decimalPlaces, code };\n        const roundedWithCode = { symbol, position, decimalPlaces: 0, code };\n        const simpleOpposite = { symbol, position: opposite, decimalPlaces };\n        const roundedOpposite = { symbol, position: opposite, decimalPlaces: 0 };\n        const simpleOppositeWithCode = { symbol, position: opposite, decimalPlaces, code };\n        const roundedOppositeWithCode = { symbol, position: opposite, decimalPlaces: 0, code };\n        const currencies = [\n            rounded,\n            simple,\n            roundedWithCode,\n            simpleWithCode,\n            roundedOpposite,\n            simpleOpposite,\n            roundedOppositeWithCode,\n            simpleOppositeWithCode,\n        ];\n        const usedFormats = new Set();\n        const locale = this.env.model.getters.getLocale();\n        return currencies\n            .map((currency) => {\n            const format = createCurrencyFormat(currency);\n            if ((!currency.symbol && !currency.code) || usedFormats.has(format)) {\n                return undefined;\n            }\n            usedFormats.add(format);\n            return {\n                format,\n                accountingFormat: createAccountingFormat(currency),\n                example: formatValue(1000.0, { format, locale }),\n            };\n        })\n            .filter(isDefined);\n    }\n    get isSameFormat() {\n        return this.selectedFormat ? this.selectedFormat === this.getCommonFormat() : false;\n    }\n    async updateAvailableCurrencies() {\n        if (currenciesRegistry.getAll().length === 0) {\n            const currencies = (await this.env.loadCurrencies?.()) || [];\n            currencies.forEach((currency, index) => {\n                currenciesRegistry.add(index.toString(), currency);\n            });\n        }\n        const emptyCurrency = {\n            name: _t(CustomCurrencyTerms.Custom),\n            code: \"\",\n            symbol: \"\",\n            decimalPlaces: 2,\n            position: \"after\",\n        };\n        this.availableCurrencies = [emptyCurrency, ...currenciesRegistry.getAll()];\n    }\n    updateSelectCurrency(ev) {\n        const target = ev.target;\n        this.state.selectedCurrencyIndex = parseInt(target.value, 10);\n        const currency = this.availableCurrencies[this.state.selectedCurrencyIndex];\n        this.state.currencyCode = currency.code;\n        this.state.currencySymbol = currency.symbol;\n    }\n    updateCode(ev) {\n        const target = ev.target;\n        this.state.currencyCode = target.value;\n        this.initAvailableCurrencies();\n    }\n    updateSymbol(ev) {\n        const target = ev.target;\n        this.state.currencySymbol = target.value;\n        this.initAvailableCurrencies();\n    }\n    updateSelectFormat(ev) {\n        const target = ev.target;\n        this.state.selectedFormatIndex = parseInt(target.value, 10);\n    }\n    apply() {\n        this.env.model.dispatch(\"SET_FORMATTING_WITH_PIVOT\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            target: this.env.model.getters.getSelectedZones(),\n            format: this.selectedFormat,\n        });\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    initAvailableCurrencies() {\n        this.state.selectedCurrencyIndex = 0;\n    }\n    getCommonFormat() {\n        const selectedZones = this.env.model.getters.getSelectedZones();\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const cells = selectedZones\n            .map((zone) => this.env.model.getters.getEvaluatedCellsInZone(sheetId, zone))\n            .flat();\n        const firstFormat = cells[0].format;\n        return cells.every((cell) => cell.format === firstFormat) ? firstFormat : undefined;\n    }\n    currencyDisplayName(currency) {\n        return currency.name + (currency.code ? ` (${currency.code})` : \"\");\n    }\n    toggleAccountingFormat() {\n        this.state.isAccountingFormat = !this.state.isAccountingFormat;\n    }\n    getFormatExamples() {\n        const format = this.selectedFormat;\n        const locale = this.env.model.getters.getLocale();\n        return [\n            { label: _t(\"positive\") + \":\", value: formatValue(1234.56, { format, locale }) },\n            { label: _t(\"negative\") + \":\", value: formatValue(-1234.56, { format, locale }) },\n            { label: _t(\"zero\") + \":\", value: formatValue(0, { format, locale }) },\n        ];\n    }\n    get selectedFormat() {\n        const proposal = this.formatProposals[this.state.selectedFormatIndex];\n        return this.state.isAccountingFormat ? proposal?.accountingFormat : proposal?.format;\n    }\n}\n\nconst dataValidationEvaluatorRegistry = new Registry();\ndataValidationEvaluatorRegistry.add(\"textContains\", {\n    type: \"textContains\",\n    isValueValid: (value, criterion) => {\n        const strValue = String(value);\n        return strValue.toLowerCase().includes(criterion.values[0].toLowerCase());\n    },\n    getErrorString: (criterion) => {\n        return _t('The value must be a text that contains \"%s\"', criterion.values[0]);\n    },\n    isCriterionValueValid: (value) => !!value,\n    criterionValueErrorString: DVTerms.CriterionError.notEmptyValue,\n    numberOfValues: () => 1,\n    name: _t(\"Text contains\"),\n    getPreview: (criterion) => _t('Text contains \"%s\"', criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"textNotContains\", {\n    type: \"textNotContains\",\n    isValueValid: (value, criterion) => {\n        const strValue = String(value);\n        return !strValue.toLowerCase().includes(criterion.values[0].toLowerCase());\n    },\n    getErrorString: (criterion) => {\n        return _t('The value must be a text that does not contain \"%s\"', criterion.values[0]);\n    },\n    isCriterionValueValid: (value) => !!value,\n    criterionValueErrorString: DVTerms.CriterionError.notEmptyValue,\n    numberOfValues: () => 1,\n    name: _t(\"Text does not contains\"),\n    getPreview: (criterion) => _t('Text does not contain \"%s\"', criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"textIs\", {\n    type: \"textIs\",\n    isValueValid: (value, criterion) => {\n        const strValue = String(value);\n        return strValue.toLowerCase() === criterion.values[0].toLowerCase();\n    },\n    getErrorString: (criterion) => {\n        return _t('The value must be exactly \"%s\"', criterion.values[0]);\n    },\n    isCriterionValueValid: (value) => !!value,\n    criterionValueErrorString: DVTerms.CriterionError.notEmptyValue,\n    numberOfValues: () => 1,\n    name: _t(\"Text is exactly\"),\n    getPreview: (criterion) => _t('Text is exactly \"%s\"', criterion.values[0]),\n});\n/** Note: this regex doesn't allow for all the RFC-compliant mail addresses but should be enough for our purpose. */\nconst emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63}$/;\ndataValidationEvaluatorRegistry.add(\"textIsEmail\", {\n    type: \"textIsEmail\",\n    isValueValid: (value) => typeof value === \"string\" && emailRegex.test(value),\n    getErrorString: () => _t(\"The value must be a valid email address\"),\n    isCriterionValueValid: () => true,\n    criterionValueErrorString: \"\",\n    numberOfValues: () => 0,\n    name: _t(\"Text is valid email\"),\n    getPreview: () => _t(\"Text is valid email\"),\n});\ndataValidationEvaluatorRegistry.add(\"textIsLink\", {\n    type: \"textIsLink\",\n    isValueValid: (value) => detectLink(value) !== undefined,\n    getErrorString: () => _t(\"The value must be a valid link\"),\n    isCriterionValueValid: () => true,\n    criterionValueErrorString: \"\",\n    numberOfValues: () => 0,\n    name: _t(\"Text is valid link\"),\n    getPreview: () => _t(\"Text is valid link\"),\n});\ndataValidationEvaluatorRegistry.add(\"dateIs\", {\n    type: \"dateIs\",\n    isValueValid: (value, criterion) => {\n        const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];\n        const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);\n        if (dateValue === undefined || criterionValue === undefined) {\n            return false;\n        }\n        if ([\"lastWeek\", \"lastMonth\", \"lastYear\"].includes(criterion.dateValue)) {\n            const today = jsDateToRoundNumber(DateTime.now());\n            return isDateBetween(dateValue, today, criterionValue);\n        }\n        return areDatesSameDay(dateValue, criterionValue);\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"The value must be the date %s\", getDateCriterionLocalizedValues(criterion, locale)[0])\n            : _t(\"The value must be %s\", DVTerms.DateIs[criterion.dateValue]);\n    },\n    isCriterionValueValid: (value) => checkValueIsDate(value),\n    criterionValueErrorString: DVTerms.CriterionError.dateValue,\n    numberOfValues: (criterion) => (criterion.dateValue === \"exactDate\" ? 1 : 0),\n    name: _t(\"Date is\"),\n    getPreview: (criterion, getters) => {\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"Date is %s\", getDateCriterionFormattedValues(criterion, getters)[0])\n            : _t(\"Date is %s\", DVTerms.DateIs[criterion.dateValue]);\n    },\n});\ndataValidationEvaluatorRegistry.add(\"dateIsBefore\", {\n    type: \"dateIsBefore\",\n    isValueValid: (value, criterion) => {\n        const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];\n        const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);\n        return (dateValue !== undefined &&\n            criterionValue !== undefined &&\n            isDateStrictlyBefore(dateValue, criterionValue));\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"The value must be a date before %s\", getDateCriterionLocalizedValues(criterion, locale)[0])\n            : _t(\"The value must be a date before %s\", DVTerms.DateIsBefore[criterion.dateValue]);\n    },\n    isCriterionValueValid: (value) => checkValueIsDate(value),\n    criterionValueErrorString: DVTerms.CriterionError.dateValue,\n    numberOfValues: (criterion) => (criterion.dateValue === \"exactDate\" ? 1 : 0),\n    name: _t(\"Date is before\"),\n    getPreview: (criterion, getters) => {\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"Date is before %s\", getDateCriterionFormattedValues(criterion, getters)[0])\n            : _t(\"Date is before %s\", DVTerms.DateIsBefore[criterion.dateValue]);\n    },\n});\ndataValidationEvaluatorRegistry.add(\"dateIsOnOrBefore\", {\n    type: \"dateIsOnOrBefore\",\n    isValueValid: (value, criterion) => {\n        const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];\n        const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);\n        return (dateValue !== undefined &&\n            criterionValue !== undefined &&\n            isDateBefore(dateValue, criterionValue));\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"The value must be a date on or before %s\", getDateCriterionLocalizedValues(criterion, locale)[0])\n            : _t(\"The value must be a date on or before %s\", DVTerms.DateIsBefore[criterion.dateValue]);\n    },\n    isCriterionValueValid: (value) => checkValueIsDate(value),\n    criterionValueErrorString: DVTerms.CriterionError.dateValue,\n    numberOfValues: (criterion) => (criterion.dateValue === \"exactDate\" ? 1 : 0),\n    name: _t(\"Date is on or before\"),\n    getPreview: (criterion, getters) => {\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"Date is on or before %s\", getDateCriterionFormattedValues(criterion, getters)[0])\n            : _t(\"Date is on or before %s\", DVTerms.DateIsBefore[criterion.dateValue]);\n    },\n});\ndataValidationEvaluatorRegistry.add(\"dateIsAfter\", {\n    type: \"dateIsAfter\",\n    isValueValid: (value, criterion) => {\n        const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];\n        const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);\n        return (dateValue !== undefined &&\n            criterionValue !== undefined &&\n            isDateStrictlyAfter(dateValue, criterionValue));\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"The value must be a date after %s\", getDateCriterionLocalizedValues(criterion, locale)[0])\n            : _t(\"The value must be a date after %s\", DVTerms.DateIsBefore[criterion.dateValue]);\n    },\n    isCriterionValueValid: (value) => checkValueIsDate(value),\n    criterionValueErrorString: DVTerms.CriterionError.dateValue,\n    numberOfValues: (criterion) => (criterion.dateValue === \"exactDate\" ? 1 : 0),\n    name: _t(\"Date is after\"),\n    getPreview: (criterion, getters) => {\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"Date is after %s\", getDateCriterionFormattedValues(criterion, getters)[0])\n            : _t(\"Date is after %s\", DVTerms.DateIsBefore[criterion.dateValue]);\n    },\n});\ndataValidationEvaluatorRegistry.add(\"dateIsOnOrAfter\", {\n    type: \"dateIsOnOrAfter\",\n    isValueValid: (value, criterion) => {\n        const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];\n        const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);\n        return (dateValue !== undefined &&\n            criterionValue !== undefined &&\n            isDateAfter(dateValue, criterionValue));\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"The value must be a date on or after %s\", getDateCriterionLocalizedValues(criterion, locale)[0])\n            : _t(\"The value must be a date on or after %s\", DVTerms.DateIsBefore[criterion.dateValue]);\n    },\n    isCriterionValueValid: (value) => checkValueIsDate(value),\n    criterionValueErrorString: DVTerms.CriterionError.dateValue,\n    numberOfValues: (criterion) => (criterion.dateValue === \"exactDate\" ? 1 : 0),\n    name: _t(\"Date is on or after\"),\n    getPreview: (criterion, getters) => {\n        return criterion.dateValue === \"exactDate\"\n            ? _t(\"Date is on or after %s\", getDateCriterionFormattedValues(criterion, getters)[0])\n            : _t(\"Date is on or after %s\", DVTerms.DateIsBefore[criterion.dateValue]);\n    },\n});\ndataValidationEvaluatorRegistry.add(\"dateIsBetween\", {\n    type: \"dateIsBetween\",\n    isValueValid: (value, criterion) => {\n        const criterionValues = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE);\n        const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);\n        if (dateValue === undefined ||\n            criterionValues[0] === undefined ||\n            criterionValues[1] === undefined) {\n            return false;\n        }\n        return isDateBetween(dateValue, criterionValues[0], criterionValues[1]);\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const criterionValues = getDateCriterionLocalizedValues(criterion, locale);\n        return _t(\"The value must be a date between %s and %s\", criterionValues[0], criterionValues[1]);\n    },\n    isCriterionValueValid: (value) => checkValueIsDate(value),\n    criterionValueErrorString: DVTerms.CriterionError.dateValue,\n    numberOfValues: () => 2,\n    name: _t(\"Date is between\"),\n    getPreview: (criterion, getters) => {\n        const values = getDateCriterionFormattedValues(criterion, getters);\n        return _t(\"Date is between %s and %s\", values[0], values[1]);\n    },\n});\ndataValidationEvaluatorRegistry.add(\"dateIsNotBetween\", {\n    type: \"dateIsNotBetween\",\n    isValueValid: (value, criterion) => {\n        const criterionValues = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE);\n        const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);\n        if (dateValue === undefined ||\n            criterionValues[0] === undefined ||\n            criterionValues[1] === undefined) {\n            return false;\n        }\n        return !isDateBetween(dateValue, criterionValues[0], criterionValues[1]);\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const criterionValues = getDateCriterionLocalizedValues(criterion, locale);\n        return _t(\"The value must be a date not between %s and %s\", criterionValues[0], criterionValues[1]);\n    },\n    isCriterionValueValid: (value) => checkValueIsDate(value),\n    criterionValueErrorString: DVTerms.CriterionError.dateValue,\n    numberOfValues: () => 2,\n    name: _t(\"Date is not between\"),\n    getPreview: (criterion, getters) => {\n        const values = getDateCriterionFormattedValues(criterion, getters);\n        return _t(\"Date is not between %s and %s\", values[0], values[1]);\n    },\n});\ndataValidationEvaluatorRegistry.add(\"dateIsValid\", {\n    type: \"dateIsValid\",\n    isValueValid: (value) => {\n        return valueToDateNumber(value, DEFAULT_LOCALE) !== undefined;\n    },\n    getErrorString: () => _t(\"The value must be a valid date\"),\n    isCriterionValueValid: (value) => checkValueIsDate(value),\n    criterionValueErrorString: \"\",\n    numberOfValues: () => 0,\n    name: _t(\"Is valid date\"),\n    getPreview: () => _t(\"Date is valid\"),\n});\ndataValidationEvaluatorRegistry.add(\"isEqual\", {\n    type: \"isEqual\",\n    isValueValid: (value, criterion) => {\n        if (typeof value !== \"number\") {\n            return false;\n        }\n        const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];\n        if (criterionValue === undefined) {\n            return false;\n        }\n        return value === criterionValue;\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const values = getNumberCriterionlocalizedValues(criterion, locale);\n        return _t(\"The value must be equal to %s\", values[0]);\n    },\n    isCriterionValueValid: (value) => checkValueIsNumber(value),\n    criterionValueErrorString: DVTerms.CriterionError.numberValue,\n    numberOfValues: () => 1,\n    name: _t(\"Is equal to\"),\n    getPreview: (criterion) => _t(\"Value is equal to %s\", criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"isNotEqual\", {\n    type: \"isNotEqual\",\n    isValueValid: (value, criterion) => {\n        if (typeof value !== \"number\") {\n            return false;\n        }\n        const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];\n        if (criterionValue === undefined) {\n            return false;\n        }\n        return value !== criterionValue;\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const values = getNumberCriterionlocalizedValues(criterion, locale);\n        return _t(\"The value must not be equal to %s\", values[0]);\n    },\n    isCriterionValueValid: (value) => checkValueIsNumber(value),\n    criterionValueErrorString: DVTerms.CriterionError.numberValue,\n    numberOfValues: () => 1,\n    name: _t(\"Is not equal to\"),\n    getPreview: (criterion) => _t(\"Value is not equal to %s\", criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"isGreaterThan\", {\n    type: \"isGreaterThan\",\n    isValueValid: (value, criterion) => {\n        if (typeof value !== \"number\") {\n            return false;\n        }\n        const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];\n        if (criterionValue === undefined) {\n            return false;\n        }\n        return value > criterionValue;\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const values = getNumberCriterionlocalizedValues(criterion, locale);\n        return _t(\"The value must be greater than %s\", values[0]);\n    },\n    isCriterionValueValid: (value) => checkValueIsNumber(value),\n    criterionValueErrorString: DVTerms.CriterionError.numberValue,\n    numberOfValues: () => 1,\n    name: _t(\"Is greater than\"),\n    getPreview: (criterion) => _t(\"Value is greater than %s\", criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"isGreaterOrEqualTo\", {\n    type: \"isGreaterOrEqualTo\",\n    isValueValid: (value, criterion) => {\n        if (typeof value !== \"number\") {\n            return false;\n        }\n        const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];\n        if (criterionValue === undefined) {\n            return false;\n        }\n        return value >= criterionValue;\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const values = getNumberCriterionlocalizedValues(criterion, locale);\n        return _t(\"The value must be greater or equal to %s\", values[0]);\n    },\n    isCriterionValueValid: (value) => checkValueIsNumber(value),\n    criterionValueErrorString: DVTerms.CriterionError.numberValue,\n    numberOfValues: () => 1,\n    name: _t(\"Is greater or equal to\"),\n    getPreview: (criterion) => _t(\"Value is greater or equal to %s\", criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"isLessThan\", {\n    type: \"isLessThan\",\n    isValueValid: (value, criterion) => {\n        if (typeof value !== \"number\") {\n            return false;\n        }\n        const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];\n        if (criterionValue === undefined) {\n            return false;\n        }\n        return value < criterionValue;\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const values = getNumberCriterionlocalizedValues(criterion, locale);\n        return _t(\"The value must be less than %s\", values[0]);\n    },\n    isCriterionValueValid: (value) => checkValueIsNumber(value),\n    criterionValueErrorString: DVTerms.CriterionError.numberValue,\n    numberOfValues: () => 1,\n    name: _t(\"Is less than\"),\n    getPreview: (criterion) => _t(\"Value is less than %s\", criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"isLessOrEqualTo\", {\n    type: \"isLessOrEqualTo\",\n    isValueValid: (value, criterion) => {\n        if (typeof value !== \"number\") {\n            return false;\n        }\n        const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];\n        if (criterionValue === undefined) {\n            return false;\n        }\n        return value <= criterionValue;\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const values = getNumberCriterionlocalizedValues(criterion, locale);\n        return _t(\"The value must be less or equal to %s\", values[0]);\n    },\n    isCriterionValueValid: (value) => checkValueIsNumber(value),\n    criterionValueErrorString: DVTerms.CriterionError.numberValue,\n    numberOfValues: () => 1,\n    name: _t(\"Is less or equal to\"),\n    getPreview: (criterion) => _t(\"Value is less or equal to %s\", criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"isBetween\", {\n    type: \"isBetween\",\n    isValueValid: (value, criterion) => {\n        if (typeof value !== \"number\") {\n            return false;\n        }\n        const criterionValues = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE);\n        if (criterionValues[0] === undefined || criterionValues[1] === undefined) {\n            return false;\n        }\n        return isNumberBetween(value, criterionValues[0], criterionValues[1]);\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const values = getNumberCriterionlocalizedValues(criterion, locale);\n        return _t(\"The value must be between %s and %s\", values[0], values[1]);\n    },\n    isCriterionValueValid: (value) => checkValueIsNumber(value),\n    criterionValueErrorString: DVTerms.CriterionError.numberValue,\n    numberOfValues: () => 2,\n    name: _t(\"Is between\"),\n    getPreview: (criterion) => _t(\"Value is between %s and %s\", criterion.values[0], criterion.values[1]),\n});\ndataValidationEvaluatorRegistry.add(\"isNotBetween\", {\n    type: \"isNotBetween\",\n    isValueValid: (value, criterion) => {\n        if (typeof value !== \"number\") {\n            return false;\n        }\n        const criterionValues = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE);\n        if (criterionValues[0] === undefined || criterionValues[1] === undefined) {\n            return false;\n        }\n        return !isNumberBetween(value, criterionValues[0], criterionValues[1]);\n    },\n    getErrorString: (criterion, getters) => {\n        const locale = getters.getLocale();\n        const values = getNumberCriterionlocalizedValues(criterion, locale);\n        return _t(\"The value must not be between %s and %s\", values[0], values[1]);\n    },\n    isCriterionValueValid: (value) => checkValueIsNumber(value),\n    criterionValueErrorString: DVTerms.CriterionError.numberValue,\n    numberOfValues: () => 2,\n    name: _t(\"Is not between\"),\n    getPreview: (criterion) => _t(\"Value is not between %s and %s\", criterion.values[0], criterion.values[1]),\n});\ndataValidationEvaluatorRegistry.add(\"isBoolean\", {\n    type: \"isBoolean\",\n    isValueValid: (value) => value === \"\" || typeof value === \"boolean\",\n    getErrorString: () => _t(\"The value must be a boolean\"),\n    isCriterionValueValid: () => true,\n    criterionValueErrorString: \"\",\n    numberOfValues: () => 0,\n    name: _t(\"Checkbox\"),\n    getPreview: () => _t(\"Checkbox\"),\n});\ndataValidationEvaluatorRegistry.add(\"isValueInList\", {\n    type: \"isValueInList\",\n    isValueValid: (value, criterion) => {\n        if (value === null) {\n            return false;\n        }\n        return criterion.values\n            .map((str) => str.toLowerCase())\n            .includes(value.toString().toLowerCase());\n    },\n    getErrorString: (criterion) => _t(\"The value must be one of: %s\", criterion.values.join(\", \")),\n    isCriterionValueValid: () => true,\n    criterionValueErrorString: \"\",\n    numberOfValues: () => undefined,\n    allowedValues: \"onlyLiterals\",\n    name: _t(\"Value in list\"),\n    getPreview: (criterion) => _t(\"Value one of: %s\", criterion.values.join(\", \")),\n});\ndataValidationEvaluatorRegistry.add(\"isValueInRange\", {\n    type: \"isValueInList\",\n    isValueValid: (value, criterion, getters, sheetId) => {\n        if (!value) {\n            return false;\n        }\n        const range = getters.getRangeFromSheetXC(sheetId, criterion.values[0]);\n        const criterionValues = getters.getRangeValues(range);\n        return criterionValues\n            .filter(isNotNull)\n            .map((value) => value.toString().toLowerCase())\n            .includes(value.toString().toLowerCase());\n    },\n    getErrorString: (criterion) => _t(\"The value must be a value in the range %s\", criterion.values[0]),\n    isCriterionValueValid: (value) => rangeReference.test(value),\n    criterionValueErrorString: DVTerms.CriterionError.validRange,\n    numberOfValues: () => 1,\n    allowedValues: \"onlyLiterals\",\n    name: _t(\"Value in range\"),\n    getPreview: (criterion) => _t(\"Value in range %s\", criterion.values[0]),\n});\ndataValidationEvaluatorRegistry.add(\"customFormula\", {\n    type: \"customFormula\",\n    isValueValid: (value, criterion) => {\n        const parsedValue = parseLiteral(criterion.values[0], DEFAULT_LOCALE);\n        if (typeof parsedValue === \"number\" || typeof parsedValue === \"boolean\") {\n            return !!parsedValue;\n        }\n        return false;\n    },\n    getErrorString: () => _t(\"The value does not match the custom formula data validation rule\"),\n    isCriterionValueValid: () => true,\n    criterionValueErrorString: \"\",\n    numberOfValues: () => 1,\n    allowedValues: \"onlyFormulas\",\n    name: _t(\"Custom formula\"),\n    getPreview: (criterion) => _t(\"Custom formula %s\", criterion.values[0]),\n});\nfunction getNumberCriterionlocalizedValues(criterion, locale) {\n    return criterion.values.map((value) => value !== undefined ? localizeContent(value, locale) : CellErrorType.InvalidReference);\n}\nfunction getDateCriterionLocalizedValues(criterion, locale) {\n    const values = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE);\n    return values.map((value) => value !== undefined\n        ? formatValue(value, { locale, format: locale.dateFormat })\n        : CellErrorType.InvalidReference);\n}\nfunction checkValueIsDate(value) {\n    const valueAsNumber = valueToDateNumber(value, DEFAULT_LOCALE);\n    return valueAsNumber !== undefined;\n}\nfunction checkValueIsNumber(value) {\n    const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);\n    return valueAsNumber !== undefined;\n}\nfunction getDateCriterionFormattedValues(criterion, getters) {\n    const locale = getters.getLocale();\n    return criterion.values.map((valueStr) => {\n        if (valueStr.startsWith(\"=\")) {\n            return valueStr;\n        }\n        const value = parseLiteral(valueStr, locale);\n        if (typeof value === \"number\") {\n            return formatValue(value, { format: locale.dateFormat, locale });\n        }\n        return \"\";\n    });\n}\n\n/** This component looks like a select input, but on click it opens a Menu with the items given as props instead of a dropdown */\nclass SelectMenu extends Component {\n    static template = \"o-spreadsheet-SelectMenu\";\n    static props = {\n        menuItems: Array,\n        selectedValue: String,\n        class: { type: String, optional: true },\n    };\n    static components = { Menu };\n    menuId = new UuidGenerator().uuidv4();\n    selectRef = useRef(\"select\");\n    selectRect = useAbsoluteBoundingRect(this.selectRef);\n    state = useState({\n        isMenuOpen: false,\n    });\n    onClick(ev) {\n        if (ev.closedMenuId === this.menuId) {\n            return;\n        }\n        this.state.isMenuOpen = !this.state.isMenuOpen;\n    }\n    onMenuClosed() {\n        this.state.isMenuOpen = false;\n    }\n    get menuPosition() {\n        return {\n            x: this.selectRect.x,\n            y: this.selectRect.y + this.selectRect.height,\n        };\n    }\n}\n\nclass DataValidationCriterionForm extends Component {\n    static props = {\n        criterion: Object,\n        onCriterionChanged: Function,\n    };\n    setup() {\n        const composerFocusStore = useStore(ComposerFocusStore);\n        onMounted(() => {\n            composerFocusStore.activeComposer.stopEdition();\n        });\n    }\n    updateCriterion(criterion) {\n        const filteredCriterion = {\n            ...this.props.criterion,\n            ...criterion,\n        };\n        this.props.onCriterionChanged(filteredCriterion);\n    }\n}\n\ncss /* scss */ `\n  .o-dv-input {\n    .o-invalid {\n      background-color: #ffdddd;\n    }\n    .error-icon {\n      right: 7px;\n      top: 7px;\n    }\n  }\n`;\nclass DataValidationInput extends Component {\n    static template = \"o-spreadsheet-DataValidationInput\";\n    static props = {\n        value: { type: String, optional: true },\n        criterionType: String,\n        onValueChanged: Function,\n        onKeyDown: { type: Function, optional: true },\n        focused: { type: Boolean, optional: true },\n        onBlur: { type: Function, optional: true },\n        onFocus: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        value: \"\",\n        onKeyDown: () => { },\n        focused: false,\n        onBlur: () => { },\n    };\n    inputRef = useRef(\"input\");\n    setup() {\n        useEffect(() => {\n            if (this.props.focused) {\n                this.inputRef.el.focus();\n            }\n        }, () => [this.props.focused, this.inputRef.el]);\n    }\n    state = useState({\n        shouldDisplayError: !!this.props.value, // Don't display error if user inputted nothing yet\n    });\n    onValueChanged(ev) {\n        this.state.shouldDisplayError = true;\n        this.props.onValueChanged(ev.target.value);\n    }\n    get placeholder() {\n        const evaluator = dataValidationEvaluatorRegistry.get(this.props.criterionType);\n        if (evaluator.allowedValues === \"onlyFormulas\") {\n            return _t(\"Formula\");\n        }\n        else if (evaluator.allowedValues === \"onlyLiterals\") {\n            return _t(\"Value\");\n        }\n        return _t(\"Value or formula\");\n    }\n    get errorMessage() {\n        if (!this.state.shouldDisplayError) {\n            return undefined;\n        }\n        return this.env.model.getters.getDataValidationInvalidCriterionValueMessage(this.props.criterionType, canonicalizeContent(this.props.value, this.env.model.getters.getLocale()));\n    }\n}\n\nconst DATES_VALUES = {\n    today: _t(\"today\"),\n    yesterday: _t(\"yesterday\"),\n    tomorrow: _t(\"tomorrow\"),\n    lastWeek: _t(\"in the past week\"),\n    lastMonth: _t(\"in the past month\"),\n    lastYear: _t(\"in the past year\"),\n    exactDate: _t(\"exact date\"),\n};\nclass DataValidationDateCriterionForm extends DataValidationCriterionForm {\n    static template = \"o-spreadsheet-DataValidationDateCriterion\";\n    static components = { DataValidationInput };\n    setup() {\n        super.setup();\n        const setupDefault = (props) => {\n            if (props.criterion.dateValue === undefined) {\n                this.updateCriterion({ dateValue: \"exactDate\" });\n            }\n        };\n        onWillUpdateProps(setupDefault);\n        onWillStart(() => setupDefault(this.props));\n    }\n    onValueChanged(value) {\n        this.updateCriterion({ values: [value] });\n    }\n    onDateValueChanged(ev) {\n        const dateValue = ev.target.value;\n        this.updateCriterion({ dateValue });\n    }\n    get dateValues() {\n        return Object.keys(DATES_VALUES).map((key) => ({\n            value: key,\n            title: DATES_VALUES[key],\n        }));\n    }\n}\n\nclass DataValidationDoubleInputCriterionForm extends DataValidationCriterionForm {\n    static template = \"o-spreadsheet-DataValidationDoubleInput\";\n    static components = { DataValidationInput };\n    onFirstValueChanged(value) {\n        const values = this.props.criterion.values;\n        this.updateCriterion({\n            values: [value, values[1]],\n        });\n    }\n    onSecondValueChanged(value) {\n        const values = this.props.criterion.values;\n        this.updateCriterion({\n            values: [values[0], value],\n        });\n    }\n}\n\nclass DataValidationSingleInputCriterionForm extends DataValidationCriterionForm {\n    static template = \"o-spreadsheet-DataValidationSingleInput\";\n    static components = { DataValidationInput };\n    onValueChanged(value) {\n        const criterion = deepCopy(this.props.criterion);\n        criterion.values[0] = value;\n        this.updateCriterion(criterion);\n    }\n}\n\ncss /* scss */ `\n  .o-dv-list-item-delete {\n    color: #666666;\n    cursor: pointer;\n  }\n`;\nclass DataValidationListCriterionForm extends DataValidationCriterionForm {\n    static template = \"o-spreadsheet-DataValidationListCriterionForm\";\n    static components = { DataValidationInput };\n    state = useState({\n        numberOfValues: Math.max(this.props.criterion.values.length, 2),\n    });\n    setup() {\n        super.setup();\n        const setupDefault = (props) => {\n            if (props.criterion.displayStyle === undefined) {\n                this.updateCriterion({ displayStyle: \"arrow\" });\n            }\n        };\n        onWillUpdateProps(setupDefault);\n        onWillStart(() => setupDefault(this.props));\n    }\n    onValueChanged(value, index) {\n        const values = [...this.displayedValues];\n        values[index] = value;\n        this.updateCriterion({ values });\n    }\n    onAddAnotherValue() {\n        this.state.numberOfValues++;\n    }\n    removeItem(index) {\n        const values = [...this.displayedValues];\n        values.splice(index, 1);\n        this.state.numberOfValues--;\n        this.updateCriterion({ values });\n    }\n    onChangedDisplayStyle(ev) {\n        const displayStyle = ev.target.value;\n        this.updateCriterion({ displayStyle });\n    }\n    onKeyDown(ev, index) {\n        if ((ev.key === \"Enter\" || ev.key === \"Tab\") && index === this.state.numberOfValues - 1) {\n            this.onAddAnotherValue();\n            this.state.focusedValueIndex = index + 1;\n            ev.preventDefault();\n        }\n        else if (ev.key === \"Enter\") {\n            this.state.focusedValueIndex = index + 1;\n        }\n    }\n    onBlurInput() {\n        this.state.focusedValueIndex = undefined;\n    }\n    get displayedValues() {\n        const values = [];\n        for (let i = 0; i < this.state.numberOfValues; i++) {\n            values.push(this.props.criterion.values[i] || \"\");\n        }\n        return values;\n    }\n}\n\nclass DataValidationValueInRangeCriterionForm extends DataValidationCriterionForm {\n    static template = \"o-spreadsheet-DataValidationValueInRangeCriterionForm\";\n    static components = { SelectionInput };\n    setup() {\n        super.setup();\n        const setupDefault = (props) => {\n            if (props.criterion.displayStyle === undefined) {\n                this.updateCriterion({ displayStyle: \"arrow\" });\n            }\n        };\n        onWillUpdateProps(setupDefault);\n        onWillStart(() => setupDefault(this.props));\n    }\n    onRangeChanged(rangeXc) {\n        this.updateCriterion({ values: [rangeXc] });\n    }\n    onChangedDisplayStyle(ev) {\n        const displayStyle = ev.target.value;\n        this.updateCriterion({ displayStyle });\n    }\n}\n\nconst dvCriterionCategoriesSequences = {\n    list: 10,\n    text: 20,\n    date: 30,\n    number: 40,\n    misc: 50,\n};\nconst dataValidationPanelCriteriaRegistry = new Registry();\ndataValidationPanelCriteriaRegistry.add(\"textContains\", {\n    type: \"textContains\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"text\",\n    sequence: 10,\n});\ndataValidationPanelCriteriaRegistry.add(\"textNotContains\", {\n    type: \"textNotContains\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"text\",\n    sequence: 20,\n});\ndataValidationPanelCriteriaRegistry.add(\"textIs\", {\n    type: \"textIs\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"text\",\n    sequence: 30,\n});\ndataValidationPanelCriteriaRegistry.add(\"textIsEmail\", {\n    type: \"textIsEmail\",\n    component: undefined,\n    category: \"text\",\n    sequence: 40,\n});\ndataValidationPanelCriteriaRegistry.add(\"textIsLink\", {\n    type: \"textIsLink\",\n    component: undefined,\n    category: \"text\",\n    sequence: 50,\n});\ndataValidationPanelCriteriaRegistry.add(\"dateIs\", {\n    type: \"dateIs\",\n    component: DataValidationDateCriterionForm,\n    category: \"date\",\n    sequence: 20,\n});\ndataValidationPanelCriteriaRegistry.add(\"dateIsBefore\", {\n    type: \"dateIsBefore\",\n    component: DataValidationDateCriterionForm,\n    category: \"date\",\n    sequence: 30,\n});\ndataValidationPanelCriteriaRegistry.add(\"dateIsOnOrBefore\", {\n    type: \"dateIsOnOrBefore\",\n    component: DataValidationDateCriterionForm,\n    category: \"date\",\n    sequence: 40,\n});\ndataValidationPanelCriteriaRegistry.add(\"dateIsAfter\", {\n    type: \"dateIsAfter\",\n    component: DataValidationDateCriterionForm,\n    category: \"date\",\n    sequence: 50,\n});\ndataValidationPanelCriteriaRegistry.add(\"dateIsOnOrAfter\", {\n    type: \"dateIsOnOrAfter\",\n    component: DataValidationDateCriterionForm,\n    category: \"date\",\n    sequence: 60,\n});\ndataValidationPanelCriteriaRegistry.add(\"dateIsBetween\", {\n    type: \"dateIsBetween\",\n    component: DataValidationDoubleInputCriterionForm,\n    category: \"date\",\n    sequence: 70,\n});\ndataValidationPanelCriteriaRegistry.add(\"dateIsNotBetween\", {\n    type: \"dateIsNotBetween\",\n    component: DataValidationDoubleInputCriterionForm,\n    category: \"date\",\n    sequence: 80,\n});\ndataValidationPanelCriteriaRegistry.add(\"dateIsValid\", {\n    type: \"dateIsValid\",\n    component: undefined,\n    category: \"date\",\n    sequence: 10,\n});\ndataValidationPanelCriteriaRegistry.add(\"isEqual\", {\n    type: \"isEqual\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"number\",\n    sequence: 10,\n});\ndataValidationPanelCriteriaRegistry.add(\"isNotEqual\", {\n    type: \"isNotEqual\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"number\",\n    sequence: 20,\n});\ndataValidationPanelCriteriaRegistry.add(\"isGreaterThan\", {\n    type: \"isGreaterThan\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"number\",\n    sequence: 50,\n});\ndataValidationPanelCriteriaRegistry.add(\"isGreaterOrEqualTo\", {\n    type: \"isGreaterOrEqualTo\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"number\",\n    sequence: 60,\n});\ndataValidationPanelCriteriaRegistry.add(\"isLessThan\", {\n    type: \"isLessThan\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"number\",\n    sequence: 30,\n});\ndataValidationPanelCriteriaRegistry.add(\"isLessOrEqualTo\", {\n    type: \"isLessOrEqualTo\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"number\",\n    sequence: 40,\n});\ndataValidationPanelCriteriaRegistry.add(\"isBetween\", {\n    type: \"isBetween\",\n    component: DataValidationDoubleInputCriterionForm,\n    category: \"number\",\n    sequence: 70,\n});\ndataValidationPanelCriteriaRegistry.add(\"isNotBetween\", {\n    type: \"isNotBetween\",\n    component: DataValidationDoubleInputCriterionForm,\n    category: \"number\",\n    sequence: 80,\n});\ndataValidationPanelCriteriaRegistry.add(\"isBoolean\", {\n    type: \"isBoolean\",\n    component: undefined,\n    category: \"misc\",\n    sequence: 10,\n});\ndataValidationPanelCriteriaRegistry.add(\"isValueInList\", {\n    type: \"isValueInList\",\n    component: DataValidationListCriterionForm,\n    category: \"list\",\n    sequence: 10,\n});\ndataValidationPanelCriteriaRegistry.add(\"isValueInRange\", {\n    type: \"isValueInRange\",\n    component: DataValidationValueInRangeCriterionForm,\n    category: \"list\",\n    sequence: 20,\n});\ndataValidationPanelCriteriaRegistry.add(\"customFormula\", {\n    type: \"customFormula\",\n    component: DataValidationSingleInputCriterionForm,\n    category: \"misc\",\n    sequence: 20,\n});\nfunction getDataValidationCriterionMenuItems(callback) {\n    const items = dataValidationPanelCriteriaRegistry.getAll().sort((a, b) => {\n        if (a.category === b.category) {\n            return a.sequence - b.sequence;\n        }\n        return dvCriterionCategoriesSequences[a.category] - dvCriterionCategoriesSequences[b.category];\n    });\n    const actionSpecs = items.map((item, index) => {\n        const evaluator = dataValidationEvaluatorRegistry.get(item.type);\n        return {\n            name: evaluator.name,\n            id: item.type,\n            separator: item.category !== items[index + 1]?.category,\n            execute: () => callback(item.type),\n        };\n    });\n    return createActions(actionSpecs);\n}\n\nclass DataValidationEditor extends Component {\n    static template = \"o-spreadsheet-DataValidationEditor\";\n    static components = { SelectionInput, SelectMenu, Section };\n    static props = {\n        rule: { type: Object, optional: true },\n        onExit: Function,\n        onCloseSidePanel: { type: Function, optional: true },\n    };\n    state = useState({ rule: this.defaultDataValidationRule });\n    setup() {\n        if (this.props.rule) {\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            this.state.rule = {\n                ...this.props.rule,\n                ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, sheetId)),\n            };\n            this.state.rule.criterion.type = this.props.rule.criterion.type;\n        }\n    }\n    onCriterionTypeChanged(type) {\n        this.state.rule.criterion.type = type;\n    }\n    onRangesChanged(ranges) {\n        this.state.rule.ranges = ranges;\n    }\n    onCriterionChanged(criterion) {\n        this.state.rule.criterion = criterion;\n    }\n    changeRuleIsBlocking(ev) {\n        const isBlocking = ev.target.value;\n        this.state.rule.isBlocking = isBlocking === \"true\";\n    }\n    onSave() {\n        if (!this.canSave) {\n            return;\n        }\n        this.env.model.dispatch(\"ADD_DATA_VALIDATION_RULE\", this.dispatchPayload);\n        this.props.onExit();\n    }\n    get canSave() {\n        return this.env.model.canDispatch(\"ADD_DATA_VALIDATION_RULE\", this.dispatchPayload)\n            .isSuccessful;\n    }\n    get dispatchPayload() {\n        const rule = { ...this.state.rule, ranges: undefined };\n        const locale = this.env.model.getters.getLocale();\n        const criterion = rule.criterion;\n        const criterionEvaluator = dataValidationEvaluatorRegistry.get(criterion.type);\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const values = criterion.values\n            .slice(0, criterionEvaluator.numberOfValues(criterion))\n            .map((value) => value?.trim())\n            .filter((value) => value !== \"\" && value !== undefined)\n            .map((value) => canonicalizeContent(value, locale));\n        rule.criterion = { ...criterion, values };\n        return {\n            sheetId,\n            ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),\n            rule,\n        };\n    }\n    get dvCriterionMenuItems() {\n        return getDataValidationCriterionMenuItems((type) => this.onCriterionTypeChanged(type));\n    }\n    get selectedCriterionName() {\n        const selectedType = this.state.rule.criterion.type;\n        return dataValidationEvaluatorRegistry.get(selectedType).name;\n    }\n    get defaultDataValidationRule() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const ranges = this.env.model.getters\n            .getSelectedZones()\n            .map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone)));\n        return {\n            id: this.env.model.uuidGenerator.uuidv4(),\n            criterion: { type: \"textContains\", values: [\"\"] },\n            ranges,\n        };\n    }\n    get criterionComponent() {\n        return dataValidationPanelCriteriaRegistry.get(this.state.rule.criterion.type).component;\n    }\n}\n\ncss /* scss */ `\n  .o-sidePanel {\n    .o-dv-preview {\n      height: 70px;\n      box-sizing: border-box;\n      cursor: pointer;\n      border-bottom: 1px solid ${FIGURE_BORDER_COLOR};\n\n      .o-dv-container {\n        min-width: 0; // otherwise flex won't shrink correctly\n      }\n\n      .o-dv-preview-description {\n        font-size: 13px;\n      }\n\n      &:hover {\n        background-color: rgba(0, 0, 0, 0.08);\n      }\n\n      &:not(:hover) .o-dv-preview-delete {\n        display: none !important;\n      }\n    }\n  }\n`;\nclass DataValidationPreview extends Component {\n    static template = \"o-spreadsheet-DataValidationPreview\";\n    static props = {\n        onClick: Function,\n        rule: Object,\n    };\n    ref = useRef(\"dvPreview\");\n    setup() {\n        useHighlightsOnHover(this.ref, this);\n    }\n    deleteDataValidation() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        this.env.model.dispatch(\"REMOVE_DATA_VALIDATION_RULE\", { sheetId, id: this.props.rule.id });\n    }\n    get highlights() {\n        return this.props.rule.ranges.map((range) => ({\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            zone: range.zone,\n            color: HIGHLIGHT_COLOR,\n            fillAlpha: 0.06,\n        }));\n    }\n    get rangesString() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.props.rule.ranges\n            .map((range) => this.env.model.getters.getRangeString(range, sheetId))\n            .join(\", \");\n    }\n    get descriptionString() {\n        return dataValidationEvaluatorRegistry\n            .get(this.props.rule.criterion.type)\n            .getPreview(this.props.rule.criterion, this.env.model.getters);\n    }\n}\n\nclass DataValidationPanel extends Component {\n    static template = \"o-spreadsheet-DataValidationPanel\";\n    static props = {\n        onCloseSidePanel: Function,\n    };\n    static components = { DataValidationPreview, DataValidationEditor };\n    state = useState({ mode: \"list\", activeRule: undefined });\n    onPreviewClick(id) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const rule = this.env.model.getters.getDataValidationRule(sheetId, id);\n        if (rule) {\n            this.state.mode = \"edit\";\n            this.state.activeRule = rule;\n        }\n    }\n    addDataValidationRule() {\n        this.state.mode = \"edit\";\n        this.state.activeRule = undefined;\n    }\n    onExitEditMode() {\n        this.state.mode = \"list\";\n        this.state.activeRule = undefined;\n    }\n    localizeDVRule(rule) {\n        if (!rule)\n            return rule;\n        const locale = this.env.model.getters.getLocale();\n        return localizeDataValidationRule(rule, locale);\n    }\n    get validationRules() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.env.model.getters.getDataValidationRules(sheetId);\n    }\n}\n\nconst FIND_AND_REPLACE_HIGHLIGHT_COLOR = \"#8B008B\";\nvar Direction;\n(function (Direction) {\n    Direction[Direction[\"previous\"] = -1] = \"previous\";\n    Direction[Direction[\"current\"] = 0] = \"current\";\n    Direction[Direction[\"next\"] = 1] = \"next\";\n})(Direction || (Direction = {}));\nclass FindAndReplaceStore extends SpreadsheetStore {\n    mutators = [\n        \"updateSearchOptions\",\n        \"updateSearchContent\",\n        \"searchFormulas\",\n        \"selectPreviousMatch\",\n        \"selectNextMatch\",\n        \"replace\",\n    ];\n    allSheetsMatches = [];\n    activeSheetMatches = [];\n    specificRangeMatches = [];\n    currentSearchRegex = null;\n    isSearchDirty = false;\n    initialShowFormulaState;\n    preserveSelectedMatchIndex = false;\n    irreplaceableMatchCount = 0;\n    notificationStore = this.get(NotificationStore);\n    // fixme: why do we make selectedMatchIndex on top of a selected\n    // property in the matches?\n    selectedMatchIndex = null;\n    toSearch = \"\";\n    toReplace = \"\";\n    searchOptions = {\n        matchCase: false,\n        exactMatch: false,\n        searchFormulas: false,\n        searchScope: \"activeSheet\",\n        specificRange: undefined,\n    };\n    constructor(get) {\n        super(get);\n        this.initialShowFormulaState = this.model.getters.shouldShowFormulas();\n        this.searchOptions.searchFormulas = this.initialShowFormulaState;\n        const highlightStore = get(HighlightStore);\n        highlightStore.register(this);\n        this.onDispose(() => {\n            this.model.dispatch(\"SET_FORMULA_VISIBILITY\", { show: this.initialShowFormulaState });\n            highlightStore.unRegister(this);\n        });\n    }\n    get searchMatches() {\n        switch (this.searchOptions.searchScope) {\n            case \"allSheets\":\n                return this.allSheetsMatches;\n            case \"activeSheet\":\n                return this.activeSheetMatches;\n            case \"specificRange\":\n                return this.specificRangeMatches;\n        }\n    }\n    updateSearchContent(toSearch) {\n        this._updateSearch(toSearch, this.searchOptions);\n    }\n    updateSearchOptions(searchOptions) {\n        this._updateSearch(this.toSearch, { ...this.searchOptions, ...searchOptions });\n    }\n    searchFormulas(showFormula) {\n        this.model.dispatch(\"SET_FORMULA_VISIBILITY\", { show: showFormula });\n        this.updateSearchOptions({ searchFormulas: showFormula });\n    }\n    selectPreviousMatch() {\n        this.selectNextCell(Direction.previous);\n    }\n    selectNextMatch() {\n        this.selectNextCell(Direction.next);\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SET_FORMULA_VISIBILITY\":\n                this.updateSearchOptions({ searchFormulas: cmd.show });\n                break;\n            case \"UNDO\":\n            case \"REDO\":\n            case \"REMOVE_TABLE\":\n            case \"UPDATE_FILTER\":\n            case \"REMOVE_COLUMNS_ROWS\":\n            case \"HIDE_COLUMNS_ROWS\":\n            case \"UNHIDE_COLUMNS_ROWS\":\n            case \"ADD_COLUMNS_ROWS\":\n            case \"EVALUATE_CELLS\":\n            case \"UPDATE_CELL\":\n            case \"ACTIVATE_SHEET\":\n                this.isSearchDirty = true;\n                break;\n            case \"REPLACE_SEARCH\":\n                for (const match of cmd.matches) {\n                    this.replaceMatch(match, cmd.searchString, cmd.replaceWith, cmd.searchOptions);\n                }\n                if (this.irreplaceableMatchCount > 0) {\n                    this.showReplaceWarningMessage(cmd.matches.length, this.irreplaceableMatchCount);\n                }\n                this.irreplaceableMatchCount = 0;\n                break;\n        }\n    }\n    finalize() {\n        if (this.isSearchDirty) {\n            this.refreshSearch(false);\n            this.isSearchDirty = false;\n        }\n    }\n    get allSheetMatchesCount() {\n        return this.allSheetsMatches.length;\n    }\n    get activeSheetMatchesCount() {\n        return this.activeSheetMatches.length;\n    }\n    get specificRangeMatchesCount() {\n        return this.specificRangeMatches.length;\n    }\n    // ---------------------------------------------------------------------------\n    // Search\n    // ---------------------------------------------------------------------------\n    /**\n     * Will update the current searchOptions and accordingly update the regex.\n     * It will then search for matches using the regex and store them.\n     */\n    _updateSearch(toSearch, searchOptions) {\n        this.searchOptions = searchOptions;\n        if (toSearch !== this.toSearch) {\n            this.selectedMatchIndex = null;\n        }\n        this.toSearch = toSearch;\n        this.currentSearchRegex = getSearchRegex(this.toSearch, this.searchOptions);\n        this.refreshSearch();\n    }\n    /**\n     * refresh the matches according to the current search options\n     */\n    refreshSearch(jumpToMatchSheet = true) {\n        if (!this.preserveSelectedMatchIndex) {\n            this.selectedMatchIndex = null;\n        }\n        this.findMatches();\n        this.selectNextCell(Direction.current, jumpToMatchSheet);\n    }\n    getSheetsInSearchOrder() {\n        switch (this.searchOptions.searchScope) {\n            case \"allSheets\":\n                const sheetIds = this.getters.getSheetIds();\n                const activeSheetIndex = sheetIds.findIndex((id) => id === this.getters.getActiveSheetId());\n                return [\n                    sheetIds[activeSheetIndex],\n                    ...sheetIds.slice(activeSheetIndex + 1),\n                    ...sheetIds.slice(0, activeSheetIndex),\n                ];\n            case \"activeSheet\":\n                return [this.getters.getActiveSheetId()];\n            case \"specificRange\":\n                const specificRange = this.searchOptions.specificRange;\n                if (!specificRange) {\n                    return [];\n                }\n                return specificRange ? [specificRange.sheetId] : [];\n        }\n    }\n    /**\n     * Find matches using the current regex\n     */\n    findMatches() {\n        const matches = [];\n        if (this.toSearch) {\n            for (const sheetId of this.getters.getSheetIds()) {\n                matches.push(...this.findMatchesInSheet(sheetId));\n            }\n        }\n        // set results\n        this.allSheetsMatches = matches;\n        this.activeSheetMatches = matches.filter((match) => match.sheetId === this.getters.getActiveSheetId());\n        if (this.searchOptions.specificRange) {\n            const { sheetId, zone } = this.searchOptions.specificRange;\n            this.specificRangeMatches = matches.filter((match) => match.sheetId === sheetId && isInside(match.col, match.row, zone));\n        }\n        else {\n            this.specificRangeMatches = [];\n        }\n    }\n    findMatchesInSheet(sheetId) {\n        const matches = [];\n        const { left, right, top, bottom } = this.getters.getSheetZone(sheetId);\n        for (let row = top; row <= bottom; row++) {\n            for (let col = left; col <= right; col++) {\n                const isColHidden = this.getters.isColHidden(sheetId, col);\n                const isRowHidden = this.getters.isRowHidden(sheetId, row);\n                if (isColHidden || isRowHidden) {\n                    continue;\n                }\n                const cellPosition = { sheetId, col, row };\n                if (this.currentSearchRegex?.test(this.getSearchableString(cellPosition))) {\n                    const match = { sheetId, col, row };\n                    matches.push(match);\n                }\n            }\n        }\n        return matches;\n    }\n    /**\n     * Changes the selected search cell. Given a direction it will\n     * Change the selection to the previous, current or nextCell,\n     * if it exists otherwise it will set the selectedMatchIndex to null.\n     * It will also reset the index to 0 if the search has changed.\n     * It is also used to keep coherence between the selected searchMatch\n     * and selectedMatchIndex.\n     */\n    selectNextCell(indexChange, jumpToMatchSheet = true) {\n        const matches = this.searchMatches;\n        if (!matches.length) {\n            this.selectedMatchIndex = null;\n            return;\n        }\n        let nextIndex;\n        if (this.selectedMatchIndex === null) {\n            let nextMatchIndex = -1;\n            // if search is not available in current sheet will select in next sheet\n            for (const sheetId of this.getSheetsInSearchOrder()) {\n                nextMatchIndex = matches.findIndex((match) => match.sheetId === sheetId);\n                if (nextMatchIndex !== -1) {\n                    break;\n                }\n            }\n            nextIndex = nextMatchIndex;\n        }\n        else {\n            nextIndex = this.selectedMatchIndex + indexChange;\n        }\n        // loop index value inside the array (index -1 => last index)\n        nextIndex = (nextIndex + matches.length) % matches.length;\n        this.selectedMatchIndex = nextIndex;\n        const selectedMatch = matches[nextIndex];\n        // Switch to the sheet where the match is located\n        if (jumpToMatchSheet && this.getters.getActiveSheetId() !== selectedMatch.sheetId) {\n            // We set `preserveSelectedMatchIndex` to true to avoid resetting the selected search\n            // index in the `refreshSearch` function when a new sheet is activated. The reason being\n            // that, when we automatically go back to previous sheet while performing a search, the\n            // search index is reset to the first occurrence each time.\n            this.preserveSelectedMatchIndex = true;\n            this.model.dispatch(\"ACTIVATE_SHEET\", {\n                sheetIdFrom: this.getters.getActiveSheetId(),\n                sheetIdTo: selectedMatch.sheetId,\n            });\n            this.preserveSelectedMatchIndex = false;\n            // We do not want to reset the selection at finalize in this case\n            this.isSearchDirty = false;\n        }\n        // we want grid selection to capture the selection stream\n        this.model.selection.getBackToDefault();\n        this.model.selection.selectCell(selectedMatch.col, selectedMatch.row);\n    }\n    /**\n     * Replace the value of the currently selected match\n     */\n    replace() {\n        if (this.selectedMatchIndex === null) {\n            return;\n        }\n        this.model.dispatch(\"REPLACE_SEARCH\", {\n            searchString: this.toSearch,\n            replaceWith: this.toReplace,\n            matches: [this.searchMatches[this.selectedMatchIndex]],\n            searchOptions: this.searchOptions,\n        });\n        this.selectNextCell(Direction.next);\n    }\n    /**\n     * Apply the replace function to all the matches one time.\n     */\n    replaceAll() {\n        this.model.dispatch(\"REPLACE_SEARCH\", {\n            searchString: this.toSearch,\n            replaceWith: this.toReplace,\n            matches: this.searchMatches,\n            searchOptions: this.searchOptions,\n        });\n    }\n    /**\n     * Show a warning message based on the number of matches replaced and irreplaceable.\n     */\n    showReplaceWarningMessage(totalMatches, irreplaceableMatches) {\n        const replaceableMatches = totalMatches - irreplaceableMatches;\n        if (replaceableMatches === 0) {\n            this.notificationStore.notifyUser({\n                type: \"warning\",\n                sticky: false,\n                text: _t(\"Match(es) cannot be replaced as they are part of a formula.\"),\n            });\n        }\n        else {\n            this.notificationStore.notifyUser({\n                type: \"warning\",\n                sticky: false,\n                text: _t(\"%(replaceable_count)s match(es) replaced. %(irreplaceable_count)s match(es) cannot be replaced as they are part of a formula.\", {\n                    replaceable_count: replaceableMatches,\n                    irreplaceable_count: irreplaceableMatches,\n                }),\n            });\n        }\n    }\n    replaceMatch(selectedMatch, searchString, replaceWith, searchOptions) {\n        const cell = this.getters.getCell(selectedMatch);\n        if (!cell?.content) {\n            return;\n        }\n        if (cell?.isFormula && !searchOptions.searchFormulas) {\n            this.irreplaceableMatchCount++;\n            return;\n        }\n        const searchRegex = getSearchRegex(searchString, searchOptions);\n        const replaceRegex = new RegExp(searchRegex.source, searchRegex.flags + \"g\");\n        const toReplace = this.getters.getCellText(selectedMatch, {\n            showFormula: searchOptions.searchFormulas,\n        });\n        const content = toReplace.replace(replaceRegex, replaceWith);\n        const canonicalContent = canonicalizeNumberContent(content, this.getters.getLocale());\n        this.model.dispatch(\"UPDATE_CELL\", { ...selectedMatch, content: canonicalContent });\n    }\n    getSearchableString(position) {\n        return this.getters.getCellText(position, { showFormula: this.searchOptions.searchFormulas });\n    }\n    // ---------------------------------------------------------------------------\n    // Grid rendering\n    // ---------------------------------------------------------------------------\n    get highlights() {\n        const highlights = [];\n        const sheetId = this.getters.getActiveSheetId();\n        for (const [index, match] of this.searchMatches.entries()) {\n            if (match.sheetId !== sheetId) {\n                continue; // Skip drawing matches from other sheets\n            }\n            const zone = positionToZone(match);\n            const zoneWithMerge = this.getters.expandZone(sheetId, zone);\n            const { width, height } = this.getters.getVisibleRect(zoneWithMerge);\n            if (width > 0 && height > 0) {\n                highlights.push({\n                    sheetId,\n                    zone: zoneWithMerge,\n                    color: FIND_AND_REPLACE_HIGHLIGHT_COLOR,\n                    noBorder: index !== this.selectedMatchIndex,\n                    thinLine: true,\n                    fillAlpha: 0.2,\n                });\n            }\n        }\n        if (this.searchOptions.searchScope === \"specificRange\") {\n            const range = this.searchOptions.specificRange;\n            if (range && range.sheetId === sheetId) {\n                highlights.push({\n                    sheetId,\n                    zone: range.zone,\n                    color: FIND_AND_REPLACE_HIGHLIGHT_COLOR,\n                    noFill: true,\n                    thinLine: true,\n                });\n            }\n        }\n        return highlights;\n    }\n}\n\ncss /* scss */ `\n  .o-find-and-replace {\n    outline: none;\n    height: 100%;\n    .o-input-search-container {\n      display: flex;\n      .o-input-with-count {\n        flex-grow: 1;\n        width: auto;\n      }\n      .o-input-without-count {\n        width: 100%;\n      }\n      .o-input-count {\n        width: fit-content;\n        padding: 4px 0 4px 4px;\n        white-space: nowrap;\n      }\n    }\n\n    .o-result-buttons {\n      .o-button {\n        height: 19px;\n        width: 19px;\n        .o-icon {\n          height: 14px;\n          width: 14px;\n        }\n      }\n    }\n  }\n`;\nclass FindAndReplacePanel extends Component {\n    static template = \"o-spreadsheet-FindAndReplacePanel\";\n    static components = { SelectionInput, Section, Checkbox, ValidationMessages };\n    static props = {\n        onCloseSidePanel: Function,\n    };\n    searchInput = useRef(\"searchInput\");\n    store;\n    state;\n    updateSearchContent;\n    get hasSearchResult() {\n        return this.store.selectedMatchIndex !== null;\n    }\n    get searchOptions() {\n        return this.store.searchOptions;\n    }\n    get allSheetsMatchesCount() {\n        return _t(\"%s matches in all sheets\", this.store.allSheetMatchesCount);\n    }\n    get currentSheetMatchesCount() {\n        return _t(\"%(matches)s matches in %(sheetName)s\", {\n            matches: this.store.activeSheetMatchesCount,\n            sheetName: this.env.model.getters.getSheetName(this.env.model.getters.getActiveSheetId()),\n        });\n    }\n    get specificRangeMatchesCount() {\n        const range = this.searchOptions.specificRange;\n        if (!range) {\n            return \"\";\n        }\n        const { sheetId, zone } = range;\n        return _t(\"%(matches)s matches in range %(range)s of %(sheetName)s\", {\n            matches: this.store.specificRangeMatchesCount,\n            range: zoneToXc(zone),\n            sheetName: this.env.model.getters.getSheetName(sheetId),\n        });\n    }\n    get searchInfo() {\n        if (!this.store.toSearch) {\n            return [];\n        }\n        return [\n            this.specificRangeMatchesCount,\n            this.currentSheetMatchesCount,\n            this.allSheetsMatchesCount,\n        ];\n    }\n    setup() {\n        this.store = useLocalStore(FindAndReplaceStore);\n        this.state = useState({ dataRange: \"\" });\n        onMounted(() => this.searchInput.el?.focus());\n        onWillUnmount(() => this.updateSearchContent.stopDebounce());\n        this.updateSearchContent = debounce(this.store.updateSearchContent, 200);\n    }\n    onFocusSearch() {\n        this.updateDataRange();\n    }\n    onSearchInput(ev) {\n        this.updateSearchContent(ev.target.value);\n    }\n    onKeydownSearch(ev) {\n        if (ev.key === \"Enter\") {\n            ev.preventDefault();\n            ev.stopPropagation();\n            ev.shiftKey ? this.store.selectPreviousMatch() : this.store.selectNextMatch();\n        }\n    }\n    onKeydownReplace(ev) {\n        if (ev.key === \"Enter\") {\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.store.replace();\n        }\n    }\n    searchFormulas(searchFormulas) {\n        this.store.searchFormulas(searchFormulas);\n    }\n    searchExactMatch(exactMatch) {\n        this.store.updateSearchOptions({ exactMatch });\n    }\n    searchMatchCase(matchCase) {\n        this.store.updateSearchOptions({ matchCase });\n    }\n    changeSearchScope(ev) {\n        const searchScope = ev.target.value;\n        this.store.updateSearchOptions({ searchScope });\n    }\n    onSearchRangeChanged(ranges) {\n        this.state.dataRange = ranges[0];\n    }\n    updateDataRange() {\n        if (!this.state.dataRange || this.searchOptions.searchScope !== \"specificRange\") {\n            return;\n        }\n        const specificRange = this.env.model.getters.getRangeFromSheetXC(this.env.model.getters.getActiveSheetId(), this.state.dataRange);\n        this.store.updateSearchOptions({ specificRange });\n    }\n    get pendingSearch() {\n        return this.updateSearchContent.isDebouncePending();\n    }\n}\n\ncss /* scss */ `\n  .o-more-formats-panel {\n    .format-preview {\n      height: 48px;\n      background-color: white;\n      cursor: pointer;\n\n      &:hover {\n        background-color: rgba(0, 0, 0, 0.08);\n      }\n    }\n    .check-icon {\n      width: 24px;\n    }\n  }\n`;\nconst DATE_FORMAT_ACTIONS = createActions([\n    formatNumberFullDateTime,\n    formatNumberFullWeekDayAndMonth,\n    formatNumberDayAndFullMonth,\n    formatNumberShortWeekDay,\n    formatNumberDayAndShortMonth,\n    formatNumberFullMonth,\n    formatNumberShortMonth,\n    formatNumberDate,\n    formatNumberTime,\n    formatNumberDateTime,\n    formatNumberDuration,\n    formatNumberQuarter,\n    formatNumberFullQuarter,\n]);\nclass MoreFormatsPanel extends Component {\n    static template = \"o-spreadsheet-MoreFormatsPanel\";\n    static props = {\n        onCloseSidePanel: Function,\n    };\n    get dateFormatsActions() {\n        return DATE_FORMAT_ACTIONS;\n    }\n}\n\nclass PivotMeasureDisplayPanelStore extends SpreadsheetStore {\n    pivotId;\n    initialMeasure;\n    mutators = [\n        \"cancelMeasureDisplayEdition\",\n        \"updateMeasureDisplayType\",\n        \"updateMeasureDisplayField\",\n        \"updateMeasureDisplayValue\",\n    ];\n    measureDisplay;\n    constructor(get, pivotId, initialMeasure) {\n        super(get);\n        this.pivotId = pivotId;\n        this.initialMeasure = initialMeasure;\n        this.measureDisplay = initialMeasure.display || { type: \"no_calculations\" };\n    }\n    updateMeasureDisplayType(measureDisplayType) {\n        this.updatePivotMeasureDisplay(this.getMeasureDisplay(measureDisplayType, this.measureDisplay.fieldNameWithGranularity, this.measureDisplay.value));\n    }\n    updateMeasureDisplayField(fieldNameWithGranularity) {\n        this.updatePivotMeasureDisplay(this.getMeasureDisplay(this.measureDisplay.type, fieldNameWithGranularity, this.measureDisplay.value));\n    }\n    updateMeasureDisplayValue(value) {\n        this.updatePivotMeasureDisplay(this.getMeasureDisplay(this.measureDisplay.type, this.measureDisplay.fieldNameWithGranularity, value));\n    }\n    updatePivotMeasureDisplay(newDisplay) {\n        const pivotDefinition = deepCopy(this.model.getters.getPivotCoreDefinition(this.pivotId));\n        const measureIndex = this.getMeasureIndex(this.initialMeasure.id, pivotDefinition);\n        const newMeasure = { ...pivotDefinition.measures[measureIndex], display: newDisplay };\n        pivotDefinition.measures[measureIndex] = newMeasure;\n        const result = this.model.dispatch(\"UPDATE_PIVOT\", {\n            pivot: pivotDefinition,\n            pivotId: this.pivotId,\n        });\n        if (result.isSuccessful) {\n            this.measureDisplay = newDisplay;\n        }\n    }\n    getMeasureDisplay(measureDisplayType, fieldNameWithGranularity, value) {\n        switch (measureDisplayType) {\n            case \"no_calculations\":\n            case \"%_of_grand_total\":\n            case \"%_of_col_total\":\n            case \"%_of_row_total\":\n            case \"%_of_parent_row_total\":\n            case \"%_of_parent_col_total\":\n            case \"index\":\n                return { type: measureDisplayType };\n            case \"%_of_parent_total\":\n            case \"running_total\":\n            case \"%_running_total\":\n            case \"rank_asc\":\n            case \"rank_desc\":\n                if (!fieldNameWithGranularity) {\n                    fieldNameWithGranularity = this.fields[0]?.nameWithGranularity;\n                }\n                return { type: measureDisplayType, fieldNameWithGranularity };\n            case \"%_of\":\n            case \"difference_from\":\n            case \"%_difference_from\":\n                if (!fieldNameWithGranularity) {\n                    fieldNameWithGranularity = this.fields[0]?.nameWithGranularity;\n                }\n                const possibleValues = this.getPossibleValues(fieldNameWithGranularity);\n                if (value === undefined || !possibleValues.find((v) => v.value === value)) {\n                    value = PREVIOUS_VALUE;\n                }\n                return {\n                    type: measureDisplayType,\n                    fieldNameWithGranularity,\n                    value: value ?? PREVIOUS_VALUE,\n                };\n        }\n    }\n    getMeasureIndex(measureId, pivotDefinition) {\n        const measureIndex = pivotDefinition.measures.findIndex((m) => m.id === measureId);\n        if (measureIndex === -1) {\n            throw new Error(`Measure with id ${measureId} not found in pivot.`);\n        }\n        return measureIndex;\n    }\n    get doesDisplayNeedsField() {\n        return ([\"%_of_parent_total\", \"running_total\", \"%_running_total\", \"rank_asc\", \"rank_desc\"].includes(this.measureDisplay.type) || this.doesDisplayNeedsValue);\n    }\n    get fields() {\n        const definition = this.getters.getPivot(this.pivotId).definition;\n        return [...definition.columns, ...definition.rows].map((f) => ({\n            ...f,\n            displayName: getFieldDisplayName(f),\n        }));\n    }\n    get doesDisplayNeedsValue() {\n        return this.isDisplayValueDependant(this.measureDisplay);\n    }\n    isDisplayValueDependant(display) {\n        return [\"%_of\", \"difference_from\", \"%_difference_from\"].includes(display.type);\n    }\n    get values() {\n        const display = this.measureDisplay;\n        if (!this.isDisplayValueDependant(display)) {\n            return [];\n        }\n        return this.getPossibleValues(display.fieldNameWithGranularity);\n    }\n    getPossibleValues(fieldNameWithGranularity) {\n        const baseValues = [\n            { value: PREVIOUS_VALUE, label: _t(\"(previous)\") },\n            { value: NEXT_VALUE, label: _t(\"(next)\") },\n        ];\n        const field = this.fields.find((f) => f.nameWithGranularity === fieldNameWithGranularity);\n        if (!field) {\n            return [];\n        }\n        const values = this.getters.getPivot(this.pivotId).getPossibleFieldValues(field);\n        return [...baseValues, ...values];\n    }\n    cancelMeasureDisplayEdition() {\n        const pivotDefinition = deepCopy(this.model.getters.getPivotCoreDefinition(this.pivotId));\n        const measureIndex = this.getMeasureIndex(this.initialMeasure.id, pivotDefinition);\n        pivotDefinition.measures[measureIndex] = {\n            ...pivotDefinition.measures[measureIndex],\n            display: this.initialMeasure.display,\n        };\n        this.model.dispatch(\"UPDATE_PIVOT\", {\n            pivot: pivotDefinition,\n            pivotId: this.pivotId,\n        });\n    }\n}\n\ncss /* scss */ `\n  .o-sidePanel {\n    .o-pivot-measure-display-field,\n    .o-pivot-measure-display-value {\n      box-sizing: border-box;\n      border: solid 1px ${GRAY_300};\n      border-radius: 3px;\n    }\n\n    .o-pivot-measure-display-description {\n      white-space: pre-wrap;\n      color: dimgray;\n      border-left: 2px solid ${GRAY_300};\n    }\n  }\n`;\nclass PivotMeasureDisplayPanel extends Component {\n    static template = \"o-spreadsheet-PivotMeasureDisplayPanel\";\n    static props = {\n        onCloseSidePanel: Function,\n        pivotId: String,\n        measure: Object,\n    };\n    static components = { Section, Checkbox, RadioSelection };\n    measureDisplayTypeLabels = measureDisplayTerms.labels;\n    measureDisplayDescription = measureDisplayTerms.documentation;\n    store;\n    setup() {\n        this.store = useLocalStore(PivotMeasureDisplayPanelStore, this.props.pivotId, this.props.measure);\n    }\n    onSave() {\n        this.env.openSidePanel(\"PivotSidePanel\", { pivotId: this.props.pivotId });\n    }\n    onCancel() {\n        this.store.cancelMeasureDisplayEdition();\n        this.env.openSidePanel(\"PivotSidePanel\", { pivotId: this.props.pivotId });\n    }\n    get fieldChoices() {\n        return this.store.fields.map((field) => ({\n            value: field.nameWithGranularity,\n            label: field.displayName,\n        }));\n    }\n}\n\ncss /* scss */ `\n  .pivot-defer-update {\n    min-height: 35px;\n  }\n`;\nclass PivotDeferUpdate extends Component {\n    static template = \"o-spreadsheet-PivotDeferUpdate\";\n    static props = {\n        deferUpdate: Boolean,\n        isDirty: Boolean,\n        toggleDeferUpdate: Function,\n        discard: Function,\n        apply: Function,\n    };\n    static components = {\n        Section,\n        Checkbox,\n    };\n    get deferUpdatesLabel() {\n        return _t(\"Defer updates\");\n    }\n    get deferUpdatesTooltip() {\n        return _t(\"Changing the pivot definition requires to reload the data. It may take some time.\");\n    }\n}\n\nfunction useAutofocus({ refName }) {\n    const ref = useRef(refName);\n    useEffect((el) => {\n        el?.focus();\n    }, () => [ref.el]);\n}\n\ncss /* scss */ `\n  input.pivot-dimension-search-field:focus {\n    outline: none;\n  }\n  .pivot-dimension-search-field-icon svg {\n    width: 13px;\n    height: 13px;\n  }\n  .pivot-dimension-search {\n    background-color: white;\n  }\n  .add-dimension.o-button {\n    padding: 2px 7px;\n    font-weight: 400;\n    font-size: 12px;\n    flex-grow: 0;\n    height: inherit;\n  }\n`;\nclass AddDimensionButton extends Component {\n    static template = \"o-spreadsheet-AddDimensionButton\";\n    static components = { Popover, TextValueProvider };\n    static props = {\n        onFieldPicked: Function,\n        fields: Array,\n        slots: { type: Object, optional: true },\n    };\n    buttonRef = useRef(\"button\");\n    popover = useState({ isOpen: false });\n    search = useState({ input: \"\" });\n    autoComplete;\n    // TODO navigation keys. (this looks a lot like auto-complete list. Could maybe be factorized)\n    setup() {\n        this.autoComplete = useLocalStore(AutoCompleteStore);\n        this.autoComplete.useProvider(this.getProvider());\n        useExternalListener(window, \"click\", (ev) => {\n            if (ev.target !== this.buttonRef.el) {\n                this.popover.isOpen = false;\n            }\n        });\n        useAutofocus({ refName: \"autofocus\" });\n    }\n    getProvider() {\n        return {\n            proposals: this.proposals,\n            autoSelectFirstProposal: false,\n            selectProposal: (value) => {\n                const field = this.props.fields.find((field) => field.string === value);\n                if (field) {\n                    this.pickField(field);\n                }\n            },\n        };\n    }\n    get proposals() {\n        let fields;\n        if (this.search.input) {\n            fields = fuzzyLookup(this.search.input, this.props.fields, (field) => field.string);\n        }\n        else {\n            fields = this.props.fields;\n        }\n        return fields.map((field) => {\n            const text = field.string;\n            return {\n                text,\n                fuzzySearchKey: text,\n                htmlContent: getHtmlContentFromPattern(this.search.input, text, COMPOSER_ASSISTANT_COLOR, \"o-semi-bold\"),\n            };\n        });\n    }\n    get popoverProps() {\n        const { x, y, width, height } = this.buttonRef.el.getBoundingClientRect();\n        return {\n            anchorRect: { x, y, width, height },\n            positioning: \"BottomLeft\",\n        };\n    }\n    updateSearch(searchInput) {\n        this.search.input = searchInput;\n        this.autoComplete.useProvider(this.getProvider());\n    }\n    pickField(field) {\n        this.props.onFieldPicked(field.name);\n        this.togglePopover();\n    }\n    togglePopover() {\n        this.popover.isOpen = !this.popover.isOpen;\n        this.search.input = \"\";\n        this.autoComplete.useProvider(this.getProvider());\n    }\n    onKeyDown(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                const proposals = this.autoComplete.provider?.proposals;\n                if (proposals?.length === 1) {\n                    this.autoComplete.provider?.selectProposal(proposals[0].text || \"\");\n                }\n                const proposal = this.autoComplete.selectedProposal;\n                this.autoComplete.provider?.selectProposal(proposal?.text || \"\");\n                break;\n            case \"ArrowUp\":\n            case \"ArrowDown\":\n                this.autoComplete.moveSelection(ev.key === \"ArrowDown\" ? \"next\" : \"previous\");\n                break;\n            case \"Escape\":\n                this.popover.isOpen = false;\n                break;\n        }\n    }\n}\n\ncss /* scss */ `\n  .o-spreadsheet {\n    .os-input {\n      box-sizing: border-box;\n      border-width: 0 0 1px 0;\n      border-color: transparent;\n      outline: none;\n      text-overflow: ellipsis;\n      color: ${TEXT_BODY};\n    }\n    .os-input:hover,\n    .os-input:focus {\n      border-color: ${GRAY_300};\n    }\n  }\n`;\nclass TextInput extends Component {\n    static template = \"o-spreadsheet-TextInput\";\n    static props = {\n        value: String,\n        onChange: Function,\n        class: {\n            type: String,\n            optional: true,\n        },\n        id: {\n            type: String,\n            optional: true,\n        },\n        placeholder: {\n            type: String,\n            optional: true,\n        },\n    };\n    inputRef = useRef(\"input\");\n    setup() {\n        useExternalListener(window, \"click\", (ev) => {\n            if (ev.target !== this.inputRef.el && this.inputRef.el?.value !== this.props.value) {\n                this.save();\n            }\n        }, { capture: true });\n    }\n    onKeyDown(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                this.save();\n                ev.preventDefault();\n                ev.stopPropagation();\n                break;\n            case \"Escape\":\n                if (this.inputRef.el) {\n                    this.inputRef.el.value = this.props.value;\n                    this.inputRef.el.blur();\n                }\n                ev.preventDefault();\n                ev.stopPropagation();\n                break;\n        }\n    }\n    save() {\n        const currentValue = (this.inputRef.el?.value || \"\").trim();\n        if (currentValue !== this.props.value) {\n            this.props.onChange(currentValue);\n        }\n        this.inputRef.el?.blur();\n    }\n    focusInputAndSelectContent() {\n        const inputEl = this.inputRef.el;\n        if (!inputEl)\n            return;\n        // The onFocus event selects all text in the input.\n        // The subsequent mouseup event can deselect this text,\n        // so t-on-mouseup.prevent.stop is used to prevent this\n        // default behavior and preserve the selection.\n        inputEl.focus();\n        inputEl.select();\n    }\n}\n\nclass CogWheelMenu extends Component {\n    static template = \"o-spreadsheet-CogWheelMenu\";\n    static components = { Menu };\n    static props = {\n        items: Array,\n    };\n    buttonRef = useRef(\"button\");\n    menuState = useState({ isOpen: false, position: null, menuItems: [] });\n    menuId = this.env.model.uuidGenerator.uuidv4();\n    toggleMenu(ev) {\n        if (ev.closedMenuId === this.menuId) {\n            return;\n        }\n        const { x, y } = this.buttonRef.el.getBoundingClientRect();\n        this.menuState.isOpen = !this.menuState.isOpen;\n        this.menuState.position = { x, y };\n        this.menuState.menuItems = createActions(this.props.items);\n    }\n}\n\n// don't use bg-white since it's flipped to dark in dark mode and we don't support dark mode\ncss /* scss */ `\n  .pivot-dimension {\n    background-color: white;\n    border: 1px solid ${GRAY_300};\n    border-radius: 4px;\n\n    select.o-input {\n      height: inherit;\n    }\n\n    select > option {\n      background-color: white;\n    }\n\n    .pivot-dim-operator-label {\n      min-width: 120px;\n    }\n\n    &.pivot-dimension-invalid {\n      background-color: #ffdddd;\n      border-color: red !important;\n      select {\n        background-color: #ffdddd;\n      }\n    }\n  }\n`;\nclass PivotDimension extends Component {\n    static template = \"o-spreadsheet-PivotDimension\";\n    static props = {\n        dimension: Object,\n        onRemoved: { type: Function, optional: true },\n        onNameUpdated: { type: Function, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    static components = { CogWheelMenu, TextInput };\n    updateName(name) {\n        this.props.onNameUpdated?.(this.props.dimension, name === \"\" || name.startsWith(\"=\") ? undefined : name);\n    }\n}\n\nclass PivotDimensionGranularity extends Component {\n    static template = \"o-spreadsheet-PivotDimensionGranularity\";\n    static props = {\n        dimension: Object,\n        onUpdated: Function,\n        availableGranularities: Set,\n        allGranularities: Array,\n    };\n    periods = ALL_PERIODS;\n}\n\nclass PivotDimensionOrder extends Component {\n    static template = \"o-spreadsheet-PivotDimensionOrder\";\n    static props = {\n        dimension: Object,\n        onUpdated: Function,\n    };\n}\n\nfunction createMeasureAutoComplete(pivot, forComputedMeasure) {\n    return {\n        sequence: 0,\n        autoSelectFirstProposal: true,\n        getProposals(tokenAtCursor) {\n            const measureProposals = pivot.measures\n                .filter((m) => m !== forComputedMeasure)\n                .map((measure) => {\n                const text = getCanonicalSymbolName(measure.id);\n                return {\n                    text: text,\n                    description: measure.displayName,\n                    htmlContent: [{ value: text, color: tokenColors.FUNCTION }],\n                    fuzzySearchKey: measure.displayName + text + measure.fieldName,\n                };\n            });\n            const dimensionsProposals = pivot.rows.concat(pivot.columns).map((dimension) => {\n                const text = getCanonicalSymbolName(dimension.nameWithGranularity);\n                return {\n                    text: text,\n                    description: dimension.displayName,\n                    htmlContent: [{ value: text, color: tokenColors.FUNCTION }],\n                    fuzzySearchKey: dimension.displayName + text + dimension.fieldName,\n                };\n            });\n            return measureProposals.concat(dimensionsProposals);\n        },\n        selectProposal(tokenAtCursor, value) {\n            let start = tokenAtCursor.end;\n            if (tokenAtCursor.type === \"SYMBOL\") {\n                start = tokenAtCursor.start;\n            }\n            const end = tokenAtCursor.end;\n            this.composer.changeComposerCursorSelection(start, end);\n            this.composer.replaceComposerCursorSelection(value);\n        },\n    };\n}\n\nclass PivotMeasureEditor extends Component {\n    static template = \"o-spreadsheet-PivotMeasureEditor\";\n    static components = {\n        PivotDimension,\n        StandaloneComposer,\n    };\n    static props = {\n        definition: Object,\n        measure: Object,\n        onMeasureUpdated: Function,\n        onRemoved: Function,\n        generateMeasureId: Function,\n        aggregators: Object,\n        pivotId: String,\n    };\n    getMeasureAutocomplete() {\n        return createMeasureAutoComplete(this.props.definition, this.props.measure);\n    }\n    updateMeasureFormula(formula) {\n        this.props.onMeasureUpdated({\n            ...this.props.measure,\n            computedBy: {\n                sheetId: this.env.model.getters.getActiveSheetId(),\n                formula: formula[0] === \"=\" ? formula : \"=\" + formula,\n            },\n        });\n    }\n    updateAggregator(aggregator) {\n        this.props.onMeasureUpdated({\n            ...this.props.measure,\n            aggregator,\n            id: this.props.generateMeasureId(this.props.measure.fieldName, aggregator),\n        });\n    }\n    updateName(measure, userDefinedName) {\n        if (this.props.measure.computedBy && userDefinedName) {\n            this.props.onMeasureUpdated({\n                ...this.props.measure,\n                userDefinedName,\n                id: this.props.generateMeasureId(userDefinedName, this.props.measure.aggregator),\n                fieldName: userDefinedName,\n            });\n        }\n        else {\n            this.props.onMeasureUpdated({\n                ...this.props.measure,\n                userDefinedName,\n            });\n        }\n    }\n    toggleMeasureVisibility() {\n        this.props.onMeasureUpdated({\n            ...this.props.measure,\n            isHidden: !this.props.measure.isHidden,\n        });\n    }\n    openShowValuesAs() {\n        this.env.openSidePanel(\"PivotMeasureDisplayPanel\", {\n            pivotId: this.props.pivotId,\n            measure: this.props.measure,\n        });\n    }\n}\n\ncss /* scss */ `\n  .add-calculated-measure {\n    cursor: pointer;\n  }\n`;\nclass PivotLayoutConfigurator extends Component {\n    static template = \"o-spreadsheet-PivotLayoutConfigurator\";\n    static components = {\n        AddDimensionButton,\n        PivotDimension,\n        PivotDimensionOrder,\n        PivotDimensionGranularity,\n        PivotMeasureEditor,\n    };\n    static props = {\n        definition: Object,\n        onDimensionsUpdated: Function,\n        unusedGroupableFields: Array,\n        measureFields: Array,\n        unusedGranularities: Object,\n        dateGranularities: Array,\n        datetimeGranularities: Array,\n        pivotId: String,\n    };\n    dimensionsRef = useRef(\"pivot-dimensions\");\n    dragAndDrop = useDragAndDropListItems();\n    AGGREGATORS = AGGREGATORS;\n    composerFocus;\n    isDateOrDatetimeField = isDateOrDatetimeField;\n    setup() {\n        this.composerFocus = useStore(ComposerFocusStore);\n    }\n    startDragAndDrop(dimension, event) {\n        if (event.button !== 0 || event.target.tagName === \"SELECT\") {\n            return;\n        }\n        const rects = this.getDimensionElementsRects();\n        const definition = this.props.definition;\n        const { columns, rows } = definition;\n        const draggableIds = [\n            ...columns.map((col) => col.nameWithGranularity),\n            \"__rows_title__\",\n            ...rows.map((row) => row.nameWithGranularity),\n        ];\n        const allDimensions = columns.concat(rows);\n        const offset = 1; // column title\n        const draggableItems = draggableIds.map((id, index) => ({\n            id,\n            size: rects[index + offset].height,\n            position: rects[index + offset].y,\n        }));\n        this.dragAndDrop.start(\"vertical\", {\n            draggedItemId: dimension.nameWithGranularity,\n            initialMousePosition: event.clientY,\n            items: draggableItems,\n            containerEl: this.dimensionsRef.el,\n            onDragEnd: (dimensionName, finalIndex) => {\n                const originalIndex = draggableIds.findIndex((id) => id === dimensionName);\n                if (originalIndex === finalIndex) {\n                    return;\n                }\n                const draggedItems = [...draggableIds];\n                draggedItems.splice(originalIndex, 1);\n                draggedItems.splice(finalIndex, 0, dimensionName);\n                const columns = draggedItems.slice(0, draggedItems.indexOf(\"__rows_title__\"));\n                const rows = draggedItems.slice(draggedItems.indexOf(\"__rows_title__\") + 1);\n                this.props.onDimensionsUpdated({\n                    columns: columns\n                        .map((nameWithGranularity) => allDimensions.find((dimension) => dimension.nameWithGranularity === nameWithGranularity))\n                        .filter(isDefined),\n                    rows: rows\n                        .map((nameWithGranularity) => allDimensions.find((dimension) => dimension.nameWithGranularity === nameWithGranularity))\n                        .filter(isDefined),\n                });\n            },\n        });\n    }\n    getGranularitiesFor(field) {\n        if (!isDateOrDatetimeField(field)) {\n            return [];\n        }\n        return field.type === \"date\" ? this.props.dateGranularities : this.props.datetimeGranularities;\n    }\n    startDragAndDropMeasures(measure, event) {\n        if (event.button !== 0 ||\n            event.target.tagName === \"SELECT\" ||\n            event.target.tagName === \"INPUT\" ||\n            this.composerFocus.focusMode !== \"inactive\") {\n            return;\n        }\n        const rects = this.getDimensionElementsRects();\n        const definition = this.props.definition;\n        const { measures, columns, rows } = definition;\n        const draggableIds = measures.map((m) => m.id);\n        const offset = 3 + columns.length + rows.length; // column title, row title, measure title\n        const draggableItems = draggableIds.map((id, index) => ({\n            id,\n            size: rects[index + offset].height,\n            position: rects[index + offset].y,\n        }));\n        this.dragAndDrop.start(\"vertical\", {\n            draggedItemId: measure.id,\n            initialMousePosition: event.clientY,\n            items: draggableItems,\n            containerEl: this.dimensionsRef.el,\n            onDragEnd: (measureName, finalIndex) => {\n                const originalIndex = draggableIds.findIndex((id) => id === measureName);\n                if (originalIndex === finalIndex) {\n                    return;\n                }\n                const draggedItems = [...draggableIds];\n                draggedItems.splice(originalIndex, 1);\n                draggedItems.splice(finalIndex, 0, measureName);\n                this.props.onDimensionsUpdated({\n                    measures: draggedItems\n                        .map((measureId) => measures.find((measure) => measure.id === measureId))\n                        .filter(isDefined),\n                });\n            },\n        });\n    }\n    getDimensionElementsRects() {\n        return Array.from(this.dimensionsRef.el.children).map((el) => {\n            const style = getComputedStyle(el);\n            const rect = el.getBoundingClientRect();\n            return {\n                x: rect.x,\n                y: rect.y,\n                width: rect.width + parseInt(style.marginLeft || \"0\") + parseInt(style.marginRight || \"0\"),\n                height: rect.height + parseInt(style.marginTop || \"0\") + parseInt(style.marginBottom || \"0\"),\n            };\n        });\n    }\n    removeDimension(dimension) {\n        const { columns, rows } = this.props.definition;\n        this.props.onDimensionsUpdated({\n            columns: columns.filter((col) => col.nameWithGranularity !== dimension.nameWithGranularity),\n            rows: rows.filter((row) => row.nameWithGranularity !== dimension.nameWithGranularity),\n        });\n    }\n    removeMeasureDimension(measure) {\n        const { measures } = this.props.definition;\n        this.props.onDimensionsUpdated({\n            measures: measures.filter((m) => m.id !== measure.id),\n        });\n    }\n    addColumnDimension(fieldName) {\n        const { columns } = this.props.definition;\n        this.props.onDimensionsUpdated({\n            columns: columns.concat([{ fieldName: fieldName, order: \"asc\" }]),\n        });\n    }\n    addRowDimension(fieldName) {\n        const { rows } = this.props.definition;\n        this.props.onDimensionsUpdated({\n            rows: rows.concat([{ fieldName: fieldName, order: \"asc\" }]),\n        });\n    }\n    addMeasureDimension(fieldName) {\n        const { measures } = this.props.definition;\n        const aggregator = this.getDefaultMeasureAggregator(fieldName);\n        this.props.onDimensionsUpdated({\n            measures: measures.concat([\n                { id: this.getMeasureId(fieldName, aggregator), fieldName, aggregator },\n            ]),\n        });\n    }\n    updateMeasure(measure, newMeasure) {\n        const { measures } = this.props.definition;\n        this.props.onDimensionsUpdated({\n            measures: measures.map((m) => (m.id === measure.id ? newMeasure : m)),\n        });\n    }\n    getMeasureId(fieldName, aggregator) {\n        const baseId = fieldName + (aggregator ? `:${aggregator}` : \"\");\n        let id = baseId;\n        let i = 2;\n        while (this.props.definition.measures.some((m) => m.id === id)) {\n            id = `${baseId}:${i}`;\n            i++;\n        }\n        return id;\n    }\n    getDefaultMeasureAggregator(fieldName) {\n        const field = this.props.measureFields.find((f) => f.name === fieldName);\n        return field?.aggregator ? field.aggregator : \"count\";\n    }\n    addCalculatedMeasure() {\n        const { measures } = this.props.definition;\n        const measureName = this.env.model.getters.generateNewCalculatedMeasureName(measures);\n        this.props.onDimensionsUpdated({\n            measures: measures.concat([\n                {\n                    id: this.getMeasureId(measureName),\n                    fieldName: measureName,\n                    aggregator: \"sum\",\n                    computedBy: {\n                        sheetId: this.env.model.getters.getActiveSheetId(),\n                        formula: \"=0\",\n                    },\n                },\n            ]),\n        });\n    }\n    updateOrder(updateDimension, order) {\n        const { rows, columns } = this.props.definition;\n        this.props.onDimensionsUpdated({\n            rows: rows.map((row) => {\n                if (row.nameWithGranularity === updateDimension.nameWithGranularity) {\n                    return { ...row, order: order || undefined };\n                }\n                return row;\n            }),\n            columns: columns.map((col) => {\n                if (col.nameWithGranularity === updateDimension.nameWithGranularity) {\n                    return { ...col, order: order || undefined };\n                }\n                return col;\n            }),\n        });\n    }\n    updateGranularity(dimension, granularity) {\n        const { rows, columns } = this.props.definition;\n        this.props.onDimensionsUpdated({\n            rows: rows.map((row) => {\n                if (row.nameWithGranularity === dimension.nameWithGranularity) {\n                    return { ...row, granularity };\n                }\n                return row;\n            }),\n            columns: columns.map((col) => {\n                if (col.nameWithGranularity === dimension.nameWithGranularity) {\n                    return { ...col, granularity };\n                }\n                return col;\n            }),\n        });\n    }\n    getMeasureDescription(measure) {\n        const measureDisplay = measure.display;\n        if (!measureDisplay || measureDisplay.type === \"no_calculations\") {\n            return \"\";\n        }\n        const pivot = this.env.model.getters.getPivot(this.props.pivotId);\n        const field = [...pivot.definition.columns, ...pivot.definition.rows].find((f) => f.nameWithGranularity === measureDisplay.fieldNameWithGranularity);\n        const fieldName = field ? getFieldDisplayName(field) : \"\";\n        return measureDisplayTerms.descriptions[measureDisplay.type](fieldName);\n    }\n}\n\nclass PivotTitleSection extends Component {\n    static template = \"o-spreadsheet-PivotTitleSection\";\n    static components = { CogWheelMenu, Section, TextInput };\n    static props = {\n        pivotId: String,\n        flipAxis: Function,\n    };\n    get cogWheelMenuItems() {\n        return [\n            {\n                name: _t(\"Flip axes\"),\n                icon: \"o-spreadsheet-Icon.EXCHANGE\",\n                execute: this.props.flipAxis,\n            },\n            {\n                name: _t(\"Duplicate\"),\n                icon: \"o-spreadsheet-Icon.COPY\",\n                execute: () => this.duplicatePivot(),\n            },\n            {\n                name: _t(\"Delete\"),\n                icon: \"o-spreadsheet-Icon.TRASH\",\n                execute: () => this.delete(),\n            },\n        ];\n    }\n    get name() {\n        return this.env.model.getters.getPivotName(this.props.pivotId);\n    }\n    get displayName() {\n        return this.env.model.getters.getPivotDisplayName(this.props.pivotId);\n    }\n    duplicatePivot() {\n        const newPivotId = this.env.model.uuidGenerator.uuidv4();\n        const newSheetId = this.env.model.uuidGenerator.uuidv4();\n        const result = this.env.model.dispatch(\"DUPLICATE_PIVOT_IN_NEW_SHEET\", {\n            pivotId: this.props.pivotId,\n            newPivotId,\n            newSheetId,\n        });\n        const text = result.isSuccessful ? _t(\"Pivot duplicated.\") : _t(\"Pivot duplication failed\");\n        const type = result.isSuccessful ? \"success\" : \"danger\";\n        this.env.notifyUser({\n            text,\n            sticky: false,\n            type,\n        });\n        if (result.isSuccessful) {\n            this.env.openSidePanel(\"PivotSidePanel\", { pivotId: newPivotId });\n        }\n    }\n    delete() {\n        this.env.askConfirmation(_t(\"Are you sure you want to delete this pivot?\"), () => {\n            this.env.model.dispatch(\"REMOVE_PIVOT\", { pivotId: this.props.pivotId });\n        });\n    }\n    onNameChanged(name) {\n        const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);\n        this.env.model.dispatch(\"UPDATE_PIVOT\", {\n            pivotId: this.props.pivotId,\n            pivot: {\n                ...pivot,\n                name,\n            },\n        });\n    }\n}\n\n/**\n * Represent a pivot runtime definition. A pivot runtime definition is a pivot\n * definition that has been enriched to include the display name of its attributes\n * (measures, columns, rows).\n */\nclass PivotRuntimeDefinition {\n    measures;\n    columns;\n    rows;\n    constructor(definition, fields) {\n        this.measures = definition.measures.map((measure) => createMeasure(fields, measure));\n        this.columns = definition.columns.map((dimension) => createPivotDimension(fields, dimension));\n        this.rows = definition.rows.map((dimension) => createPivotDimension(fields, dimension));\n    }\n    getDimension(nameWithGranularity) {\n        const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||\n            this.rows.find((d) => d.nameWithGranularity === nameWithGranularity);\n        if (!dimension) {\n            throw new EvaluationError(_t(\"Dimension %s does not exist\", nameWithGranularity));\n        }\n        return dimension;\n    }\n    getMeasure(id) {\n        const measure = this.measures.find((measure) => measure.id === id);\n        if (!measure) {\n            throw new EvaluationError(_t(\"Field %s is not a measure\", id));\n        }\n        return measure;\n    }\n}\nfunction createMeasure(fields, measure) {\n    const fieldName = measure.fieldName;\n    const field = fieldName === \"__count\"\n        ? { name: \"__count\", string: _t(\"Count\"), type: \"integer\", aggregator: \"sum\" }\n        : fields[fieldName];\n    const aggregator = measure.aggregator;\n    return {\n        /**\n         * Get the id of the measure, as it is stored in the pivot formula\n         */\n        id: measure.id,\n        /**\n         * Display name of the measure\n         * e.g. \"__count\" -> \"Count\", \"amount_total\" -> \"Total Amount\"\n         */\n        get displayName() {\n            return measure.userDefinedName ?? field?.string ?? measure.fieldName;\n        },\n        userDefinedName: measure.userDefinedName,\n        /**\n         * Get the name of the field of the measure\n         */\n        fieldName,\n        /**\n         * Get the aggregator of the measure\n         */\n        aggregator,\n        /**\n         * Get the type of the measure field\n         * e.g. \"stage_id\" -> \"many2one\", \"create_date:month\" -> \"date\"\n         */\n        type: fieldName === \"__count\" ? \"integer\" : field?.type ?? \"integer\",\n        isValid: !!(field || measure.computedBy),\n        isHidden: measure.isHidden,\n        format: measure.format,\n        computedBy: measure.computedBy,\n        display: measure.display,\n    };\n}\nfunction createPivotDimension(fields, dimension) {\n    const field = fields[dimension.fieldName];\n    const type = field?.type ?? \"integer\";\n    const granularity = field && isDateOrDatetimeField(field) ? dimension.granularity : undefined;\n    return {\n        /**\n         * Get the display name of the dimension\n         * e.g. \"stage_id\" -> \"Stage\", \"create_date:month\" -> \"Create Date\"\n         */\n        displayName: field?.string ?? dimension.fieldName,\n        /**\n         * Get the name of the dimension, as it is stored in the pivot formula\n         * e.g. \"stage_id\", \"create_date:month\"\n         */\n        nameWithGranularity: dimension.fieldName + (granularity ? `:${granularity}` : \"\"),\n        /**\n         * Get the name of the field of the dimension\n         * e.g. \"stage_id\" -> \"stage_id\", \"create_date:month\" -> \"create_date\"\n         */\n        fieldName: dimension.fieldName,\n        /**\n         * Get the aggregate operator of the dimension\n         * e.g. \"stage_id\" -> undefined, \"create_date:month\" -> \"month\"\n         */\n        granularity,\n        /**\n         * Get the type of the field of the dimension\n         * e.g. \"stage_id\" -> \"many2one\", \"create_date:month\" -> \"date\"\n         */\n        type,\n        order: dimension.order,\n        isValid: !!field,\n    };\n}\n\nclass SpreadsheetPivotRuntimeDefinition extends PivotRuntimeDefinition {\n    range;\n    constructor(definition, fields, getters) {\n        super(definition, fields);\n        if (definition.dataSet) {\n            const { sheetId, zone } = definition.dataSet;\n            this.range = getters.getRangeFromZone(sheetId, zone);\n        }\n    }\n}\n\n/**\n * Class used to ease the construction of a pivot table.\n * Let's consider the following example, with:\n * - columns groupBy: [sales_team, create_date]\n * - rows groupBy: [continent, city]\n * - measures: [revenues]\n * _____________________________________________________________________________________|   ----|\n * |                |   Sale Team 1             |  Sale Team 2            |             |       |\n * |                |___________________________|_________________________|_____________|       |\n * |                |   May 2020   | June 2020  |  May 2020  | June 2020  |   Total     |       |<---- `cols`\n * |                |______________|____________|____________|____________|_____________|       |   ----|\n * |                |   Revenues   |  Revenues  |  Revenues  |  Revenues  |   Revenues  |       |       |<--- `measureRow`\n * |________________|______________|____________|____________|____________|_____________|   ----|   ----|\n * |Europe          |     25       |     35     |     40     |     30     |     65      |   ----|\n * |    Brussels    |      0       |     15     |     30     |     30     |     30      |       |\n * |    Paris       |     25       |     20     |     10     |     0      |     35      |       |\n * |North America   |     60       |     75     |            |            |     60      |       |<---- `body`\n * |    Washington  |     60       |     75     |            |            |     60      |       |\n * |Total           |     85       |     110    |     40     |     30     |     125     |       |\n * |________________|______________|____________|____________|____________|_____________|   ----|\n *\n * |                |\n * |----------------|\n *         |\n *         |\n *       `rows`\n *\n * `rows` is an array of cells, each cells contains the indent level, the fields used for the group by and the values for theses fields.\n * For example:\n *   `Europe`: { indent: 1, fields: [\"continent\"], values: [\"id_of_Europe\"]}\n *   `Brussels`: { indent: 2, fields: [\"continent\", \"city\"], values: [\"id_of_Europe\", \"id_of_Brussels\"]}\n *   `Total`: { indent: 0, fields: [], values: []}\n *\n * `columns` is an double array, first by row and then by cell. So, in this example, it looks like:\n *   [[row1], [row2], [measureRow]]\n *   Each cell of a column's row contains the width (span) of the cells, the fields used for the group by and the values for theses fields.\n * For example:\n *   `Sale Team 1`: { width: 2, fields: [\"sales_team\"], values: [\"id_of_SaleTeam1\"]}\n *   `May 2020` (the one under Sale Team 2): { width: 1, fields: [\"sales_team\", \"create_date\"], values: [\"id_of_SaleTeam2\", \"May 2020\"]}\n *   `Revenues` (the one under Total): { width: 1, fields: [\"measure\"], values: [\"revenues\"]}\n *\n */\nclass SpreadsheetPivotTable {\n    columns;\n    rows;\n    measures;\n    fieldsType;\n    maxIndent;\n    pivotCells = {};\n    rowTree;\n    colTree;\n    constructor(columns, rows, measures, fieldsType) {\n        this.columns = columns.map((row) => {\n            // offset in the pivot table\n            // starts at 1 because the first column is the row title\n            let offset = 1;\n            return row.map((col) => {\n                col = { ...col, offset };\n                offset += col.width;\n                return col;\n            });\n        });\n        this.rows = rows;\n        this.measures = measures;\n        this.fieldsType = fieldsType;\n        this.maxIndent = Math.max(...this.rows.map((row) => row.indent));\n        this.rowTree = lazy(() => this.buildRowsTree());\n        this.colTree = lazy(() => this.buildColumnsTree());\n    }\n    /**\n     * Get the number of columns leafs (i.e. the number of the last row of columns)\n     */\n    getNumberOfDataColumns() {\n        return this.columns.at(-1)?.length || 0;\n    }\n    getPivotCells(includeTotal = true, includeColumnHeaders = true) {\n        const key = JSON.stringify({ includeTotal, includeColumnHeaders });\n        if (!this.pivotCells[key]) {\n            const numberOfDataRows = this.rows.length;\n            const numberOfDataColumns = this.getNumberOfDataColumns();\n            let pivotHeight = this.columns.length + numberOfDataRows;\n            let pivotWidth = 1 /*(row headers)*/ + numberOfDataColumns;\n            if (!includeTotal && numberOfDataRows !== 1) {\n                pivotHeight -= 1;\n            }\n            if (!includeTotal && numberOfDataColumns !== this.measures.length) {\n                pivotWidth -= this.measures.length;\n            }\n            const domainArray = [];\n            const startRow = includeColumnHeaders ? 0 : this.columns.length;\n            for (let col = 0; col < pivotWidth; col++) {\n                domainArray.push([]);\n                for (let row = startRow; row < pivotHeight; row++) {\n                    if (!includeTotal && row === pivotHeight) {\n                        continue;\n                    }\n                    domainArray[col].push(this.getPivotCell(col, row, includeTotal));\n                }\n            }\n            this.pivotCells[key] = domainArray;\n        }\n        return this.pivotCells[key];\n    }\n    getRowTree() {\n        return this.rowTree();\n    }\n    getColTree() {\n        return this.colTree();\n    }\n    isTotalRow(index) {\n        return this.rows[index].indent !== this.maxIndent;\n    }\n    getPivotCell(col, row, includeTotal = true) {\n        const colHeadersHeight = this.columns.length;\n        if (col > 0 && row === colHeadersHeight - 1) {\n            const domain = this.getColHeaderDomain(col, row);\n            if (!domain) {\n                return EMPTY_PIVOT_CELL;\n            }\n            const measure = domain.at(-1)?.value?.toString() || \"\";\n            return { type: \"MEASURE_HEADER\", domain: domain.slice(0, -1), measure };\n        }\n        else if (row <= colHeadersHeight - 1) {\n            const domain = this.getColHeaderDomain(col, row);\n            return domain ? { type: \"HEADER\", domain } : EMPTY_PIVOT_CELL;\n        }\n        else if (col === 0) {\n            const rowIndex = row - colHeadersHeight;\n            const domain = this.getRowDomain(rowIndex);\n            return { type: \"HEADER\", domain };\n        }\n        else {\n            const rowIndex = row - colHeadersHeight;\n            if (!includeTotal && this.isTotalRow(rowIndex)) {\n                return EMPTY_PIVOT_CELL;\n            }\n            const domain = [...this.getRowDomain(rowIndex), ...this.getColDomain(col)];\n            const measure = this.getColMeasure(col);\n            return { type: \"VALUE\", domain, measure };\n        }\n    }\n    getColHeaderDomain(col, row) {\n        if (col === 0) {\n            return undefined;\n        }\n        const domain = [];\n        const pivotCol = this.columns[row].find((pivotCol) => pivotCol.offset === col);\n        if (!pivotCol) {\n            return undefined;\n        }\n        for (let i = 0; i < pivotCol.fields.length; i++) {\n            const fieldWithGranularity = pivotCol.fields[i];\n            if (fieldWithGranularity === \"measure\") {\n                domain.push({\n                    type: \"char\",\n                    field: fieldWithGranularity,\n                    value: toNormalizedPivotValue({ displayName: \"measure\", type: \"char\" }, pivotCol.values[i]),\n                });\n            }\n            else {\n                const { fieldName, granularity } = parseDimension(fieldWithGranularity);\n                const type = this.fieldsType[fieldName] || \"char\";\n                domain.push({\n                    type,\n                    field: fieldWithGranularity,\n                    value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, pivotCol.values[i]),\n                });\n            }\n        }\n        return domain;\n    }\n    getColDomain(col) {\n        const domain = this.getColHeaderDomain(col, this.columns.length - 1);\n        return domain ? domain.slice(0, -1) : []; // slice: remove measure and value\n    }\n    getColMeasure(col) {\n        const domain = this.getColHeaderDomain(col, this.columns.length - 1);\n        const measure = domain?.at(-1)?.value;\n        if (measure === undefined || measure === null) {\n            throw new Error(\"Measure is missing\");\n        }\n        return measure.toString();\n    }\n    getRowDomain(row) {\n        const domain = [];\n        for (let i = 0; i < this.rows[row].fields.length; i++) {\n            const fieldWithGranularity = this.rows[row].fields[i];\n            const { fieldName, granularity } = parseDimension(fieldWithGranularity);\n            const type = this.fieldsType[fieldName] || \"char\";\n            domain.push({\n                type,\n                field: fieldWithGranularity,\n                value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, this.rows[row].values[i]),\n            });\n        }\n        return domain;\n    }\n    buildRowsTree() {\n        const tree = [];\n        let depth = 0;\n        const treesAtDepth = {};\n        treesAtDepth[0] = tree;\n        for (const row of this.rows) {\n            if (row.fields.length === 0 || row.values.length === 0) {\n                return tree;\n            }\n            const rowDepth = row.fields.length - 1;\n            const fieldWithGranularity = row.fields[rowDepth];\n            const { fieldName, granularity } = parseDimension(fieldWithGranularity);\n            const type = this.fieldsType[fieldName] ?? \"char\";\n            const value = toNormalizedPivotValue({ displayName: fieldName, type, granularity }, row.values[rowDepth]);\n            if (rowDepth > depth) {\n                depth = rowDepth;\n                treesAtDepth[depth] = [];\n                const parentNode = treesAtDepth[depth - 1].at(-1);\n                if (parentNode) {\n                    parentNode.children = treesAtDepth[depth];\n                }\n            }\n            depth = rowDepth;\n            const node = {\n                value,\n                field: row.fields[rowDepth],\n                children: [],\n                width: 0, // not used\n            };\n            treesAtDepth[depth].push(node);\n        }\n        return tree;\n    }\n    buildColumnsTree() {\n        const tree = [];\n        const columns = this.columns.at(-2) || [];\n        const treesAtDepth = {};\n        treesAtDepth[0] = tree;\n        for (const leaf of columns) {\n            for (let depth = 0; depth < leaf.fields.length; depth++) {\n                const fieldWithGranularity = leaf.fields[depth];\n                const { fieldName, granularity } = parseDimension(fieldWithGranularity);\n                const type = this.fieldsType[fieldName] ?? \"char\";\n                const value = toNormalizedPivotValue({ displayName: fieldName, type, granularity }, leaf.values[depth]);\n                const node = {\n                    value,\n                    field: leaf.fields[depth],\n                    children: [],\n                    width: leaf.width,\n                };\n                if (treesAtDepth[depth]?.at(-1)?.value !== value) {\n                    treesAtDepth[depth + 1] = [];\n                    node.children = treesAtDepth[depth + 1];\n                    treesAtDepth[depth].push(node);\n                }\n            }\n        }\n        return tree;\n    }\n    export() {\n        return {\n            cols: this.columns,\n            rows: this.rows,\n            measures: this.measures,\n            fieldsType: this.fieldsType,\n        };\n    }\n}\nconst EMPTY_PIVOT_CELL = { type: \"EMPTY\" };\n\n/**\n * This function converts a list of data entry into a spreadsheet pivot table.\n */\nfunction dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {\n    const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);\n    const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);\n    computeWidthOfColumnsNodes(columnsTree, measureIds.length);\n    const cols = columnsTreeToColumns(columnsTree, definition);\n    const rows = dataEntriesToRows(dataEntries, 0, definition.rows, [], []);\n    // Add the total row\n    rows.push({\n        fields: [],\n        values: [],\n        indent: 0,\n    });\n    const fieldsType = {};\n    for (const columns of definition.columns) {\n        fieldsType[columns.fieldName] = columns.type;\n    }\n    for (const row of definition.rows) {\n        fieldsType[row.fieldName] = row.type;\n    }\n    return new SpreadsheetPivotTable(cols, rows, measureIds, fieldsType);\n}\n// -----------------------------------------------------------------------------\n// ROWS\n// -----------------------------------------------------------------------------\n/**\n * Create the rows from the data entries. This function is called recursively\n * for each level of rows dimensions.\n */\nfunction dataEntriesToRows(dataEntries, index, rows, fields, values) {\n    if (index >= rows.length) {\n        return [];\n    }\n    const row = rows[index];\n    const rowName = row.nameWithGranularity;\n    const groups = groupPivotDataEntriesBy(dataEntries, row);\n    const orderedKeys = orderDataEntriesKeys(groups, row);\n    const pivotTableRows = [];\n    const _fields = fields.concat(rowName);\n    for (const value of orderedKeys) {\n        const _values = values.concat(value);\n        pivotTableRows.push({\n            fields: _fields,\n            values: _values,\n            indent: index,\n        });\n        const record = groups[value];\n        if (record) {\n            pivotTableRows.push(...dataEntriesToRows(record, index + 1, rows, _fields, _values));\n        }\n    }\n    return pivotTableRows;\n}\n// -----------------------------------------------------------------------------\n// COLUMNS\n// -----------------------------------------------------------------------------\n/**\n * Create the columns tree from data entries.\n */\nfunction dataEntriesToColumnsTree(dataEntries, columns, index) {\n    if (index >= columns.length) {\n        return [];\n    }\n    const column = columns[index];\n    const colName = columns[index].nameWithGranularity;\n    const groups = groupPivotDataEntriesBy(dataEntries, column);\n    const orderedKeys = orderDataEntriesKeys(groups, columns[index]);\n    return orderedKeys.map((key) => {\n        return {\n            value: groups[key]?.[0]?.[column.nameWithGranularity]?.value ?? null,\n            field: colName,\n            children: dataEntriesToColumnsTree(groups[key] || [], columns, index + 1),\n            width: 0,\n        };\n    });\n}\n/**\n * Compute the width of each node in the column tree.\n * The width of a node is the sum of the width of its children.\n * For leaf nodes, the width is the number of measures.\n */\nfunction computeWidthOfColumnsNodes(tree, measureCount) {\n    for (const key in tree) {\n        const node = tree[key];\n        if (node.children.length === 0) {\n            node.width = measureCount;\n        }\n        else {\n            computeWidthOfColumnsNodes(node.children, measureCount);\n            node.width = node.children.reduce((acc, child) => acc + child.width, 0);\n        }\n    }\n}\n/**\n * Convert the columns tree to the columns\n */\nfunction columnsTreeToColumns(mainTree, definition) {\n    const columnNames = definition.columns.map((col) => col.nameWithGranularity);\n    const height = columnNames.length;\n    const measures = definition.measures.filter((measure) => !measure.isHidden);\n    const measureCount = measures.length;\n    const headers = new Array(height).fill(0).map(() => []);\n    function generateTreeHeaders(tree, rowIndex, val) {\n        const row = headers[rowIndex];\n        for (const node of tree) {\n            const localVal = val.concat([node.value]);\n            const cell = {\n                fields: columnNames.slice(0, rowIndex + 1),\n                values: localVal,\n                width: node.width,\n                offset: 0,\n            };\n            row.push(cell);\n            if (rowIndex <= height - 1) {\n                generateTreeHeaders(node.children, rowIndex + 1, localVal);\n            }\n        }\n    }\n    generateTreeHeaders(mainTree, 0, []);\n    const hasColGroupBys = columnNames.length > 0;\n    // 2) generate measures row\n    const measureRow = [];\n    if (hasColGroupBys) {\n        headers[headers.length - 1].forEach((cell) => {\n            measures.forEach((measure) => {\n                const measureCell = {\n                    fields: [...cell.fields, \"measure\"],\n                    values: [...cell.values, measure.id],\n                    width: 1,\n                    offset: 0,\n                };\n                measureRow.push(measureCell);\n            });\n        });\n    }\n    // Add the totals of the measures\n    measures.forEach((measure) => {\n        const measureCell = {\n            fields: [\"measure\"],\n            values: [measure.id],\n            width: 1,\n            offset: 0,\n        };\n        measureRow.push(measureCell);\n    });\n    headers.push(measureRow);\n    // 3) Add the total cell\n    if (headers.length === 1) {\n        headers.unshift([]); // Will add the total there\n    }\n    headers[headers.length - 2].push({\n        fields: [],\n        values: [],\n        width: measureCount,\n        offset: 0,\n    });\n    return headers;\n}\n// -----------------------------------------------------------------------------\n// HELPERS\n// -----------------------------------------------------------------------------\n/**\n * Group the dataEntries based on the given dimension\n */\nfunction groupPivotDataEntriesBy(dataEntries, dimension) {\n    return Object.groupBy(dataEntries, keySelector(dimension));\n}\n/**\n * Function used to identify the key that should be used to group dataEntries\n */\nfunction keySelector(dimension) {\n    const name = dimension.nameWithGranularity;\n    return (item) => {\n        return `${item[name]?.value ?? null}`;\n    };\n}\n/**\n * Order the keys of the given data entries, based on the given dimension\n */\nfunction orderDataEntriesKeys(groups, dimension) {\n    const order = dimension.order;\n    if (!order) {\n        return Object.keys(groups);\n    }\n    return Object.keys(groups).sort((a, b) => compareDimensionValues(dimension, a, b));\n}\n/**\n * Function used to compare two values, based on the type of the given dimension.\n * Used to order two values\n */\nfunction compareDimensionValues(dimension, a, b) {\n    if (a === \"null\") {\n        return dimension.order === \"asc\" ? 1 : -1;\n    }\n    if (b === \"null\") {\n        return dimension.order === \"asc\" ? -1 : 1;\n    }\n    if (dimension.type === \"integer\" || dimension.type === \"datetime\") {\n        return dimension.order === \"asc\" ? Number(a) - Number(b) : Number(b) - Number(a);\n    }\n    return dimension.order === \"asc\" ? a.localeCompare(b) : b.localeCompare(a);\n}\n\nconst NULL_SYMBOL = Symbol(\"NULL\");\nfunction createDate(dimension, value, locale) {\n    const granularity = dimension.granularity;\n    if (!granularity || !(granularity in MAP_VALUE_DIMENSION_DATE)) {\n        throw new Error(`Unknown date granularity: ${granularity}`);\n    }\n    const keyInMap = typeof value === \"number\" || typeof value === \"string\" ? value : NULL_SYMBOL;\n    if (!MAP_VALUE_DIMENSION_DATE[granularity].set.has(value)) {\n        MAP_VALUE_DIMENSION_DATE[granularity].set.add(value);\n        let number = null;\n        if (typeof value === \"number\" || typeof value === \"string\") {\n            const date = toJsDate(value, locale);\n            switch (granularity) {\n                case \"year\":\n                    number = date.getFullYear();\n                    break;\n                case \"quarter_number\":\n                    number = Math.floor(date.getMonth() / 3) + 1;\n                    break;\n                case \"month_number\":\n                    number = date.getMonth() + 1;\n                    break;\n                case \"iso_week_number\":\n                    number = date.getIsoWeek();\n                    break;\n                case \"day_of_month\":\n                    number = date.getDate();\n                    break;\n                case \"day\":\n                    number = Math.floor(toNumber(value, locale));\n                    break;\n                case \"day_of_week\":\n                    /**\n                     * getDay() returns the day of the week in the range 0-6, with 0\n                     * being Sunday. We need to normalize this to the range 1-7, with 1\n                     * being the first day of the week depending on the locale.\n                     * Normalized value: fr_FR: 1: Monday, 7: Sunday   (weekStart = 1)\n                     *                   en_US: 1: Sunday, 7: Saturday (weekStart = 7)\n                     */\n                    number = ((date.getDay() + 7 - locale.weekStart) % 7) + 1;\n                    break;\n                case \"hour_number\":\n                    number = date.getHours();\n                    break;\n                case \"minute_number\":\n                    number = date.getMinutes();\n                    break;\n                case \"second_number\":\n                    number = date.getSeconds();\n                    break;\n            }\n        }\n        MAP_VALUE_DIMENSION_DATE[granularity].values[keyInMap] = toNormalizedPivotValue(dimension, number);\n    }\n    return MAP_VALUE_DIMENSION_DATE[granularity].values[keyInMap];\n}\n/**\n * This map is used to cache the different values of a pivot date value\n * 43_831 -> 01/01/2012\n * Example: {\n *   year: {\n *     set: { 43_831 },\n *     values: { '43_831': 2012 }\n *   },\n *   quarter_number: {\n *     set: { 43_831 },\n *     values: { '43_831': 1 }\n *   },\n *   month_number: {\n *     set: { 43_831 },\n *     values: { '43_831': 0 }\n *   },\n *   iso_week_number: {\n *     set: { 43_831 },\n *     values: { '43_831': 1 }\n *   },\n *   day_of_month: {\n *     set: { 43_831 },\n *     values: { '43_831': 1 }\n *   },\n *   day: {\n *     set: { 43_831 },\n *     values: { '43_831': 43_831 }\n *   }\n *   day_of_week: {\n *     set: { 45_387 },\n *     values: { '45_387': 6 } (in locale with startWeek = 7)\n *   }\n *   hour_number: {\n *     set: { 45_387.13 },\n *     values: { '45_387.13': 3 }\n *   }\n *   minute_number: {\n *     set: { 45_387.13 },\n *     values: { '45_387.13': 7 }\n *   }\n *   second_number: {\n *     set: { 45_387.13 },\n *     values: { '45_387.13': 12 }\n *   }\n * }\n */\nconst MAP_VALUE_DIMENSION_DATE = {\n    year: {\n        set: new Set(),\n        values: {},\n    },\n    quarter_number: {\n        set: new Set(),\n        values: {},\n    },\n    month_number: {\n        set: new Set(),\n        values: {},\n    },\n    iso_week_number: {\n        set: new Set(),\n        values: {},\n    },\n    day_of_month: {\n        set: new Set(),\n        values: {},\n    },\n    day: {\n        set: new Set(),\n        values: {},\n    },\n    day_of_week: {\n        set: new Set(),\n        values: {},\n    },\n    hour_number: {\n        set: new Set(),\n        values: {},\n    },\n    minute_number: {\n        set: new Set(),\n        values: {},\n    },\n    second_number: {\n        set: new Set(),\n        values: {},\n    },\n};\n/**\n * Reset the cache of the pivot date values.\n */\nfunction resetMapValueDimensionDate() {\n    for (const key in MAP_VALUE_DIMENSION_DATE) {\n        MAP_VALUE_DIMENSION_DATE[key].set.clear();\n        MAP_VALUE_DIMENSION_DATE[key].values = {};\n    }\n}\n\nvar ReloadType;\n(function (ReloadType) {\n    ReloadType[ReloadType[\"NONE\"] = 0] = \"NONE\";\n    ReloadType[ReloadType[\"TABLE\"] = 1] = \"TABLE\";\n    ReloadType[ReloadType[\"DATA\"] = 2] = \"DATA\";\n    ReloadType[ReloadType[\"DEFINITION\"] = 3] = \"DEFINITION\";\n    ReloadType[ReloadType[\"ALL\"] = 4] = \"ALL\";\n})(ReloadType || (ReloadType = {}));\n/**\n * This class represents a pivot table that is created from a range of cells.\n * It will extract the fields from the first row of the range and the data from\n * the rest of the rows.\n */\nclass SpreadsheetPivot {\n    type = \"SPREADSHEET\";\n    getters;\n    _definition;\n    coreDefinition;\n    metaData = { fields: {}, fieldKeys: [] };\n    /**\n     * This array contains the data entries of the pivot. Each entry is an object\n     * that contains the values of the fields for a row.\n     */\n    dataEntries = [];\n    /**\n     * This object contains the pivot table structure. It is created from the\n     * data entries and the pivot definition.\n     */\n    table;\n    /**\n     * This error is set when the range is invalid. It is used to show an error\n     * message to the user.\n     */\n    invalidRangeError;\n    /**\n     * This flag is used to know when the pivot needs to be reloaded. It's only\n     * used in the evaluation process. At the end of each cycle, the flag is set\n     * to true so the pivot is reloaded in the next cycle.\n     */\n    needsReevaluation = true;\n    constructor(custom, params) {\n        this.getters = params.getters;\n        this.coreDefinition = params.definition;\n    }\n    init(params = {}) {\n        if (!this._definition || params.reload) {\n            this.reload(ReloadType.ALL);\n            this.needsReevaluation = false;\n        }\n    }\n    reload(type) {\n        if (type === ReloadType.ALL) {\n            this.metaData = this.loadMetaData();\n        }\n        if (type >= ReloadType.DEFINITION) {\n            this._definition = this.loadRuntimeDefinition();\n        }\n        if (type >= ReloadType.DATA) {\n            this.dataEntries = this.loadData();\n        }\n        if (type >= ReloadType.TABLE) {\n            this.table = undefined;\n        }\n    }\n    onDefinitionChange(nextDefinition) {\n        const actualDefinition = this.coreDefinition;\n        this.coreDefinition = nextDefinition;\n        if (this._definition) {\n            const reloadType = Math.max(this.computeShouldReload(actualDefinition, nextDefinition), ReloadType.NONE);\n            this.reload(reloadType);\n        }\n    }\n    computeShouldReload(actualDefinition, nextDefinition) {\n        if (deepEquals(actualDefinition.dataSet, nextDefinition.dataSet)) {\n            return ReloadType.DEFINITION;\n        }\n        return ReloadType.ALL;\n    }\n    get isInvalidRange() {\n        return !!this.invalidRangeError;\n    }\n    get invalidRangeMessage() {\n        return this.invalidRangeError?.message;\n    }\n    get definition() {\n        if (!this._definition) {\n            this.init();\n        }\n        if (!this._definition) {\n            throw new Error(\"Pivot definition should be defined at this point.\");\n        }\n        return this._definition;\n    }\n    isValid() {\n        if (this.invalidRangeError || !this.definition) {\n            return false;\n        }\n        for (const measure of this.definition.measures) {\n            if (!measure.isValid) {\n                return false;\n            }\n        }\n        for (const column of this.definition.columns) {\n            if (!column.isValid) {\n                return false;\n            }\n        }\n        for (const row of this.definition.rows) {\n            if (!row.isValid) {\n                return false;\n            }\n        }\n        return true;\n    }\n    assertIsValid({ throwOnError }) {\n        if (!this.isValid()) {\n            if (throwOnError) {\n                if (this.invalidRangeError) {\n                    throw this.invalidRangeError;\n                }\n                else {\n                    throw new EvaluationError(_t(\"At least one measure and/or dimension is not correct.\"));\n                }\n            }\n            return {\n                value: CellErrorType.GenericError,\n                message: this.invalidRangeError?.message ??\n                    _t(\"At least one measure and/or dimension is not correct.\"),\n            };\n        }\n        return undefined;\n    }\n    areDomainArgsFieldsValid(args) {\n        let dimensions = args.filter((_, index) => index % 2 === 0).map(toString);\n        if (dimensions.length && dimensions.at(-1) === \"measure\") {\n            dimensions = dimensions.slice(0, -1);\n        }\n        return areDomainArgsFieldsValid(dimensions, this.definition);\n    }\n    parseArgsToPivotDomain(args) {\n        const domain = [];\n        for (let i = 0; i < args.length - 1; i += 2) {\n            const fieldWithGranularity = toString(args[i]);\n            const type = this.getTypeOfDimension(fieldWithGranularity);\n            const normalizedValue = fieldWithGranularity === \"measure\"\n                ? toString(args[i + 1])\n                : toNormalizedPivotValue(this.getDimension(fieldWithGranularity), args[i + 1]);\n            domain.push({ field: fieldWithGranularity, value: normalizedValue, type });\n        }\n        return domain;\n    }\n    markAsDirtyForEvaluation() {\n        this.needsReevaluation = true;\n    }\n    getMeasure(id) {\n        return this.definition.getMeasure(id);\n    }\n    getPivotMeasureValue(id) {\n        return {\n            value: this.getMeasure(id).displayName,\n        };\n    }\n    getPivotHeaderValueAndFormat(domain) {\n        const lastNode = domain.at(-1);\n        if (!lastNode) {\n            return { value: _t(\"Total\") };\n        }\n        const dimension = this.getDimension(lastNode.field);\n        const cells = this.filterDataEntriesFromDomain(this.dataEntries, domain);\n        const finalCell = cells[0]?.[dimension.nameWithGranularity];\n        if (dimension.type === \"datetime\") {\n            const adapter = pivotTimeAdapter(dimension.granularity);\n            return adapter.toValueAndFormat(lastNode.value, this.getters.getLocale());\n        }\n        if (!finalCell) {\n            return { value: \"\" };\n        }\n        if (finalCell.value === null) {\n            return { value: _t(\"(Undefined)\") };\n        }\n        return {\n            value: finalCell.value,\n            format: finalCell.format,\n        };\n    }\n    getPivotCellValueAndFormat(measureId, domain) {\n        const dataEntries = this.filterDataEntriesFromDomain(this.dataEntries, domain);\n        if (dataEntries.length === 0) {\n            return { value: \"\" };\n        }\n        const measure = this.getMeasure(measureId);\n        const allValues = dataEntries.map((value) => value[measure.fieldName]).filter(isDefined);\n        const values = allValues.filter((cell) => cell.type !== CellValueType.empty);\n        const aggregator = measure.aggregator;\n        const operator = AGGREGATORS_FN[aggregator];\n        if (!operator) {\n            throw new Error(`Aggregator ${aggregator} does not exist`);\n        }\n        try {\n            const result = operator([allValues], this.getters.getLocale());\n            if (values.length === 0) {\n                return { ...result, value: \"\" };\n            }\n            return result;\n        }\n        catch (e) {\n            return handleError(e, aggregator.toUpperCase());\n        }\n    }\n    getPossibleFieldValues(dimension) {\n        const values = [];\n        const groups = groupPivotDataEntriesBy(this.dataEntries, dimension);\n        const orderedKeys = orderDataEntriesKeys(groups, dimension);\n        for (const key of orderedKeys) {\n            values.push({\n                value: groups[key]?.[0]?.[dimension.nameWithGranularity]?.value ?? \"\",\n                label: groups[key]?.[0]?.[dimension.nameWithGranularity]?.formattedValue || \"\",\n            });\n        }\n        return values;\n    }\n    getTableStructure() {\n        if (!this.isValid()) {\n            throw new Error(\"Pivot is not valid !\");\n        }\n        if (!this.table) {\n            this.table = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition);\n        }\n        return this.table;\n    }\n    getFields() {\n        return this.metaData.fields;\n    }\n    get fields() {\n        return this.getFields();\n    }\n    loadMetaData() {\n        this.invalidRangeError = undefined;\n        if (this.coreDefinition.dataSet) {\n            const { zone, sheetId } = this.coreDefinition.dataSet;\n            const range = this.getters.getRangeFromZone(sheetId, zone);\n            try {\n                return this.extractFieldsFromRange(range);\n            }\n            catch (e) {\n                this.invalidRangeError = e;\n                return { fields: {}, fieldKeys: [] };\n            }\n        }\n        else {\n            this.invalidRangeError = new EvaluationError(_t(\"The pivot cannot be created because the dataset is missing.\"));\n            return { fields: {}, fieldKeys: [] };\n        }\n    }\n    loadRuntimeDefinition() {\n        return new SpreadsheetPivotRuntimeDefinition(this.coreDefinition, this.fields, this.getters);\n    }\n    loadData() {\n        const range = this._definition?.range;\n        return this.isValid() && range ? this.extractDataEntriesFromRange(range) : [];\n    }\n    getTypeOfDimension(fieldWithGranularity) {\n        if (fieldWithGranularity === \"measure\") {\n            return \"char\";\n        }\n        const { fieldName } = parseDimension(fieldWithGranularity);\n        const type = this.fields[fieldName]?.type;\n        if (!type) {\n            throw new Error(`Field ${fieldName} does not exist`);\n        }\n        return type;\n    }\n    filterDataEntriesFromDomain(dataEntries, domain) {\n        return domain.reduce((current, acc) => this.filterDataEntriesFromDomainNode(current, acc), dataEntries);\n    }\n    filterDataEntriesFromDomainNode(dataEntries, domain) {\n        const { field, value } = domain;\n        const { nameWithGranularity } = this.getDimension(field);\n        return dataEntries.filter((entry) => entry[nameWithGranularity]?.value === value);\n    }\n    getDimension(nameWithGranularity) {\n        return this.definition.getDimension(nameWithGranularity);\n    }\n    getTypeFromZone(sheetId, zone) {\n        const cells = this.getters.getEvaluatedCellsInZone(sheetId, zone);\n        const nonEmptyCells = cells.filter((cell) => cell.type !== CellValueType.empty);\n        if (nonEmptyCells.length === 0) {\n            return \"integer\";\n        }\n        if (nonEmptyCells.every((cell) => cell.format && isDateTimeFormat(cell.format))) {\n            return \"datetime\";\n        }\n        if (nonEmptyCells.every((cell) => cell.type === CellValueType.boolean)) {\n            return \"boolean\";\n        }\n        if (nonEmptyCells.some((cell) => cell.type === CellValueType.text)) {\n            return \"char\";\n        }\n        return \"integer\";\n    }\n    assertCellIsValidField(col, row, cell) {\n        if (cell.type === CellValueType.error) {\n            throw new EvaluationError(_t(\"The pivot cannot be created because cell %s contains an error\", toXC(col, row)));\n        }\n        if (cell.type === CellValueType.empty || cell.value === \"\") {\n            throw new EvaluationError(_t(\"The pivot cannot be created because cell %s is empty\", toXC(col, row)));\n        }\n        if (cell.value === \"__count\") {\n            throw new EvaluationError(_t(\"The pivot cannot be created because cell %s contains a reserved value\", toXC(col, row)));\n        }\n    }\n    /**\n     * Create the fields from the given range. It will extract all the fields from\n     * the first row of the range.\n     */\n    extractFieldsFromRange(range) {\n        const fields = {};\n        const fieldKeys = [];\n        const sheetId = range.sheetId;\n        const row = range.zone.top;\n        for (let col = range.zone.left; col <= range.zone.right; col++) {\n            const cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n            this.assertCellIsValidField(col, row, cell);\n            const field = cell.value?.toString();\n            if (field) {\n                const type = this.getTypeFromZone(sheetId, {\n                    top: range.zone.top + 1,\n                    left: col,\n                    bottom: range.zone.bottom,\n                    right: col,\n                });\n                const string = this.findName(field, fields);\n                fields[string] = {\n                    name: string,\n                    type,\n                    string,\n                    aggregator: type === \"integer\" ? \"sum\" : \"count\",\n                };\n                fieldKeys.push(string);\n            }\n        }\n        return { fields, fieldKeys };\n    }\n    /**\n     * Take cares of double names\n     */\n    findName(name, fields) {\n        let increment = 1;\n        const initialName = name;\n        while (name in fields) {\n            name = `${initialName}${++increment}`;\n        }\n        return name;\n    }\n    extractDataEntriesFromRange(range) {\n        const dataEntries = [];\n        for (let row = range.zone.top + 1; row <= range.zone.bottom; row++) {\n            const zone = { top: row, bottom: row, left: range.zone.left, right: range.zone.right };\n            const cells = this.getters.getEvaluatedCellsInZone(range.sheetId, zone);\n            const entry = {};\n            for (const index in cells) {\n                const cell = cells[index];\n                const field = this.fields[this.metaData.fieldKeys[index]];\n                if (!field) {\n                    throw new Error(`Field ${this.metaData.fieldKeys[index]} does not exist`);\n                }\n                if (cell.value === \"\") {\n                    entry[field.name] = { value: null, type: CellValueType.empty, formattedValue: \"\" };\n                }\n                else {\n                    entry[field.name] = cell;\n                }\n            }\n            entry[\"__count\"] = { value: 1, type: CellValueType.number, formattedValue: \"1\" };\n            dataEntries.push(entry);\n        }\n        const dateDimensions = this.definition.columns\n            .concat(this.definition.rows)\n            .filter((d) => d.type === \"datetime\");\n        if (dateDimensions.length) {\n            const locale = this.getters.getLocale();\n            for (const entry of dataEntries) {\n                for (const dimension of dateDimensions) {\n                    const value = createDate(dimension, entry[dimension.fieldName]?.value || null, this.getters.getLocale());\n                    const adapter = pivotTimeAdapter(dimension.granularity);\n                    const { format, value: valueToFormat } = adapter.toValueAndFormat(value, locale);\n                    entry[dimension.nameWithGranularity] = {\n                        value,\n                        type: entry[dimension.fieldName]?.type || CellValueType.empty,\n                        format: entry[dimension.fieldName]?.format,\n                        formattedValue: formatValue(valueToFormat, { locale, format }),\n                    };\n                }\n            }\n        }\n        return dataEntries;\n    }\n}\n\nconst pivotRegistry = new Registry();\nconst dateGranularities = [\n    \"year\",\n    \"quarter_number\",\n    \"month_number\",\n    \"iso_week_number\",\n    \"day_of_month\",\n    \"day\",\n    \"day_of_week\",\n];\npivotRegistry.add(\"SPREADSHEET\", {\n    ui: SpreadsheetPivot,\n    definition: SpreadsheetPivotRuntimeDefinition,\n    externalData: false,\n    onIterationEndEvaluation: (pivot) => pivot.markAsDirtyForEvaluation(),\n    dateGranularities: [...dateGranularities],\n    datetimeGranularities: [...dateGranularities, \"hour_number\", \"minute_number\", \"second_number\"],\n    isMeasureCandidate: (field) => ![\"datetime\", \"boolean\"].includes(field.type),\n    isGroupable: () => true,\n});\n\nclass PivotSidePanelStore extends SpreadsheetStore {\n    pivotId;\n    mutators = [\"reset\", \"deferUpdates\", \"applyUpdate\", \"discardPendingUpdate\", \"update\"];\n    updatesAreDeferred = false;\n    draft = null;\n    notification = this.get(NotificationStore);\n    alreadyNotified = false;\n    constructor(get, pivotId) {\n        super(get);\n        this.pivotId = pivotId;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"UPDATE_PIVOT\":\n                if (cmd.pivotId === this.pivotId) {\n                    this.getters.getPivot(this.pivotId).init();\n                }\n        }\n    }\n    get fields() {\n        return this.pivot.getFields();\n    }\n    get pivot() {\n        return this.getters.getPivot(this.pivotId);\n    }\n    get definition() {\n        const Definition = pivotRegistry.get(this.pivot.type).definition;\n        return this.draft\n            ? new Definition(this.draft, this.fields, this.getters)\n            : this.pivot.definition;\n    }\n    get isDirty() {\n        return !!this.draft;\n    }\n    get measureFields() {\n        const measureFields = [\n            {\n                name: \"__count\",\n                string: _t(\"Count\"),\n                type: \"integer\",\n                aggregator: \"sum\",\n            },\n        ];\n        const fields = this.fields;\n        for (const fieldName in fields) {\n            const field = fields[fieldName];\n            if (!field) {\n                continue;\n            }\n            if (pivotRegistry.get(this.pivot.type).isMeasureCandidate(field)) {\n                measureFields.push(field);\n            }\n        }\n        return measureFields.sort((a, b) => a.string.localeCompare(b.string));\n    }\n    get unusedGroupableFields() {\n        const groupableFields = [];\n        const fields = this.fields;\n        for (const fieldName in fields) {\n            const field = fields[fieldName];\n            if (!field) {\n                continue;\n            }\n            if (pivotRegistry.get(this.pivot.type).isGroupable(field)) {\n                groupableFields.push(field);\n            }\n        }\n        const { columns, rows, measures } = this.definition;\n        const currentlyUsed = measures\n            .concat(rows)\n            .concat(columns)\n            .map((field) => field.fieldName);\n        const unusedGranularities = this.unusedGranularities;\n        return groupableFields\n            .filter((field) => {\n            if (isDateOrDatetimeField(field)) {\n                return !currentlyUsed.includes(field.name) || unusedGranularities[field.name].size > 0;\n            }\n            return !currentlyUsed.includes(field.name);\n        })\n            .sort((a, b) => a.string.localeCompare(b.string));\n    }\n    get datetimeGranularities() {\n        return pivotRegistry.get(this.pivot.type).datetimeGranularities;\n    }\n    get dateGranularities() {\n        return pivotRegistry.get(this.pivot.type).dateGranularities;\n    }\n    get unusedGranularities() {\n        return this.getUnusedGranularities(this.fields, this.draft ?? this.getters.getPivotCoreDefinition(this.pivotId));\n    }\n    reset(pivotId) {\n        this.pivotId = pivotId;\n        this.updatesAreDeferred = true;\n        this.draft = null;\n    }\n    deferUpdates(shouldDefer) {\n        this.updatesAreDeferred = shouldDefer;\n        if (shouldDefer === false && this.draft) {\n            this.applyUpdate();\n        }\n    }\n    applyUpdate() {\n        if (this.draft) {\n            this.model.dispatch(\"UPDATE_PIVOT\", {\n                pivotId: this.pivotId,\n                pivot: this.draft,\n            });\n            this.draft = null;\n            if (!this.alreadyNotified && !this.isDynamicPivotInViewport()) {\n                const formulaId = this.getters.getPivotFormulaId(this.pivotId);\n                const pivotExample = `=PIVOT(${formulaId})`;\n                this.alreadyNotified = true;\n                this.notification.notifyUser({\n                    type: \"info\",\n                    text: _t(\"Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.\", pivotExample),\n                    sticky: false,\n                });\n            }\n        }\n    }\n    discardPendingUpdate() {\n        this.draft = null;\n    }\n    update(definitionUpdate) {\n        const coreDefinition = this.getters.getPivotCoreDefinition(this.pivotId);\n        const definition = {\n            ...coreDefinition,\n            ...this.draft,\n            ...definitionUpdate,\n        };\n        // clean to make sure we only keep the core properties\n        const cleanedDefinition = {\n            ...definition,\n            columns: definition.columns.map((col) => ({\n                fieldName: col.fieldName,\n                order: col.order,\n                granularity: col.granularity,\n            })),\n            rows: definition.rows.map((row) => ({\n                fieldName: row.fieldName,\n                order: row.order,\n                granularity: row.granularity,\n            })),\n            measures: definition.measures.map((measure) => ({\n                id: measure.id,\n                fieldName: measure.fieldName,\n                aggregator: measure.aggregator,\n                userDefinedName: measure.userDefinedName,\n                computedBy: measure.computedBy,\n                isHidden: measure.isHidden,\n                format: measure.format,\n                display: measure.display,\n            })),\n        };\n        if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {\n            return;\n        }\n        const cleanedWithGranularity = this.addDefaultDateTimeGranularity(this.fields, cleanedDefinition);\n        this.draft = cleanedWithGranularity;\n        if (!this.updatesAreDeferred) {\n            this.applyUpdate();\n        }\n    }\n    isDynamicPivotInViewport() {\n        const sheetId = this.getters.getActiveSheetId();\n        for (const col of this.getters.getSheetViewVisibleCols()) {\n            for (const row of this.getters.getSheetViewVisibleRows()) {\n                const isDynamicPivot = this.getters.isSpillPivotFormula({ sheetId, col, row });\n                if (isDynamicPivot) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n    addDefaultDateTimeGranularity(fields, definition) {\n        const { columns, rows } = definition;\n        const columnsWithGranularity = deepCopy(columns);\n        const rowsWithGranularity = deepCopy(rows);\n        const unusedGranularities = this.getUnusedGranularities(fields, definition);\n        for (const dimension of columnsWithGranularity.concat(rowsWithGranularity)) {\n            const fieldType = fields[dimension.fieldName]?.type;\n            if ((fieldType === \"date\" || fieldType === \"datetime\") && !dimension.granularity) {\n                const granularity = unusedGranularities[dimension.fieldName]?.values().next().value || \"year\";\n                unusedGranularities[dimension.fieldName]?.delete(granularity);\n                dimension.granularity = granularity;\n            }\n        }\n        return {\n            ...definition,\n            columns: columnsWithGranularity,\n            rows: rowsWithGranularity,\n        };\n    }\n    getUnusedGranularities(fields, definition) {\n        const { columns, rows } = definition;\n        const dateFields = columns.concat(rows).filter((dimension) => {\n            const fieldType = fields[dimension.fieldName]?.type;\n            return fieldType === \"date\" || fieldType === \"datetime\";\n        });\n        const granularitiesPerFields = {};\n        for (const field of dateFields) {\n            granularitiesPerFields[field.fieldName] = new Set(fields[field.fieldName]?.type === \"date\"\n                ? this.dateGranularities\n                : this.datetimeGranularities);\n        }\n        for (const field of dateFields) {\n            granularitiesPerFields[field.fieldName].delete(field.granularity);\n        }\n        return granularitiesPerFields;\n    }\n}\n\nclass PivotSpreadsheetSidePanel extends Component {\n    static template = \"o-spreadsheet-PivotSpreadsheetSidePanel\";\n    static props = {\n        pivotId: String,\n        onCloseSidePanel: Function,\n    };\n    static components = {\n        PivotLayoutConfigurator,\n        Section,\n        SelectionInput,\n        Checkbox,\n        PivotDeferUpdate,\n        PivotTitleSection,\n    };\n    store;\n    state;\n    setup() {\n        this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId);\n        this.state = useState({\n            range: undefined,\n            rangeHasChanged: false,\n        });\n    }\n    get shouldDisplayInvalidRangeError() {\n        if (this.store.isDirty && this.state.rangeHasChanged) {\n            return false;\n        }\n        return this.pivot.isInvalidRange;\n    }\n    get ranges() {\n        if (this.state.range) {\n            return [this.state.range];\n        }\n        if (this.definition.range) {\n            return [this.env.model.getters.getRangeString(this.definition.range, \"forceSheetReference\")];\n        }\n        return [];\n    }\n    get pivot() {\n        return this.store.pivot;\n    }\n    get definition() {\n        return this.store.definition;\n    }\n    onSelectionChanged(ranges) {\n        this.state.rangeHasChanged = true;\n        this.state.range = ranges[0];\n    }\n    onSelectionConfirmed() {\n        if (this.state.range) {\n            const range = this.env.model.getters.getRangeFromSheetXC(this.env.model.getters.getActiveSheetId(), this.state.range);\n            if (range.invalidSheetName || range.invalidXc) {\n                return;\n            }\n            const dataSet = { sheetId: range.sheetId, zone: range.zone };\n            this.store.update({ dataSet });\n            // Immediately apply the update to recompute the pivot fields\n            this.store.applyUpdate();\n        }\n    }\n    flipAxis() {\n        const { rows, columns } = this.definition;\n        this.onDimensionsUpdated({\n            rows: columns,\n            columns: rows,\n        });\n    }\n    onDimensionsUpdated(definition) {\n        this.store.update(definition);\n    }\n}\n\nconst pivotSidePanelRegistry = new Registry();\npivotSidePanelRegistry.add(\"SPREADSHEET\", {\n    editor: PivotSpreadsheetSidePanel,\n});\n\nclass PivotSidePanel extends Component {\n    static template = \"o-spreadsheet-PivotSidePanel\";\n    static props = {\n        pivotId: String,\n        onCloseSidePanel: Function,\n    };\n    static components = {\n        PivotLayoutConfigurator,\n        Section,\n    };\n    get sidePanelEditor() {\n        const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);\n        if (!pivot) {\n            throw new Error(\"pivotId does not correspond to a pivot.\");\n        }\n        return pivotSidePanelRegistry.get(pivot.type).editor;\n    }\n}\n\ncss /* scss */ `\n  .o-checkbox-selection {\n    max-height: 300px;\n  }\n`;\nclass RemoveDuplicatesPanel extends Component {\n    static template = \"o-spreadsheet-RemoveDuplicatesPanel\";\n    static components = { ValidationMessages, Section, Checkbox };\n    static props = { onCloseSidePanel: Function };\n    state = useState({\n        hasHeader: false,\n        columns: {},\n    });\n    setup() {\n        onWillUpdateProps(() => this.updateColumns());\n    }\n    toggleHasHeader() {\n        this.state.hasHeader = !this.state.hasHeader;\n    }\n    toggleAllColumns() {\n        const newState = !this.isEveryColumnSelected;\n        for (const index in this.state.columns) {\n            this.state.columns[index] = newState;\n        }\n    }\n    toggleColumn(colIndex) {\n        this.state.columns[colIndex] = !this.state.columns[colIndex];\n    }\n    onRemoveDuplicates() {\n        this.env.model.dispatch(\"REMOVE_DUPLICATES\", {\n            hasHeader: this.state.hasHeader,\n            columns: this.getColsToAnalyze(),\n        });\n    }\n    getColLabel(colKey) {\n        const col = parseInt(colKey);\n        let colLabel = _t(\"Column %s\", numberToLetters(col));\n        if (this.state.hasHeader) {\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            const row = this.env.model.getters.getSelectedZone().top;\n            const colHeader = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });\n            if (colHeader.type !== \"empty\") {\n                colLabel += ` - ${colHeader.value}`;\n            }\n        }\n        return colLabel;\n    }\n    get isEveryColumnSelected() {\n        return Object.values(this.state.columns).every((value) => value === true);\n    }\n    get errorMessages() {\n        const cancelledReasons = this.env.model.canDispatch(\"REMOVE_DUPLICATES\", {\n            hasHeader: this.state.hasHeader,\n            columns: this.getColsToAnalyze(),\n        }).reasons;\n        const errors = new Set();\n        for (const reason of cancelledReasons) {\n            errors.add(RemoveDuplicateTerms.Errors[reason] || RemoveDuplicateTerms.Errors.Unexpected);\n        }\n        return Array.from(errors);\n    }\n    get selectionStatisticalInformation() {\n        const dimension = zoneToDimension(this.env.model.getters.getSelectedZone());\n        return _t(\"%(row_count)s rows and %(column_count)s columns selected\", {\n            row_count: dimension.numberOfRows,\n            column_count: dimension.numberOfCols,\n        });\n    }\n    get canConfirm() {\n        return this.errorMessages.length === 0;\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    updateColumns() {\n        const zone = this.env.model.getters.getSelectedZone();\n        const oldColumns = this.state.columns;\n        const newColumns = {};\n        for (let i = zone.left; i <= zone.right; i++) {\n            newColumns[i] = i in oldColumns ? oldColumns[i] : true;\n        }\n        this.state.columns = newColumns;\n    }\n    getColsToAnalyze() {\n        return Object.keys(this.state.columns)\n            .filter((colIndex) => this.state.columns[colIndex])\n            .map((colIndex) => parseInt(colIndex));\n    }\n}\n\ncss /* scss */ `\n  .o-locale-preview {\n    border: 1px solid ${GRAY_300};\n    background-color: ${GRAY_100};\n  }\n`;\nclass SettingsPanel extends Component {\n    static template = \"o-spreadsheet-SettingsPanel\";\n    static components = { Section, ValidationMessages };\n    static props = { onCloseSidePanel: Function };\n    loadedLocales = [];\n    setup() {\n        onWillStart(() => this.loadLocales());\n    }\n    onLocaleChange(code) {\n        const locale = this.loadedLocales.find((l) => l.code === code);\n        if (!locale)\n            return;\n        this.env.model.dispatch(\"UPDATE_LOCALE\", { locale });\n    }\n    async loadLocales() {\n        this.loadedLocales = (await this.env.loadLocales())\n            .filter((locale) => {\n            const isValid = isValidLocale(locale);\n            if (!isValid) {\n                console.warn(`Invalid locale: ${locale[\"code\"]} ${locale}`);\n            }\n            return isValid;\n        })\n            .sort((a, b) => a.name.localeCompare(b.name));\n    }\n    get numberFormatPreview() {\n        const locale = this.env.model.getters.getLocale();\n        return formatValue(1234567.89, { format: \"#,##0.00\", locale });\n    }\n    get dateFormatPreview() {\n        const locale = this.env.model.getters.getLocale();\n        return formatValue(1.6, { format: locale.dateFormat, locale });\n    }\n    get dateTimeFormatPreview() {\n        const locale = this.env.model.getters.getLocale();\n        const dateTimeFormat = getDateTimeFormat(locale);\n        return formatValue(1.6, { format: dateTimeFormat, locale });\n    }\n    get firstDayOfWeek() {\n        const locale = this.env.model.getters.getLocale();\n        const weekStart = locale.weekStart;\n        // Week start: 1 = Monday, 7 = Sunday\n        // Days: 0 = Sunday, 6 = Saturday\n        return DAYS$1[weekStart % 7];\n    }\n    get currentLocale() {\n        return this.env.model.getters.getLocale();\n    }\n    get supportedLocales() {\n        const currentLocale = this.currentLocale;\n        const localeInLoadedLocales = this.loadedLocales.find((l) => l.code === currentLocale.code);\n        if (!localeInLoadedLocales) {\n            const locales = [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));\n            return locales;\n        }\n        else if (!deepEquals(currentLocale, localeInLoadedLocales)) {\n            const index = this.loadedLocales.indexOf(localeInLoadedLocales);\n            const locales = [...this.loadedLocales];\n            locales[index] = currentLocale;\n            locales.sort((a, b) => a.name.localeCompare(b.name));\n            return locales;\n        }\n        return this.loadedLocales;\n    }\n}\n\nconst SplitToColumnsInteractiveContent = {\n    SplitIsDestructive: _t(\"This will overwrite data in the subsequent columns. Split anyway?\"),\n};\nfunction interactiveSplitToColumns(env, separator, addNewColumns) {\n    let result = env.model.dispatch(\"SPLIT_TEXT_INTO_COLUMNS\", { separator, addNewColumns });\n    if (result.isCancelledBecause(\"SplitWillOverwriteContent\" /* CommandResult.SplitWillOverwriteContent */)) {\n        env.askConfirmation(SplitToColumnsInteractiveContent.SplitIsDestructive, () => {\n            result = env.model.dispatch(\"SPLIT_TEXT_INTO_COLUMNS\", {\n                separator,\n                addNewColumns,\n                force: true,\n            });\n        });\n    }\n    return result;\n}\n\nconst SEPARATORS = [\n    { name: _t(\"Detect automatically\"), value: \"auto\" },\n    { name: _t(\"Custom separator\"), value: \"custom\" },\n    { name: _t(\"Space\"), value: \" \" },\n    { name: _t(\"Comma\"), value: \",\" },\n    { name: _t(\"Semicolon\"), value: \";\" },\n    { name: _t(\"Line Break\"), value: NEWLINE },\n];\nclass SplitIntoColumnsPanel extends Component {\n    static template = \"o-spreadsheet-SplitIntoColumnsPanel\";\n    static components = { ValidationMessages, Section, Checkbox };\n    static props = { onCloseSidePanel: Function };\n    state = useState({ separatorValue: \"auto\", addNewColumns: false, customSeparator: \"\" });\n    setup() {\n        const composerFocusStore = useStore(ComposerFocusStore);\n        // The feature makes no sense if we are editing a cell, because then the selection isn't active\n        // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell\n        useEffect((editionMode) => {\n            if (editionMode !== \"inactive\") {\n                this.props.onCloseSidePanel();\n            }\n        }, () => [composerFocusStore.focusMode]);\n        onMounted(() => {\n            composerFocusStore.activeComposer.stopEdition();\n        });\n    }\n    onSeparatorChange(value) {\n        this.state.separatorValue = value;\n    }\n    updateCustomSeparator(ev) {\n        if (!ev.target)\n            return;\n        this.state.customSeparator = ev.target.value;\n    }\n    updateAddNewColumnsCheckbox(addNewColumns) {\n        this.state.addNewColumns = addNewColumns;\n    }\n    confirm() {\n        const result = interactiveSplitToColumns(this.env, this.separatorValue, this.state.addNewColumns);\n        if (result.isSuccessful) {\n            this.props.onCloseSidePanel();\n        }\n    }\n    get errorMessages() {\n        const cancelledReasons = this.env.model.canDispatch(\"SPLIT_TEXT_INTO_COLUMNS\", {\n            separator: this.separatorValue,\n            addNewColumns: this.state.addNewColumns,\n            force: true,\n        }).reasons;\n        const errors = new Set();\n        for (const reason of cancelledReasons) {\n            switch (reason) {\n                case \"SplitWillOverwriteContent\" /* CommandResult.SplitWillOverwriteContent */:\n                case \"EmptySplitSeparator\" /* CommandResult.EmptySplitSeparator */:\n                    break;\n                default:\n                    errors.add(SplitToColumnsTerms.Errors[reason] || SplitToColumnsTerms.Errors.Unexpected);\n            }\n        }\n        return Array.from(errors);\n    }\n    get warningMessages() {\n        const warnings = [];\n        const cancelledReasons = this.env.model.canDispatch(\"SPLIT_TEXT_INTO_COLUMNS\", {\n            separator: this.separatorValue,\n            addNewColumns: this.state.addNewColumns,\n            force: false,\n        }).reasons;\n        if (cancelledReasons.includes(\"SplitWillOverwriteContent\" /* CommandResult.SplitWillOverwriteContent */)) {\n            warnings.push(SplitToColumnsTerms.Errors[\"SplitWillOverwriteContent\" /* CommandResult.SplitWillOverwriteContent */]);\n        }\n        return warnings;\n    }\n    get separatorValue() {\n        if (this.state.separatorValue === \"custom\") {\n            return this.state.customSeparator;\n        }\n        else if (this.state.separatorValue === \"auto\") {\n            return this.env.model.getters.getAutomaticSeparator();\n        }\n        return this.state.separatorValue;\n    }\n    get separators() {\n        return SEPARATORS;\n    }\n    get isConfirmDisabled() {\n        return !this.separatorValue || this.errorMessages.length > 0;\n    }\n}\n\nconst TABLE_ELEMENTS_BY_PRIORITY = [\n    \"wholeTable\",\n    \"firstColumnStripe\",\n    \"secondColumnStripe\",\n    \"firstRowStripe\",\n    \"secondRowStripe\",\n    \"firstColumn\",\n    \"lastColumn\",\n    \"headerRow\",\n    \"totalRow\",\n];\n/** Return the content zone of the table, ie. the table zone without the headers */\nfunction getTableContentZone(tableZone, tableConfig) {\n    const numberOfHeaders = tableConfig.numberOfHeaders;\n    const contentZone = { ...tableZone, top: tableZone.top + numberOfHeaders };\n    return contentZone.top <= contentZone.bottom ? contentZone : undefined;\n}\nfunction getTableTopLeft(table) {\n    const range = table.range;\n    return { row: range.zone.top, col: range.zone.left, sheetId: range.sheetId };\n}\nfunction createFilter(id, range, config, createRange) {\n    const zone = range.zone;\n    if (zone.left !== zone.right) {\n        throw new Error(\"Can only define a filter on a single column\");\n    }\n    const filteredZone = { ...zone, top: zone.top + config.numberOfHeaders };\n    const filteredRange = createRange(range.sheetId, filteredZone);\n    return {\n        id,\n        rangeWithHeaders: range,\n        col: zone.left,\n        filteredRange: filteredZone.top > filteredZone.bottom ? undefined : filteredRange,\n    };\n}\nfunction isStaticTable(table) {\n    return table.type === \"static\" || table.type === \"forceStatic\";\n}\nfunction getComputedTableStyle(tableConfig, style, numberOfCols, numberOfRows) {\n    return {\n        borders: getAllTableBorders(tableConfig, style, numberOfCols, numberOfRows),\n        styles: getAllTableStyles(tableConfig, style, numberOfCols, numberOfRows),\n    };\n}\nfunction getAllTableBorders(tableConfig, style, nOfCols, nOfRows) {\n    const borders = generateMatrix(nOfCols, nOfRows, () => ({}));\n    for (const tableElement of TABLE_ELEMENTS_BY_PRIORITY) {\n        const styleBorder = style[tableElement]?.border;\n        if (!styleBorder)\n            continue;\n        const zones = getTableElementZones(tableElement, tableConfig, nOfCols, nOfRows);\n        for (const zone of zones) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    // Special case: we don't want borders inside the headers rows\n                    const noInsideBorder = tableElement === \"wholeTable\" && row <= tableConfig.numberOfHeaders - 1;\n                    if (row === zone.top && styleBorder?.top) {\n                        setBorderDescr(borders, \"top\", styleBorder.top, col, row, nOfCols, nOfRows);\n                    }\n                    else if (row !== zone.top && !noInsideBorder && styleBorder?.horizontal) {\n                        setBorderDescr(borders, \"top\", styleBorder.horizontal, col, row, nOfCols, nOfRows);\n                    }\n                    if (row === zone.bottom && styleBorder?.bottom) {\n                        setBorderDescr(borders, \"bottom\", styleBorder.bottom, col, row, nOfCols, nOfRows);\n                    }\n                    if (col === zone.left && styleBorder?.left) {\n                        setBorderDescr(borders, \"left\", styleBorder.left, col, row, nOfCols, nOfRows);\n                    }\n                    if (col === zone.right && styleBorder?.right) {\n                        setBorderDescr(borders, \"right\", styleBorder.right, col, row, nOfCols, nOfRows);\n                    }\n                    else if (col !== zone.right && !noInsideBorder && styleBorder?.vertical) {\n                        setBorderDescr(borders, \"right\", styleBorder.vertical, col, row, nOfCols, nOfRows);\n                    }\n                }\n            }\n        }\n    }\n    return borders;\n}\n/**\n * Set the border description for a given border direction (top, bottom, left, right) in the computedBorders array.\n * Also set the corresponding borders of adjacent cells (eg. if the border is set on the top of a cell, the bottom\n * border of the cell above is set).\n */\nfunction setBorderDescr(computedBorders, dir, borderDescr, col, row, numberOfCols, numberOfRows) {\n    switch (dir) {\n        case \"top\":\n            computedBorders[col][row].top = borderDescr;\n            if (row !== 0) {\n                computedBorders[col][row - 1].bottom = borderDescr;\n            }\n            return;\n        case \"bottom\":\n            computedBorders[col][row].bottom = borderDescr;\n            if (row !== numberOfRows - 1) {\n                computedBorders[col][row + 1].top = borderDescr;\n            }\n            return;\n        case \"left\":\n            computedBorders[col][row].left = borderDescr;\n            if (col !== 0) {\n                computedBorders[col - 1][row].right = borderDescr;\n            }\n            return;\n        case \"right\":\n            computedBorders[col][row].right = borderDescr;\n            if (col !== numberOfCols - 1) {\n                computedBorders[col + 1][row].left = borderDescr;\n            }\n            return;\n    }\n}\nfunction getAllTableStyles(tableConfig, style, numberOfCols, numberOfRows) {\n    const styles = generateMatrix(numberOfCols, numberOfRows, () => ({}));\n    for (const tableElement of TABLE_ELEMENTS_BY_PRIORITY) {\n        const tableElStyle = style[tableElement];\n        const bold = isTableElementInBold(tableElement);\n        if (!tableElStyle && !bold) {\n            continue;\n        }\n        const zones = getTableElementZones(tableElement, tableConfig, numberOfCols, numberOfRows);\n        for (const zone of zones) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    if (!styles[col][row]) {\n                        styles[col][row] = {};\n                    }\n                    styles[col][row] = {\n                        ...styles[col][row],\n                        ...tableElStyle?.style,\n                    };\n                    if (bold) {\n                        styles[col][row].bold = true;\n                    }\n                }\n            }\n        }\n    }\n    return styles;\n}\nfunction isTableElementInBold(tableElement) {\n    return (tableElement === \"firstColumn\" ||\n        tableElement === \"lastColumn\" ||\n        tableElement === \"headerRow\" ||\n        tableElement === \"totalRow\");\n}\nfunction getTableElementZones(el, tableConfig, numberOfCols, numberOfRows) {\n    const zones = [];\n    const headerRows = Math.min(tableConfig.numberOfHeaders, numberOfRows);\n    const totalRows = tableConfig.totalRow ? 1 : 0;\n    const lastCol = numberOfCols - 1;\n    const lastRow = numberOfRows - 1;\n    switch (el) {\n        case \"wholeTable\":\n            zones.push({ top: 0, left: 0, bottom: lastRow, right: lastCol });\n            break;\n        case \"firstColumn\":\n            if (!tableConfig.firstColumn)\n                break;\n            zones.push({ top: 0, left: 0, bottom: lastRow, right: 0 });\n            break;\n        case \"lastColumn\":\n            if (!tableConfig.lastColumn)\n                break;\n            zones.push({ top: 0, left: lastCol, bottom: lastRow, right: lastCol });\n            break;\n        case \"headerRow\":\n            if (!tableConfig.numberOfHeaders)\n                break;\n            zones.push({ top: 0, left: 0, bottom: headerRows - 1, right: lastCol });\n            break;\n        case \"totalRow\":\n            if (!tableConfig.totalRow)\n                break;\n            zones.push({ top: lastRow, left: 0, bottom: lastRow, right: lastCol });\n            break;\n        case \"firstRowStripe\":\n            if (!tableConfig.bandedRows)\n                break;\n            for (let i = headerRows; i < numberOfRows - totalRows; i += 2) {\n                zones.push({ top: i, left: 0, bottom: i, right: lastCol });\n            }\n            break;\n        case \"secondRowStripe\":\n            if (!tableConfig.bandedRows)\n                break;\n            for (let i = headerRows + 1; i < numberOfRows - totalRows; i += 2) {\n                zones.push({ top: i, left: 0, bottom: i, right: lastCol });\n            }\n            break;\n        case \"firstColumnStripe\":\n            if (!tableConfig.bandedColumns)\n                break;\n            for (let i = 0; i < numberOfCols; i += 2) {\n                zones.push({ top: headerRows, left: i, bottom: lastRow - totalRows, right: i });\n            }\n            break;\n        case \"secondColumnStripe\":\n            if (!tableConfig.bandedColumns)\n                break;\n            for (let i = 1; i < numberOfCols; i += 2) {\n                zones.push({ top: headerRows, left: i, bottom: lastRow - totalRows, right: i });\n            }\n            break;\n    }\n    return zones;\n}\n\nfunction createTableStyleContextMenuActions(env, styleId) {\n    if (!env.model.getters.isTableStyleEditable(styleId)) {\n        return [];\n    }\n    return createActions([\n        {\n            id: \"editTableStyle\",\n            name: _t(\"Edit table style\"),\n            execute: (env) => env.openSidePanel(\"TableStyleEditorPanel\", { styleId }),\n            icon: \"o-spreadsheet-Icon.EDIT\",\n        },\n        {\n            id: \"deleteTableStyle\",\n            name: _t(\"Delete table style\"),\n            execute: (env) => env.model.dispatch(\"REMOVE_TABLE_STYLE\", { tableStyleId: styleId }),\n            icon: \"o-spreadsheet-Icon.TRASH\",\n        },\n    ]);\n}\n\nfunction drawPreviewTable(ctx, tableStyle, colWidth, rowHeight) {\n    ctx.resetTransform();\n    drawBackgrounds(ctx, tableStyle, colWidth, rowHeight);\n    drawBorders(ctx, tableStyle, colWidth, rowHeight);\n    drawTexts(ctx, tableStyle, colWidth, rowHeight);\n}\nfunction drawBackgrounds(ctx, tableStyle, colWidth, rowHeight) {\n    ctx.save();\n    for (let col = 0; col < 5; col++) {\n        for (let row = 0; row < 5; row++) {\n            ctx.fillStyle = tableStyle.styles[col][row].fillColor || \"#fff\";\n            ctx.fillRect(col * colWidth, row * rowHeight, colWidth, rowHeight);\n        }\n    }\n    ctx.restore();\n}\nfunction drawBorders(ctx, tableStyle, colWidth, rowHeight) {\n    ctx.save();\n    ctx.translate(0, 0.5);\n    ctx.lineWidth = 1;\n    for (let col = 0; col < 5; col++) {\n        for (let row = 0; row < 5; row++) {\n            const borders = tableStyle.borders[col][row];\n            if (borders.top) {\n                ctx.strokeStyle = borders.top.color;\n                ctx.beginPath();\n                ctx.moveTo(col * colWidth, row * rowHeight);\n                ctx.lineTo(col * colWidth + colWidth, row * rowHeight);\n                ctx.stroke();\n            }\n            if (borders.bottom) {\n                ctx.strokeStyle = borders.bottom.color;\n                ctx.beginPath();\n                ctx.moveTo(col * colWidth, row * rowHeight + rowHeight);\n                ctx.lineTo(col * colWidth + colWidth, row * rowHeight + rowHeight);\n                ctx.stroke();\n            }\n        }\n    }\n    ctx.resetTransform();\n    ctx.translate(0.5, 0);\n    for (let col = 0; col < 5; col++) {\n        for (let row = 0; row < 5; row++) {\n            const borders = tableStyle.borders[col][row];\n            if (borders.left) {\n                ctx.strokeStyle = borders.left.color;\n                ctx.beginPath();\n                ctx.moveTo(col * colWidth, row * rowHeight);\n                ctx.lineTo(col * colWidth, row * rowHeight + rowHeight);\n                ctx.stroke();\n            }\n            if (borders.right) {\n                ctx.strokeStyle = borders.right.color;\n                ctx.beginPath();\n                ctx.moveTo(col * colWidth + colWidth, row * rowHeight);\n                ctx.lineTo(col * colWidth + colWidth, row * rowHeight + rowHeight + 1); // +1 to draw on the bottom-right pixel of the table\n                ctx.stroke();\n            }\n        }\n    }\n    ctx.restore();\n}\nfunction drawTexts(ctx, tableStyle, colWidth, rowHeight) {\n    ctx.save();\n    ctx.translate(0, 0.5);\n    ctx.lineWidth = 1;\n    const xPadding = Math.floor(colWidth / 4);\n    const yPadding = Math.floor(rowHeight / 2);\n    for (let col = 0; col < 5; col++) {\n        for (let row = 0; row < 5; row++) {\n            ctx.strokeStyle = tableStyle.styles[col][row].textColor || \"#000\";\n            ctx.beginPath();\n            ctx.moveTo(col * colWidth + xPadding + 1, row * rowHeight + yPadding);\n            ctx.lineTo(col * colWidth + colWidth - xPadding, row * rowHeight + yPadding);\n            ctx.stroke();\n        }\n    }\n    ctx.restore();\n}\n\ncss /* scss */ `\n  .o-table-style-list-item {\n    border: 1px solid transparent;\n    border-radius: 4px;\n    &.selected {\n      border: 1px solid ${ACTION_COLOR};\n      background: ${BADGE_SELECTED_COLOR};\n    }\n\n    &:hover {\n      background: #ddd;\n      .o-table-style-edit-button {\n        display: block !important;\n        right: 0;\n        top: 0;\n        background: #fff;\n        cursor: pointer;\n        border: 1px solid #ddd;\n        padding: 1px 1px 1px 2px;\n        .o-icon {\n          font-size: 12px;\n          width: 12px;\n          height: 12px;\n        }\n      }\n    }\n  }\n`;\nclass TableStylePreview extends Component {\n    static template = \"o-spreadsheet-TableStylePreview\";\n    static components = { Menu };\n    static props = {\n        tableConfig: Object,\n        tableStyle: Object,\n        class: String,\n        styleId: { type: String, optional: true },\n        selected: { type: Boolean, optional: true },\n        onClick: { type: Function, optional: true },\n    };\n    canvasRef = useRef(\"canvas\");\n    menu = useState({ isOpen: false, position: null, menuItems: [] });\n    setup() {\n        onWillUpdateProps((nextProps) => {\n            if (!deepEquals(this.props.tableConfig, nextProps.tableConfig) ||\n                !deepEquals(this.props.tableStyle, nextProps.tableStyle)) {\n                this.drawTable(nextProps);\n            }\n        });\n        onMounted(() => this.drawTable(this.props));\n    }\n    drawTable(props) {\n        const ctx = this.canvasRef.el.getContext(\"2d\");\n        const { width, height } = this.canvasRef.el.getBoundingClientRect();\n        this.canvasRef.el.width = width;\n        this.canvasRef.el.height = height;\n        const computedStyle = getComputedTableStyle(props.tableConfig, props.tableStyle, 5, 5);\n        drawPreviewTable(ctx, computedStyle, (width - 1) / 5, (height - 1) / 5);\n    }\n    onContextMenu(event) {\n        if (!this.props.styleId) {\n            return;\n        }\n        this.menu.menuItems = createTableStyleContextMenuActions(this.env, this.props.styleId);\n        this.menu.isOpen = true;\n        this.menu.position = { x: event.clientX, y: event.clientY };\n    }\n    closeMenu() {\n        this.menu.isOpen = false;\n        this.menu.position = null;\n        this.menu.menuItems = [];\n    }\n    get styleName() {\n        if (!this.props.styleId) {\n            return \"\";\n        }\n        return this.env.model.getters.getTableStyle(this.props.styleId).displayName;\n    }\n    get isStyleEditable() {\n        if (!this.props.styleId) {\n            return false;\n        }\n        return this.env.model.getters.isTableStyleEditable(this.props.styleId);\n    }\n    editTableStyle() {\n        this.env.openSidePanel(\"TableStyleEditorPanel\", { styleId: this.props.styleId });\n    }\n}\n\ncss /* scss */ `\n  .o-table-style-popover {\n    /** 7 tables preview + padding by line */\n    width: calc((66px + 4px * 2) * 7 + 1.5rem * 2);\n    background: #fff;\n    font-size: 14px;\n    user-select: none;\n\n    .o-notebook {\n      border-bottom: 1px solid ${GRAY_300};\n\n      .o-notebook-tab {\n        padding: 5px 15px;\n        border: 1px solid ${GRAY_300};\n        margin-bottom: -1px;\n        margin-left: -1px;\n        color: ${TEXT_BODY};\n        cursor: pointer;\n        transition: color 0.2s, border-color 0.2s;\n\n        &.selected {\n          border-bottom-color: #fff;\n          border-top-color: ${PRIMARY_BUTTON_BG};\n          color: ${TEXT_HEADING};\n        }\n      }\n    }\n\n    .o-table-style-list-item {\n      padding: 3px;\n    }\n\n    .o-table-style-popover-preview {\n      width: 66px;\n      height: 51px;\n    }\n\n    .o-new-table-style {\n      font-size: 36px;\n      color: #666;\n      &:hover {\n        background: #f5f5f5;\n      }\n    }\n  }\n`;\nclass TableStylesPopover extends Component {\n    static template = \"o-spreadsheet-TableStylesPopover\";\n    static components = { Popover, TableStylePreview };\n    static props = {\n        tableConfig: Object,\n        popoverProps: { type: Object, optional: true },\n        closePopover: Function,\n        onStylePicked: Function,\n        selectedStyleId: { type: String, optional: true },\n    };\n    categories = TABLE_STYLE_CATEGORIES;\n    tableStyleListRef = useRef(\"tableStyleList\");\n    state = useState({ selectedCategory: this.initialSelectedCategory });\n    menu = useState({ isOpen: false, position: null, menuItems: [] });\n    setup() {\n        useExternalListener(window, \"click\", this.onExternalClick, { capture: true });\n    }\n    onExternalClick(ev) {\n        if (this.tableStyleListRef.el && !isChildEvent(this.tableStyleListRef.el, ev)) {\n            this.props.closePopover();\n            ev.hasClosedTableStylesPopover = true;\n        }\n    }\n    get displayedStyles() {\n        const styles = this.env.model.getters.getTableStyles();\n        return Object.keys(styles).filter((styleId) => styles[styleId].category === this.state.selectedCategory);\n    }\n    get initialSelectedCategory() {\n        return this.props.selectedStyleId\n            ? this.env.model.getters.getTableStyle(this.props.selectedStyleId).category\n            : \"medium\";\n    }\n    newTableStyle() {\n        this.props.closePopover();\n        this.env.openSidePanel(\"TableStyleEditorPanel\", {\n            onStylePicked: this.props.onStylePicked,\n        });\n    }\n}\n\ncss /* scss */ `\n  .o-table-style-picker {\n    box-sizing: border-box;\n    border: 1px solid ${GRAY_300};\n    border-radius: 3px;\n\n    .o-table-style-picker-arrow {\n      border-left: 1px solid ${GRAY_300};\n\n      &:hover {\n        background: #f5f5f5;\n        cursor: pointer;\n      }\n    }\n\n    .o-table-style-list-item {\n      padding: 5px 6px;\n      margin: 5px 2px;\n\n      .o-table-style-picker-preview {\n        width: 51px;\n        height: 36px;\n      }\n    }\n  }\n`;\nclass TableStylePicker extends Component {\n    static template = \"o-spreadsheet-TableStylePicker\";\n    static components = { TableStylesPopover, TableStylePreview };\n    static props = { table: Object };\n    state = useState({ popoverProps: undefined });\n    getDisplayedTableStyles() {\n        const allStyles = this.env.model.getters.getTableStyles();\n        const selectedStyleCategory = allStyles[this.props.table.config.styleId].category;\n        const styles = Object.keys(allStyles).filter((key) => allStyles[key].category === selectedStyleCategory);\n        const selectedStyleIndex = styles.indexOf(this.props.table.config.styleId);\n        if (selectedStyleIndex === -1) {\n            return selectedStyleIndex;\n        }\n        const index = Math.floor(selectedStyleIndex / 4) * 4;\n        return styles.slice(index);\n    }\n    onStylePicked(styleId) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        this.env.model.dispatch(\"UPDATE_TABLE\", {\n            sheetId,\n            zone: this.props.table.range.zone,\n            config: { styleId: styleId },\n        });\n        this.closePopover();\n    }\n    onArrowButtonClick(ev) {\n        if (ev.hasClosedTableStylesPopover || this.state.popoverProps) {\n            this.closePopover();\n            return;\n        }\n        const target = ev.currentTarget;\n        const { bottom, right } = target.getBoundingClientRect();\n        this.state.popoverProps = {\n            anchorRect: { x: right, y: bottom, width: 0, height: 0 },\n            positioning: \"TopRight\",\n            verticalOffset: 0,\n        };\n    }\n    closePopover() {\n        this.state.popoverProps = undefined;\n    }\n}\n\ncss /* scss */ `\n  .o-table-panel {\n    input.o-table-n-of-headers {\n      width: 14px;\n      text-align: center;\n    }\n\n    .o-info-icon {\n      width: 14px;\n      height: 14px;\n    }\n  }\n`;\nclass TablePanel extends Component {\n    static template = \"o-spreadsheet-TablePanel\";\n    static components = { TableStylePicker, SelectionInput, ValidationMessages, Checkbox, Section };\n    static props = { onCloseSidePanel: Function, table: Object };\n    state;\n    setup() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        this.state = useState({\n            tableZoneErrors: [],\n            tableXc: this.env.model.getters.getRangeString(this.props.table.range, sheetId),\n            filtersEnabledIfPossible: this.props.table.config.hasFilters,\n        });\n    }\n    updateHasFilters(hasFilters) {\n        this.state.filtersEnabledIfPossible = hasFilters;\n        this.updateTableConfig(\"hasFilters\", hasFilters);\n    }\n    updateTableConfig(attName, value) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.env.model.dispatch(\"UPDATE_TABLE\", {\n            sheetId,\n            zone: this.props.table.range.zone,\n            config: { [attName]: value },\n        });\n    }\n    updateHasHeaders(hasHeaders) {\n        const numberOfHeaders = hasHeaders ? 1 : 0;\n        this.updateNumberOfHeaders(numberOfHeaders);\n    }\n    updateTableIsDynamic(isDynamic) {\n        const newTableType = isDynamic ? \"dynamic\" : \"forceStatic\";\n        if (newTableType === this.props.table.type) {\n            return;\n        }\n        const uiTable = this.env.model.getters.getTable(getTableTopLeft(this.props.table));\n        if (!uiTable) {\n            return;\n        }\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const result = this.env.model.dispatch(\"UPDATE_TABLE\", {\n            sheetId,\n            zone: this.props.table.range.zone,\n            newTableRange: uiTable.range.rangeData,\n            tableType: newTableType,\n        });\n        const updatedTable = this.env.model.getters.getCoreTable(getTableTopLeft(this.props.table));\n        if (result.isSuccessful && updatedTable) {\n            const newTableRange = updatedTable.range;\n            this.state.tableXc = this.env.model.getters.getRangeString(newTableRange, sheetId);\n            this.state.tableZoneErrors = [];\n        }\n    }\n    onChangeNumberOfHeaders(ev) {\n        const input = ev.target;\n        const numberOfHeaders = parseInt(input.value);\n        const result = this.updateNumberOfHeaders(numberOfHeaders);\n        if (!result.isSuccessful) {\n            input.value = this.props.table.config.numberOfHeaders.toString();\n        }\n    }\n    updateNumberOfHeaders(numberOfHeaders) {\n        const hasFilters = numberOfHeaders > 0 && (this.tableConfig.hasFilters || this.state.filtersEnabledIfPossible);\n        return this.env.model.dispatch(\"UPDATE_TABLE\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            zone: this.props.table.range.zone,\n            config: { numberOfHeaders, hasFilters },\n        });\n    }\n    onRangeChanged(ranges) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        this.state.tableXc = ranges[0];\n        const newTableRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);\n        this.state.tableZoneErrors = this.env.model.canDispatch(\"UPDATE_TABLE\", {\n            sheetId,\n            zone: this.props.table.range.zone,\n            newTableRange: this.env.model.getters.getRangeDataFromXc(sheetId, this.state.tableXc),\n            tableType: this.getNewTableType(newTableRange.zone),\n        }).reasons;\n    }\n    onRangeConfirmed() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        let newRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);\n        if (getZoneArea(newRange.zone) === 1) {\n            const extendedZone = this.env.model.getters.getContiguousZone(sheetId, newRange.zone);\n            newRange = this.env.model.getters.getRangeFromZone(sheetId, extendedZone);\n        }\n        const newTableZone = newRange.zone;\n        const oldTableZone = this.props.table.range.zone;\n        const cmdToCall = newTableZone.top === oldTableZone.top && newTableZone.left === oldTableZone.left\n            ? \"RESIZE_TABLE\"\n            : \"UPDATE_TABLE\";\n        const result = this.env.model.dispatch(cmdToCall, {\n            sheetId,\n            zone: this.props.table.range.zone,\n            newTableRange: newRange.rangeData,\n            tableType: this.getNewTableType(newRange.zone),\n        });\n        const position = { sheetId, col: newRange.zone.left, row: newRange.zone.top };\n        const updatedTable = this.env.model.getters.getCoreTable(position);\n        if (result.isSuccessful && updatedTable) {\n            const newTopLeft = getTableTopLeft(updatedTable);\n            this.env.model.selection.selectZone({\n                zone: positionToZone(newTopLeft),\n                cell: newTopLeft,\n            });\n            const newTableRange = updatedTable.range;\n            this.state.tableXc = this.env.model.getters.getRangeString(newTableRange, sheetId);\n        }\n    }\n    deleteTable() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        this.env.model.dispatch(\"REMOVE_TABLE\", {\n            sheetId,\n            target: [this.props.table.range.zone],\n        });\n    }\n    getNewTableType(newTableZone) {\n        if (this.props.table.type === \"forceStatic\") {\n            return \"forceStatic\";\n        }\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.env.model.getters.canCreateDynamicTableOnZones(sheetId, [newTableZone])\n            ? \"dynamic\"\n            : \"static\";\n    }\n    get tableConfig() {\n        return this.props.table.config;\n    }\n    get errorMessages() {\n        const cancelledReasons = this.state.tableZoneErrors || [];\n        return cancelledReasons.map((error) => TableTerms.Errors[error] || TableTerms.Errors.Unexpected);\n    }\n    getCheckboxLabel(attName) {\n        return TableTerms.Checkboxes[attName];\n    }\n    get canHaveFilters() {\n        return this.tableConfig.numberOfHeaders > 0;\n    }\n    get hasFilterCheckboxTooltip() {\n        return this.canHaveFilters ? undefined : TableTerms.Tooltips.filterWithoutHeader;\n    }\n    get canBeDynamic() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return (this.props.table.type === \"dynamic\" ||\n            this.env.model.getters.canCreateDynamicTableOnZones(sheetId, [this.props.table.range.zone]));\n    }\n    get dynamicTableTooltip() {\n        return TableTerms.Tooltips.isDynamic;\n    }\n}\n\ncss /* scss */ `\n  .o-table-style-editor-panel {\n    .o-table-style-list-item {\n      margin: 2px 7px;\n      padding: 6px 9px;\n\n      .o-table-style-edit-template-preview {\n        width: 71px;\n        height: 51px;\n      }\n    }\n  }\n`;\nconst DEFAULT_TABLE_STYLE_COLOR = \"#3C78D8\";\nclass TableStyleEditorPanel extends Component {\n    static template = \"o-spreadsheet-TableStyleEditorPanel\";\n    static components = { Section, RoundColorPicker, TableStylePreview };\n    static props = {\n        onCloseSidePanel: Function,\n        onStylePicked: { type: Function, optional: true },\n        styleId: { type: String, optional: true },\n    };\n    state = useState(this.getInitialState());\n    setup() {\n        useExternalListener(window, \"click\", () => (this.state.pickerOpened = false));\n    }\n    getInitialState() {\n        const editedStyle = this.props.styleId\n            ? this.env.model.getters.getTableStyle(this.props.styleId)\n            : null;\n        return {\n            pickerOpened: false,\n            primaryColor: editedStyle?.primaryColor || DEFAULT_TABLE_STYLE_COLOR,\n            selectedTemplateName: editedStyle?.templateName || \"lightColoredText\",\n            styleName: editedStyle?.displayName || this.env.model.getters.getNewCustomTableStyleName(),\n        };\n    }\n    togglePicker() {\n        this.state.pickerOpened = !this.state.pickerOpened;\n    }\n    onColorPicked(color) {\n        this.state.primaryColor = isColorValid(color) ? color : DEFAULT_TABLE_STYLE_COLOR;\n        this.state.pickerOpened = false;\n    }\n    onTemplatePicked(templateName) {\n        this.state.selectedTemplateName = templateName;\n    }\n    onConfirm() {\n        const tableStyleId = this.props.styleId || this.env.model.uuidGenerator.uuidv4();\n        this.env.model.dispatch(\"CREATE_TABLE_STYLE\", {\n            tableStyleId,\n            tableStyleName: this.state.styleName,\n            templateName: this.state.selectedTemplateName,\n            primaryColor: this.state.primaryColor,\n        });\n        this.props.onStylePicked?.(tableStyleId);\n        this.props.onCloseSidePanel();\n    }\n    onCancel() {\n        this.props.onCloseSidePanel();\n    }\n    onDelete() {\n        if (!this.props.styleId) {\n            return;\n        }\n        this.env.model.dispatch(\"REMOVE_TABLE_STYLE\", { tableStyleId: this.props.styleId });\n        this.props.onCloseSidePanel();\n    }\n    get colorPreviewStyle() {\n        return cssPropertiesToCss({ background: this.state.primaryColor });\n    }\n    get tableTemplates() {\n        return Object.keys(TABLE_STYLES_TEMPLATES).filter((templateName) => templateName !== \"none\");\n    }\n    get previewTableConfig() {\n        return {\n            bandedColumns: false,\n            bandedRows: true,\n            firstColumn: false,\n            lastColumn: false,\n            numberOfHeaders: 1,\n            totalRow: true,\n            hasFilters: true,\n            styleId: \"\",\n        };\n    }\n    get selectedStyle() {\n        return this.computeTableStyle(this.state.selectedTemplateName);\n    }\n    computeTableStyle(templateName) {\n        return buildTableStyle(this.state.styleName, templateName, this.state.primaryColor);\n    }\n}\n\nconst sidePanelRegistry = new Registry();\n\n//------------------------------------------------------------------------------\n// Side Panel Registry\n//------------------------------------------------------------------------------\nsidePanelRegistry.add(\"ConditionalFormatting\", {\n    title: _t(\"Conditional formatting\"),\n    Body: ConditionalFormattingPanel,\n});\nsidePanelRegistry.add(\"ChartPanel\", {\n    title: _t(\"Chart\"),\n    Body: ChartPanel,\n    computeState: (getters, initialProps) => {\n        const figureId = getters.getSelectedFigureId() ?? initialProps.figureId;\n        if (!getters.isChartDefined(figureId)) {\n            return { isOpen: false };\n        }\n        return { isOpen: true, props: { figureId } };\n    },\n});\nsidePanelRegistry.add(\"FindAndReplace\", {\n    title: _t(\"Find and Replace\"),\n    Body: FindAndReplacePanel,\n});\nsidePanelRegistry.add(\"CustomCurrency\", {\n    title: _t(\"Custom currency format\"),\n    Body: CustomCurrencyPanel,\n});\nsidePanelRegistry.add(\"SplitToColumns\", {\n    title: _t(\"Split text into columns\"),\n    Body: SplitIntoColumnsPanel,\n});\nsidePanelRegistry.add(\"Settings\", {\n    title: _t(\"Spreadsheet settings\"),\n    Body: SettingsPanel,\n});\nsidePanelRegistry.add(\"RemoveDuplicates\", {\n    title: _t(\"Remove duplicates\"),\n    Body: RemoveDuplicatesPanel,\n});\nsidePanelRegistry.add(\"DataValidation\", {\n    title: _t(\"Data validation\"),\n    Body: DataValidationPanel,\n});\nsidePanelRegistry.add(\"DataValidationEditor\", {\n    title: _t(\"Data validation\"),\n    Body: DataValidationEditor,\n});\nsidePanelRegistry.add(\"MoreFormats\", {\n    title: _t(\"More date formats\"),\n    Body: MoreFormatsPanel,\n});\nsidePanelRegistry.add(\"TableSidePanel\", {\n    title: _t(\"Edit table\"),\n    Body: TablePanel,\n    computeState: (getters) => {\n        const table = getters.getFirstTableInSelection();\n        if (!table) {\n            return { isOpen: false };\n        }\n        const coreTable = getters.getCoreTable(getTableTopLeft(table));\n        return { isOpen: true, props: { table: coreTable }, key: table.id };\n    },\n});\nsidePanelRegistry.add(\"TableStyleEditorPanel\", {\n    title: _t(\"Create custom table style\"),\n    Body: TableStyleEditorPanel,\n    computeState: (getters, initialProps) => {\n        return {\n            isOpen: true,\n            props: { ...initialProps },\n            key: initialProps.styleId ?? \"new\",\n        };\n    },\n});\nsidePanelRegistry.add(\"PivotSidePanel\", {\n    title: (env, props) => {\n        return _t(\"Pivot #%s\", env.model.getters.getPivotFormulaId(props.pivotId));\n    },\n    Body: PivotSidePanel,\n    computeState: (getters, props) => {\n        return {\n            isOpen: getters.isExistingPivot(props.pivotId),\n            props,\n            key: `pivot_key_${props.pivotId}`,\n        };\n    },\n});\nsidePanelRegistry.add(\"PivotMeasureDisplayPanel\", {\n    title: (env, props) => {\n        const measure = env.model.getters.getPivot(props.pivotId).getMeasure(props.measure.id);\n        return _t('Measure \"%s\" options', measure.displayName);\n    },\n    Body: PivotMeasureDisplayPanel,\n    computeState: (getters, props) => {\n        try {\n            // This will throw if the pivot or measure does not exist\n            getters.getPivot(props.pivotId).getMeasure(props.measure.id);\n            return { isOpen: true, props, key: \"pivot_measure_display\" };\n        }\n        catch (e) {\n            return { isOpen: false };\n        }\n    },\n});\n\nclass TopBarComponentRegistry extends Registry {\n    mapping = {};\n    uuidGenerator = new UuidGenerator();\n    add(name, value) {\n        const component = { ...value, id: this.uuidGenerator.uuidv4() };\n        return super.add(name, component);\n    }\n    getAllOrdered() {\n        return this.getAll().sort((a, b) => a.sequence - b.sequence);\n    }\n}\nconst topbarComponentRegistry = new TopBarComponentRegistry();\n\n// -----------------------------------------------------------------------------\n// STYLE\n// -----------------------------------------------------------------------------\nconst ANCHOR_SIZE = 8;\nconst BORDER_WIDTH = 1;\nconst ACTIVE_BORDER_WIDTH = 2;\ncss /*SCSS*/ `\n  div.o-figure {\n    box-sizing: border-box;\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    user-select: none;\n\n    &:focus {\n      outline: none;\n    }\n  }\n\n  div.o-figure-border {\n    box-sizing: border-box;\n    z-index: 1;\n  }\n\n  .o-figure-wrapper {\n    position: absolute;\n    box-sizing: content-box;\n\n    .o-fig-anchor {\n      z-index: ${ComponentsImportance.FigureAnchor};\n      position: absolute;\n      width: ${ANCHOR_SIZE}px;\n      height: ${ANCHOR_SIZE}px;\n      background-color: #1a73e8;\n      outline: ${BORDER_WIDTH}px solid white;\n\n      &.o-top {\n        cursor: n-resize;\n      }\n      &.o-topRight {\n        cursor: ne-resize;\n      }\n      &.o-right {\n        cursor: e-resize;\n      }\n      &.o-bottomRight {\n        cursor: se-resize;\n      }\n      &.o-bottom {\n        cursor: s-resize;\n      }\n      &.o-bottomLeft {\n        cursor: sw-resize;\n      }\n      &.o-left {\n        cursor: w-resize;\n      }\n      &.o-topLeft {\n        cursor: nw-resize;\n      }\n    }\n\n    .o-figure-menu {\n      right: 0px;\n      top: 0px;\n      display: none;\n    }\n\n    .o-figure-menu-item {\n      cursor: pointer;\n    }\n\n    .o-figure.active:focus,\n    .o-figure:hover {\n      .o-figure-menu {\n        display: flex;\n      }\n    }\n  }\n`;\nclass FigureComponent extends Component {\n    static template = \"o-spreadsheet-FigureComponent\";\n    static props = {\n        figure: Object,\n        style: { type: String, optional: true },\n        onFigureDeleted: { type: Function, optional: true },\n        onMouseDown: { type: Function, optional: true },\n        onClickAnchor: { type: Function, optional: true },\n    };\n    static components = { Menu };\n    static defaultProps = {\n        onFigureDeleted: () => { },\n        onMouseDown: () => { },\n        onClickAnchor: () => { },\n    };\n    menuState = useState({ isOpen: false, position: null, menuItems: [] });\n    figureRef = useRef(\"figure\");\n    menuButtonRef = useRef(\"menuButton\");\n    menuButtonRect = useAbsoluteBoundingRect(this.menuButtonRef);\n    borderWidth;\n    get isSelected() {\n        return this.env.model.getters.getSelectedFigureId() === this.props.figure.id;\n    }\n    get figureRegistry() {\n        return figureRegistry;\n    }\n    getBorderWidth() {\n        if (this.env.isDashboard())\n            return 0;\n        return this.isSelected ? ACTIVE_BORDER_WIDTH : this.borderWidth;\n    }\n    get borderStyle() {\n        const borderWidth = this.getBorderWidth();\n        const borderColor = this.isSelected ? SELECTION_BORDER_COLOR : FIGURE_BORDER_COLOR;\n        return `border: ${borderWidth}px solid ${borderColor};`;\n    }\n    get wrapperStyle() {\n        const { x, y, width, height } = this.props.figure;\n        return cssPropertiesToCss({\n            left: `${x}px`,\n            top: `${y}px`,\n            width: `${width}px`,\n            height: `${height}px`,\n            \"z-index\": String(ComponentsImportance.Figure + (this.isSelected ? 1 : 0)),\n        });\n    }\n    getResizerPosition(resizer) {\n        const anchorCenteringOffset = (ANCHOR_SIZE - ACTIVE_BORDER_WIDTH) / 2;\n        let style = {};\n        if (resizer.includes(\"top\")) {\n            style.top = `${-anchorCenteringOffset}px`;\n        }\n        else if (resizer.includes(\"bottom\")) {\n            style.bottom = `${-anchorCenteringOffset}px`;\n        }\n        else {\n            style.bottom = `calc(50% - ${anchorCenteringOffset}px)`;\n        }\n        if (resizer.includes(\"left\")) {\n            style.left = `${-anchorCenteringOffset}px`;\n        }\n        else if (resizer.includes(\"right\")) {\n            style.right = `${-anchorCenteringOffset}px`;\n        }\n        else {\n            style.right = `calc(50% - ${anchorCenteringOffset}px)`;\n        }\n        return cssPropertiesToCss(style);\n    }\n    setup() {\n        const borderWidth = figureRegistry.get(this.props.figure.tag).borderWidth;\n        this.borderWidth = borderWidth !== undefined ? borderWidth : BORDER_WIDTH;\n        useEffect((selectedFigureId, thisFigureId, el) => {\n            if (selectedFigureId === thisFigureId) {\n                /** Scrolling on a newly inserted figure that overflows outside the viewport\n                 * will break the whole layout.\n                 * NOTE: `preventScroll`does not work on mobile but then again,\n                 * mobile is not really supported ATM.\n                 *\n                 * TODO: When implementing proper mobile, we will need to scroll the viewport\n                 * correctly (and render?) before focusing the element.\n                 */\n                el?.focus({ preventScroll: true });\n            }\n        }, () => [this.env.model.getters.getSelectedFigureId(), this.props.figure.id, this.figureRef.el]);\n        onWillUnmount(() => {\n            this.props.onFigureDeleted();\n        });\n    }\n    clickAnchor(dirX, dirY, ev) {\n        this.props.onClickAnchor(dirX, dirY, ev);\n    }\n    onMouseDown(ev) {\n        this.props.onMouseDown(ev);\n    }\n    onKeyDown(ev) {\n        const figure = this.props.figure;\n        const keyDownShortcut = keyboardEventToShortcutString(ev);\n        switch (keyDownShortcut) {\n            case \"Delete\":\n            case \"Backspace\":\n                this.env.model.dispatch(\"DELETE_FIGURE\", {\n                    sheetId: this.env.model.getters.getActiveSheetId(),\n                    id: figure.id,\n                });\n                this.props.onFigureDeleted();\n                ev.preventDefault();\n                ev.stopPropagation();\n                break;\n            case \"ArrowDown\":\n            case \"ArrowLeft\":\n            case \"ArrowRight\":\n            case \"ArrowUp\":\n                const deltaMap = {\n                    ArrowDown: [0, 1],\n                    ArrowLeft: [-1, 0],\n                    ArrowRight: [1, 0],\n                    ArrowUp: [0, -1],\n                };\n                const delta = deltaMap[ev.key];\n                this.env.model.dispatch(\"UPDATE_FIGURE\", {\n                    sheetId: this.env.model.getters.getActiveSheetId(),\n                    id: figure.id,\n                    x: figure.x + delta[0],\n                    y: figure.y + delta[1],\n                });\n                ev.preventDefault();\n                ev.stopPropagation();\n                break;\n            case \"Ctrl+A\":\n                // Maybe in the future we will implement a way to select all figures\n                ev.preventDefault();\n                ev.stopPropagation();\n                break;\n            case \"Ctrl+Y\":\n            case \"Ctrl+Z\":\n                if (keyDownShortcut === \"Ctrl+Y\") {\n                    this.env.model.dispatch(\"REQUEST_REDO\");\n                }\n                else if (keyDownShortcut === \"Ctrl+Z\") {\n                    this.env.model.dispatch(\"REQUEST_UNDO\");\n                }\n                ev.preventDefault();\n                ev.stopPropagation();\n                break;\n        }\n    }\n    onContextMenu(ev) {\n        if (this.env.isDashboard())\n            return;\n        const position = {\n            x: ev.clientX,\n            y: ev.clientY,\n        };\n        this.openContextMenu(position);\n    }\n    showMenu() {\n        const { x, y, width } = this.menuButtonRect;\n        const menuPosition = {\n            x: x >= MENU_WIDTH ? x - MENU_WIDTH : x + width,\n            y: y,\n        };\n        this.openContextMenu(menuPosition);\n    }\n    openContextMenu(position) {\n        this.menuState.isOpen = true;\n        this.menuState.position = position;\n        this.menuState.menuItems = figureRegistry\n            .get(this.props.figure.tag)\n            .menuBuilder(this.props.figure.id, this.props.onFigureDeleted, this.env);\n    }\n}\n\nconst ToggleGroupInteractiveContent = {\n    CannotHideAllRows: _t(\"Cannot hide all the rows of a sheet.\"),\n    CannotHideAllColumns: _t(\"Cannot hide all the columns of a sheet.\"),\n};\nfunction interactiveToggleGroup(env, sheetId, dimension, start, end) {\n    const group = env.model.getters.getHeaderGroup(sheetId, dimension, start, end);\n    if (!group) {\n        return;\n    }\n    const command = group.isFolded ? \"UNFOLD_HEADER_GROUP\" : \"FOLD_HEADER_GROUP\";\n    const result = env.model.dispatch(command, {\n        sheetId,\n        dimension,\n        start: group.start,\n        end: group.end,\n    });\n    if (!result.isSuccessful) {\n        if (result.isCancelledBecause(\"NotEnoughElements\" /* CommandResult.NotEnoughElements */)) {\n            const errorMessage = dimension === \"ROW\"\n                ? ToggleGroupInteractiveContent.CannotHideAllRows\n                : ToggleGroupInteractiveContent.CannotHideAllColumns;\n            env.raiseError(errorMessage);\n        }\n    }\n}\n\nfunction createHeaderGroupContainerContextMenu(sheetId, dimension) {\n    return createActions([\n        {\n            id: \"unfold_all\",\n            name: dimension === \"ROW\" ? _t(\"Expand all row groups\") : _t(\"Expand all column groups\"),\n            execute: (env) => {\n                env.model.dispatch(\"UNFOLD_ALL_HEADER_GROUPS\", { sheetId, dimension });\n            },\n        },\n        {\n            id: \"fold_all\",\n            name: dimension === \"ROW\" ? _t(\"Collapse all row groups\") : _t(\"Collapse all column groups\"),\n            execute: (env) => {\n                env.model.dispatch(\"FOLD_ALL_HEADER_GROUPS\", { sheetId, dimension });\n            },\n        },\n    ]);\n}\nfunction getHeaderGroupContextMenu(sheetId, dimension, start, end) {\n    const groupActions = createActions([\n        {\n            id: \"toggle_group\",\n            name: (env) => {\n                const sheetId = env.model.getters.getActiveSheetId();\n                const groupIsFolded = env.model.getters.isGroupFolded(sheetId, dimension, start, end);\n                if (groupIsFolded) {\n                    return dimension === \"ROW\" ? _t(\"Expand row group\") : _t(\"Expand column group\");\n                }\n                else {\n                    return dimension === \"ROW\" ? _t(\"Collapse row group\") : _t(\"Collapse column group\");\n                }\n            },\n            execute: (env) => {\n                const sheetId = env.model.getters.getActiveSheetId();\n                interactiveToggleGroup(env, sheetId, dimension, start, end);\n            },\n        },\n        {\n            id: \"remove_group\",\n            name: dimension === \"ROW\" ? _t(\"Remove row group\") : _t(\"Remove column group\"),\n            execute: (env) => {\n                const sheetId = env.model.getters.getActiveSheetId();\n                env.model.dispatch(\"UNGROUP_HEADERS\", { sheetId, dimension, start, end });\n            },\n            separator: true,\n        },\n    ]);\n    return [...groupActions, ...createHeaderGroupContainerContextMenu(sheetId, dimension)];\n}\nconst groupHeadersMenuRegistry = new MenuItemRegistry();\ngroupHeadersMenuRegistry\n    .add(\"group_columns\", {\n    sequence: 10,\n    ...groupColumns,\n    isVisible: () => true,\n    isEnabled: groupColumns.isVisible,\n})\n    .add(\"group_rows\", {\n    sequence: 20,\n    ...groupRows,\n    isVisible: () => true,\n    isEnabled: groupRows.isVisible,\n});\nconst unGroupHeadersMenuRegistry = new MenuItemRegistry();\nunGroupHeadersMenuRegistry\n    .add(\"ungroup_columns\", {\n    sequence: 10,\n    ...ungroupColumns,\n    isEnabled: (env) => canUngroupHeaders(env, \"COL\"),\n})\n    .add(\"ungroup_rows\", {\n    sequence: 20,\n    ...ungroupRows,\n    isEnabled: (env) => canUngroupHeaders(env, \"ROW\"),\n});\n\nclass ArrayFormulaHighlight extends SpreadsheetStore {\n    highlightStore = this.get(HighlightStore);\n    constructor(get) {\n        super(get);\n        this.highlightStore.register(this);\n    }\n    get highlights() {\n        let zone;\n        const position = this.model.getters.getActivePosition();\n        const cell = this.getters.getEvaluatedCell(position);\n        const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);\n        zone = spreader\n            ? this.model.getters.getSpreadZone(spreader, { ignoreSpillError: true })\n            : this.model.getters.getSpreadZone(position, { ignoreSpillError: true });\n        if (!zone) {\n            return [];\n        }\n        return [\n            {\n                sheetId: position.sheetId,\n                zone,\n                dashed: cell.value === CellErrorType.SpilledBlocked,\n                color: \"#17A2B8\",\n                noFill: true,\n                thinLine: true,\n            },\n        ];\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Autofill\n// -----------------------------------------------------------------------------\ncss /* scss */ `\n  .o-autofill {\n    position: absolute;\n    height: ${AUTOFILL_EDGE_LENGTH}px;\n    width: ${AUTOFILL_EDGE_LENGTH}px;\n    border: 1px solid white;\n    box-sizing: border-box !important;\n    background-color: #1a73e8;\n  }\n\n  .o-autofill-handler {\n    position: absolute;\n    height: ${AUTOFILL_EDGE_LENGTH}px;\n    width: ${AUTOFILL_EDGE_LENGTH}px;\n    &:hover {\n      cursor: crosshair;\n    }\n  }\n\n  .o-autofill-nextvalue {\n    position: absolute;\n    background-color: #ffffff;\n    border: 1px solid black;\n    padding: 5px;\n    font-size: 12px;\n    pointer-events: none;\n    white-space: nowrap;\n  }\n`;\nclass Autofill extends Component {\n    static template = \"o-spreadsheet-Autofill\";\n    static props = {\n        position: Object,\n        isVisible: Boolean,\n    };\n    state = useState({\n        position: { left: 0, top: 0 },\n        handler: false,\n    });\n    get style() {\n        const { left, top } = this.props.position;\n        return cssPropertiesToCss({\n            top: `${top}px`,\n            left: `${left}px`,\n            visibility: this.props.isVisible ? \"visible\" : \"hidden\",\n        });\n    }\n    get handlerStyle() {\n        const { left, top } = this.state.handler ? this.state.position : this.props.position;\n        return cssPropertiesToCss({\n            top: `${top}px`,\n            left: `${left}px`,\n        });\n    }\n    get styleNextValue() {\n        const { left, top } = this.state.position;\n        return cssPropertiesToCss({\n            top: `${top + 5}px`,\n            left: `${left + 15}px`,\n        });\n    }\n    getTooltip() {\n        const tooltip = this.env.model.getters.getAutofillTooltip();\n        if (tooltip && !tooltip.component) {\n            tooltip.component = TooltipComponent;\n        }\n        return tooltip;\n    }\n    onMouseDown(ev) {\n        this.state.handler = true;\n        let lastCol;\n        let lastRow;\n        const start = {\n            left: ev.clientX - this.props.position.left,\n            top: ev.clientY - this.props.position.top,\n        };\n        const onMouseUp = () => {\n            this.state.handler = false;\n            this.state.position = { ...this.props.position };\n            this.env.model.dispatch(\"AUTOFILL\");\n        };\n        const onMouseMove = (col, row, ev) => {\n            this.state.position = {\n                left: ev.clientX - start.left,\n                top: ev.clientY - start.top,\n            };\n            if (lastCol !== col || lastRow !== row) {\n                const activeSheetId = this.env.model.getters.getActiveSheetId();\n                const numberOfCols = this.env.model.getters.getNumberCols(activeSheetId);\n                const numberOfRows = this.env.model.getters.getNumberRows(activeSheetId);\n                lastCol = col === -1 ? lastCol : clip(col, 0, numberOfCols);\n                lastRow = row === -1 ? lastRow : clip(row, 0, numberOfRows);\n                if (lastCol !== undefined && lastRow !== undefined) {\n                    this.env.model.dispatch(\"AUTOFILL_SELECT\", { col: lastCol, row: lastRow });\n                }\n            }\n        };\n        dragAndDropBeyondTheViewport(this.env, onMouseMove, onMouseUp);\n    }\n    onDblClick() {\n        this.env.model.dispatch(\"AUTOFILL_AUTO\");\n    }\n}\nclass TooltipComponent extends Component {\n    static props = {\n        content: String,\n    };\n    static template = xml /* xml */ `\n    <div t-esc=\"props.content\"/>\n  `;\n}\n\ncss /* scss */ `\n  .o-client-tag {\n    position: absolute;\n    border-top-left-radius: 4px;\n    border-top-right-radius: 4px;\n    font-size: ${DEFAULT_FONT_SIZE};\n    color: white;\n    pointer-events: none;\n  }\n`;\nclass ClientTag extends Component {\n    static template = \"o-spreadsheet-ClientTag\";\n    static props = {\n        active: Boolean,\n        name: String,\n        color: String,\n        col: Number,\n        row: Number,\n    };\n    get tagStyle() {\n        const { col, row, color } = this.props;\n        const { height } = this.env.model.getters.getSheetViewDimensionWithHeaders();\n        const { x, y } = this.env.model.getters.getVisibleRect({\n            left: col,\n            top: row,\n            right: col,\n            bottom: row,\n        });\n        return cssPropertiesToCss({\n            bottom: `${height - y + 15}px`,\n            left: `${x - 1}px`,\n            border: `1px solid ${color}`,\n            \"background-color\": color,\n        });\n    }\n}\n\nconst CELL_DELETED_MESSAGE = _t(\"The cell you are trying to edit has been deleted.\");\nclass CellComposerStore extends AbstractComposerStore {\n    canStopEdition() {\n        if (this.editionMode === \"inactive\") {\n            return true;\n        }\n        return this.checkDataValidation();\n    }\n    stopEdition(direction) {\n        const canStopEdition = this.canStopEdition();\n        if (canStopEdition) {\n            this._stopEdition();\n            if (direction) {\n                this.model.selection.moveAnchorCell(direction, 1);\n            }\n            return;\n        }\n        const editedCell = this.currentEditedCell;\n        const cellXc = toXC(editedCell.col, editedCell.row);\n        const rule = this.getters.getValidationRuleForCell(editedCell);\n        if (!rule) {\n            return;\n        }\n        const evaluator = dataValidationEvaluatorRegistry.get(rule.criterion.type);\n        const errorStr = evaluator.getErrorString(rule.criterion, this.getters, editedCell.sheetId);\n        this.notificationStore.raiseError(_t(\"The data you entered in %s violates the data validation rule set on the cell:\\n%s\", cellXc, errorStr));\n        this.cancelEdition();\n    }\n    handle(cmd) {\n        super.handle(cmd);\n        switch (cmd.type) {\n            case \"SET_FORMATTING\":\n                this.cancelEdition();\n                break;\n            case \"ADD_COLUMNS_ROWS\":\n                this.onAddElements(cmd);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\":\n                if (cmd.dimension === \"COL\") {\n                    this.onColumnsRemoved(cmd);\n                }\n                else {\n                    this.onRowsRemoved(cmd);\n                }\n                break;\n            case \"ACTIVATE_SHEET\":\n                if (!this._currentContent.startsWith(\"=\")) {\n                    this._cancelEdition();\n                    this.resetContent();\n                }\n                if (cmd.sheetIdFrom !== cmd.sheetIdTo) {\n                    const activePosition = this.getters.getActivePosition();\n                    const { col, row } = this.getters.getNextVisibleCellPosition({\n                        sheetId: cmd.sheetIdTo,\n                        col: activePosition.col,\n                        row: activePosition.row,\n                    });\n                    const zone = this.getters.expandZone(cmd.sheetIdTo, positionToZone({ col, row }));\n                    this.model.selection.resetAnchor(this, { cell: { col, row }, zone });\n                }\n                break;\n            case \"DELETE_SHEET\":\n            case \"UNDO\":\n            case \"REDO\":\n                const sheetIdExists = !!this.getters.tryGetSheet(this.sheetId);\n                if (!sheetIdExists && this.editionMode !== \"inactive\") {\n                    this.sheetId = this.getters.getActiveSheetId();\n                    this.cancelEditionAndActivateSheet();\n                    this.resetContent();\n                    this.notificationStore.raiseError(CELL_DELETED_MESSAGE);\n                }\n                break;\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    get placeholder() {\n        const position = this.getters.getActivePosition();\n        const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);\n        if (!spreader) {\n            return undefined;\n        }\n        const cell = this.getters.getCell(spreader);\n        return cell?.content;\n    }\n    get currentEditedCell() {\n        return {\n            sheetId: this.sheetId,\n            col: this.col,\n            row: this.row,\n        };\n    }\n    onColumnsRemoved(cmd) {\n        if (cmd.elements.includes(this.col) && this.editionMode !== \"inactive\") {\n            this.cancelEdition();\n            this.notificationStore.raiseError(CELL_DELETED_MESSAGE);\n            return;\n        }\n        const { top, left } = updateSelectionOnDeletion({ left: this.col, right: this.col, top: this.row, bottom: this.row }, \"left\", [...cmd.elements]);\n        this.col = left;\n        this.row = top;\n    }\n    onRowsRemoved(cmd) {\n        if (cmd.elements.includes(this.row) && this.editionMode !== \"inactive\") {\n            this.cancelEdition();\n            this.notificationStore.raiseError(CELL_DELETED_MESSAGE);\n            return;\n        }\n        const { top, left } = updateSelectionOnDeletion({ left: this.col, right: this.col, top: this.row, bottom: this.row }, \"top\", [...cmd.elements]);\n        this.col = left;\n        this.row = top;\n    }\n    onAddElements(cmd) {\n        const { top, left } = updateSelectionOnInsertion({ left: this.col, right: this.col, top: this.row, bottom: this.row }, cmd.dimension === \"COL\" ? \"left\" : \"top\", cmd.base, cmd.position, cmd.quantity);\n        this.col = left;\n        this.row = top;\n    }\n    confirmEdition(content) {\n        if (content) {\n            const sheetId = this.getters.getActiveSheetId();\n            const cell = this.getters.getEvaluatedCell({ sheetId, col: this.col, row: this.row });\n            if (cell.link && !content.startsWith(\"=\")) {\n                content = markdownLink(content, cell.link.url);\n            }\n            this.addHeadersForSpreadingFormula(content);\n            this.model.dispatch(\"UPDATE_CELL\", {\n                ...this.currentEditedCell,\n                content,\n            });\n        }\n        else {\n            this.model.dispatch(\"UPDATE_CELL\", {\n                ...this.currentEditedCell,\n                content: \"\",\n            });\n        }\n        this.model.dispatch(\"AUTOFILL_TABLE_COLUMN\", { ...this.currentEditedCell });\n        this.setContent(\"\");\n    }\n    getComposerContent(position) {\n        const locale = this.getters.getLocale();\n        const cell = this.getters.getCell(position);\n        if (cell?.isFormula) {\n            return localizeFormula(cell.content, locale);\n        }\n        const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);\n        if (spreader) {\n            return \"\";\n        }\n        const { format, value, type, formattedValue } = this.getters.getEvaluatedCell(position);\n        switch (type) {\n            case CellValueType.empty:\n                return \"\";\n            case CellValueType.text:\n            case CellValueType.error:\n                return value;\n            case CellValueType.boolean:\n                return formattedValue;\n            case CellValueType.number:\n                if (format && isDateTimeFormat(format)) {\n                    if (parseDateTime(formattedValue, locale) !== null) {\n                        // formatted string can be parsed again\n                        return formattedValue;\n                    }\n                    // display a simplified and parsable string otherwise\n                    const timeFormat = Number.isInteger(value)\n                        ? locale.dateFormat\n                        : getDateTimeFormat(locale);\n                    return formatValue(value, { locale, format: timeFormat });\n                }\n                return this.numberComposerContent(value, format, locale);\n        }\n    }\n    numberComposerContent(value, format, locale) {\n        if (format?.includes(\"%\")) {\n            return `${numberToString(value * 100, locale.decimalSeparator)}%`;\n        }\n        return numberToString(value, locale.decimalSeparator);\n    }\n    /** Add headers at the end of the sheet so the formula in the composer has enough space to spread */\n    addHeadersForSpreadingFormula(content) {\n        if (!content.startsWith(\"=\")) {\n            return;\n        }\n        const evaluated = this.getters.evaluateFormula(this.sheetId, content);\n        if (!isMatrix(evaluated)) {\n            return;\n        }\n        const numberOfRows = this.getters.getNumberRows(this.sheetId);\n        const numberOfCols = this.getters.getNumberCols(this.sheetId);\n        const missingRows = this.row + evaluated[0].length - numberOfRows;\n        const missingCols = this.col + evaluated.length - numberOfCols;\n        if (missingCols > 0) {\n            this.model.dispatch(\"ADD_COLUMNS_ROWS\", {\n                sheetId: this.sheetId,\n                dimension: \"COL\",\n                base: numberOfCols - 1,\n                position: \"after\",\n                quantity: missingCols + 20,\n            });\n        }\n        if (missingRows > 0) {\n            this.model.dispatch(\"ADD_COLUMNS_ROWS\", {\n                sheetId: this.sheetId,\n                dimension: \"ROW\",\n                base: numberOfRows - 1,\n                position: \"after\",\n                quantity: missingRows + 50,\n            });\n        }\n    }\n    checkDataValidation() {\n        const cellPosition = { sheetId: this.sheetId, col: this.col, row: this.row };\n        const content = this.getCurrentCanonicalContent();\n        const cellValue = content.startsWith(\"=\")\n            ? this.getters.evaluateFormula(this.sheetId, content)\n            : parseLiteral(content, this.getters.getLocale());\n        if (isMatrix(cellValue)) {\n            return true;\n        }\n        const validationResult = this.getters.getValidationResultForCellValue(cellValue, cellPosition);\n        if (!validationResult.isValid && validationResult.rule.isBlocking) {\n            return false;\n        }\n        return true;\n    }\n}\n\nconst COMPOSER_BORDER_WIDTH = 3 * 0.4 * window.devicePixelRatio || 1;\nconst GRID_CELL_REFERENCE_TOP_OFFSET = 28;\ncss /* scss */ `\n  div.o-grid-composer {\n    z-index: ${ComponentsImportance.GridComposer};\n    box-sizing: border-box;\n    position: absolute;\n    border: ${COMPOSER_BORDER_WIDTH}px solid ${SELECTION_BORDER_COLOR};\n    font-family: ${DEFAULT_FONT};\n\n    display: flex;\n    align-items: center;\n  }\n\n  div.o-cell-reference {\n    position: absolute;\n    z-index: ${ComponentsImportance.GridComposer};\n    background: ${SELECTION_BORDER_COLOR};\n    color: white;\n    font-size: 12px;\n    line-height: 14px;\n    padding: 6px 7px;\n    border-radius: 4px;\n  }\n`;\n/**\n * This component is a composer which positions itself on the grid at the anchor cell.\n * It also applies the style of the cell to the composer input.\n */\nclass GridComposer extends Component {\n    static template = \"o-spreadsheet-GridComposer\";\n    static props = {\n        gridDims: Object,\n        onInputContextMenu: Function,\n    };\n    static components = { Composer };\n    rect = this.defaultRect;\n    isEditing = false;\n    isCellReferenceVisible = false;\n    composerStore;\n    composerFocusStore;\n    composerInterface;\n    get defaultRect() {\n        return { x: 0, y: 0, width: 0, height: 0 };\n    }\n    setup() {\n        const composerStore = useStore(CellComposerStore);\n        this.composerStore = composerStore;\n        this.composerFocusStore = useStore(ComposerFocusStore);\n        this.composerInterface = {\n            id: \"gridComposer\",\n            get editionMode() {\n                return composerStore.editionMode;\n            },\n            startEdition: this.composerStore.startEdition,\n            setCurrentContent: this.composerStore.setCurrentContent,\n            stopEdition: this.composerStore.stopEdition,\n        };\n        this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: \"inactive\" });\n        onWillUpdateProps(() => {\n            this.updateComponentPosition();\n            this.updateCellReferenceVisibility();\n        });\n    }\n    get shouldDisplayCellReference() {\n        return this.isCellReferenceVisible;\n    }\n    get cellReference() {\n        const { col, row, sheetId } = this.composerStore.currentEditedCell;\n        const prefixSheet = sheetId !== this.env.model.getters.getActiveSheetId();\n        return getFullReference(prefixSheet ? this.env.model.getters.getSheetName(sheetId) : undefined, toXC(col, row));\n    }\n    get cellReferenceStyle() {\n        const { x: left, y: top } = this.rect;\n        return cssPropertiesToCss({\n            left: `${left - COMPOSER_BORDER_WIDTH}px`,\n            top: `${top - GRID_CELL_REFERENCE_TOP_OFFSET}px`,\n        });\n    }\n    get focus() {\n        const focus = this.composerFocusStore.activeComposer === this.composerInterface\n            ? this.composerFocusStore.focusMode\n            : \"inactive\";\n        return focus;\n    }\n    get composerProps() {\n        const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();\n        return {\n            rect: { ...this.rect },\n            delimitation: {\n                width,\n                height,\n            },\n            focus: this.focus,\n            isDefaultFocus: true,\n            onComposerContentFocused: () => this.composerFocusStore.focusComposer(this.composerInterface, {\n                focusMode: \"contentFocus\",\n            }),\n            onComposerCellFocused: (content) => this.composerFocusStore.focusComposer(this.composerInterface, {\n                focusMode: \"cellFocus\",\n                content,\n            }),\n            onInputContextMenu: this.props.onInputContextMenu,\n            composerStore: this.composerStore,\n        };\n    }\n    get containerStyle() {\n        if (this.composerStore.editionMode === \"inactive\") {\n            return `z-index: -1000;`;\n        }\n        const isFormula = this.composerStore.currentContent.startsWith(\"=\");\n        const cell = this.env.model.getters.getActiveCell();\n        const position = this.env.model.getters.getActivePosition();\n        const style = this.env.model.getters.getCellComputedStyle(position);\n        // position style\n        const { x: left, y: top, width, height } = this.rect;\n        // color style\n        const background = (!isFormula && style.fillColor) || \"#ffffff\";\n        const color = (!isFormula && style.textColor) || \"#000000\";\n        // font style\n        const fontSize = (!isFormula && style.fontSize) || 10;\n        const fontWeight = !isFormula && style.bold ? \"bold\" : undefined;\n        const fontStyle = !isFormula && style.italic ? \"italic\" : \"normal\";\n        const textDecoration = !isFormula ? getTextDecoration(style) : \"none\";\n        // align style\n        let textAlign = \"left\";\n        if (!isFormula) {\n            textAlign = style.align || cell.defaultAlign;\n        }\n        const maxHeight = this.props.gridDims.height - this.rect.y;\n        const maxWidth = this.props.gridDims.width - this.rect.x;\n        /**\n         * min-size is on the container, not the composer element, because we want to have the same size as the cell by default,\n         * including all the paddings/margins of the composer\n         *\n         * The +-1 are there to include cell borders in the composer sizing/positioning\n         */\n        return cssPropertiesToCss({\n            left: `${left - 1}px`,\n            top: `${top}px`,\n            \"min-width\": `${width + 1}px`,\n            \"min-height\": `${height + 1}px`,\n            \"max-width\": `${maxWidth}px`,\n            \"max-height\": `${maxHeight}px`,\n            background,\n            color,\n            \"font-size\": `${fontSizeInPixels(fontSize)}px`,\n            \"font-weight\": fontWeight,\n            \"font-style\": fontStyle,\n            \"text-decoration\": textDecoration,\n            \"text-align\": textAlign,\n        });\n    }\n    updateComponentPosition() {\n        const isEditing = this.composerFocusStore.activeComposer.editionMode !== \"inactive\";\n        if (!isEditing && this.composerFocusStore.activeComposer !== this.composerInterface) {\n            this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: \"inactive\" });\n        }\n        if (this.isEditing !== isEditing) {\n            this.isEditing = isEditing;\n            if (!isEditing) {\n                this.rect = this.defaultRect;\n                return;\n            }\n            const position = this.env.model.getters.getActivePosition();\n            const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));\n            this.rect = this.env.model.getters.getVisibleRect(zone);\n        }\n    }\n    updateCellReferenceVisibility() {\n        if (this.composerStore.editionMode === \"inactive\") {\n            this.isCellReferenceVisible = false;\n            return;\n        }\n        if (this.isCellReferenceVisible) {\n            return;\n        }\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const zone = this.env.model.getters.getSelectedZone();\n        const rect = this.env.model.getters.getVisibleRect(zone);\n        if (!deepEquals(rect, this.rect) || sheetId !== this.composerStore.currentEditedCell.sheetId) {\n            this.isCellReferenceVisible = true;\n        }\n    }\n    onFocus() {\n        this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: \"contentFocus\" });\n    }\n}\n\ncss /* scss */ `\n  .o-grid-cell-icon {\n    width: ${GRID_ICON_EDGE_LENGTH}px;\n    height: ${GRID_ICON_EDGE_LENGTH}px;\n  }\n`;\nclass GridCellIcon extends Component {\n    static template = \"o-spreadsheet-GridCellIcon\";\n    static props = {\n        cellPosition: Object,\n        horizontalAlign: { type: String, optional: true },\n        verticalAlign: { type: String, optional: true },\n        slots: Object,\n    };\n    get iconStyle() {\n        const cellPosition = this.props.cellPosition;\n        const merge = this.env.model.getters.getMerge(cellPosition);\n        const zone = merge || positionToZone(cellPosition);\n        const rect = this.env.model.getters.getVisibleRectWithoutHeaders(zone);\n        const x = this.getIconHorizontalPosition(rect, cellPosition);\n        const y = this.getIconVerticalPosition(rect, cellPosition);\n        return cssPropertiesToCss({\n            top: `${y}px`,\n            left: `${x}px`,\n        });\n    }\n    getIconVerticalPosition(rect, cellPosition) {\n        const start = rect.y;\n        const end = rect.y + rect.height;\n        const cell = this.env.model.getters.getCell(cellPosition);\n        const align = this.props.verticalAlign || cell?.style?.verticalAlign || DEFAULT_VERTICAL_ALIGN;\n        switch (align) {\n            case \"bottom\":\n                return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;\n            case \"top\":\n                return start + GRID_ICON_MARGIN;\n            default:\n                const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);\n                return end - GRID_ICON_EDGE_LENGTH - centeringOffset;\n        }\n    }\n    getIconHorizontalPosition(rect, cellPosition) {\n        const start = rect.x;\n        const end = rect.x + rect.width;\n        const cell = this.env.model.getters.getCell(cellPosition);\n        const evaluatedCell = this.env.model.getters.getEvaluatedCell(cellPosition);\n        const align = this.props.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;\n        switch (align) {\n            case \"right\":\n                return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;\n            case \"left\":\n                return start + GRID_ICON_MARGIN;\n            default:\n                const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);\n                return end - GRID_ICON_EDGE_LENGTH - centeringOffset;\n        }\n    }\n    isPositionVisible(position) {\n        const rect = this.env.model.getters.getVisibleRect(positionToZone(position));\n        return !(rect.width === 0 || rect.height === 0);\n    }\n}\n\nconst MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;\ncss /* scss */ `\n  .o-dv-checkbox {\n    box-sizing: border-box !important;\n    accent-color: #808080;\n    margin: ${MARGIN}px;\n    /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */\n    position: absolute;\n  }\n`;\nclass DataValidationCheckbox extends Component {\n    static template = \"o-spreadsheet-DataValidationCheckbox\";\n    static components = {\n        Checkbox,\n    };\n    static props = {\n        cellPosition: Object,\n    };\n    onCheckboxChange(value) {\n        const { sheetId, col, row } = this.props.cellPosition;\n        const cellContent = value ? \"TRUE\" : \"FALSE\";\n        this.env.model.dispatch(\"UPDATE_CELL\", { sheetId, col, row, content: cellContent });\n    }\n    get checkBoxValue() {\n        return !!this.env.model.getters.getEvaluatedCell(this.props.cellPosition).value;\n    }\n    get isDisabled() {\n        const cell = this.env.model.getters.getCell(this.props.cellPosition);\n        return this.env.model.getters.isReadonly() || !!cell?.isFormula;\n    }\n}\n\nconst ICON_WIDTH = 13;\ncss /* scss */ `\n  .o-dv-list-icon {\n    color: ${TEXT_BODY_MUTED};\n    border-radius: 1px;\n    height: ${GRID_ICON_EDGE_LENGTH}px;\n    width: ${GRID_ICON_EDGE_LENGTH}px;\n\n    &:hover {\n      color: #ffffff;\n      background-color: ${TEXT_BODY_MUTED};\n    }\n\n    svg {\n      width: ${ICON_WIDTH}px;\n      height: ${ICON_WIDTH}px;\n    }\n  }\n`;\nclass DataValidationListIcon extends Component {\n    static template = \"o-spreadsheet-DataValidationListIcon\";\n    static props = {\n        cellPosition: Object,\n    };\n    onClick() {\n        const { col, row } = this.props.cellPosition;\n        this.env.model.selection.selectCell(col, row);\n        this.env.startCellEdition();\n    }\n}\n\nclass DataValidationOverlay extends Component {\n    static template = \"o-spreadsheet-DataValidationOverlay\";\n    static props = {};\n    static components = { GridCellIcon, DataValidationCheckbox, DataValidationListIcon };\n    get checkBoxCellPositions() {\n        return this.env.model.getters\n            .getVisibleCellPositions()\n            .filter((position) => this.env.model.getters.isCellValidCheckbox(position) &&\n            !this.env.model.getters.isFilterHeader(position));\n    }\n    get listIconsCellPositions() {\n        if (this.env.model.getters.isReadonly()) {\n            return [];\n        }\n        return this.env.model.getters\n            .getVisibleCellPositions()\n            .filter((position) => this.env.model.getters.cellHasListDataValidationIcon(position) &&\n            !this.env.model.getters.isFilterHeader(position));\n    }\n}\n\n/**\n * Transform a figure with coordinates from the model, to coordinates as they are shown on the screen,\n * taking into account the scroll position of the active sheet and the frozen panes.\n */\nfunction internalFigureToScreen(getters, fig) {\n    return { ...fig, ...internalToScreenCoordinates(getters, { x: fig.x, y: fig.y }) };\n}\n/**\n * Transform a figure with coordinates as they are shown on the screen, to coordinates as they are in the model,\n * taking into account the scroll position of the active sheet and the frozen panes.\n *\n * Note that this isn't  exactly the reverse operation as internalFigureToScreen, because the figure will always be on top\n * of the frozen panes.\n */\nfunction screenFigureToInternal(getters, fig) {\n    return { ...fig, ...screenCoordinatesToInternal(getters, { x: fig.x, y: fig.y }) };\n}\nfunction internalToScreenCoordinates(getters, { x, y }) {\n    const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates();\n    const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();\n    x = x < viewportX ? x : x - scrollX;\n    y = y < viewportY ? y : y - scrollY;\n    return { x, y };\n}\nfunction screenCoordinatesToInternal(getters, { x, y }) {\n    const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates();\n    const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();\n    x = viewportX && x < viewportX ? x : x + scrollX;\n    y = viewportY && y < viewportY ? y : y + scrollY;\n    return { x, y };\n}\n\nfunction dragFigureForMove({ x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, initialFigure, { x: viewportX, y: viewportY }, { maxX, maxY }, { scrollX, scrollY }) {\n    const minX = viewportX ? 0 : -scrollX;\n    const minY = viewportY ? 0 : -scrollY;\n    const deltaX = mouseX - mouseInitialX;\n    const newX = clip(initialFigure.x + deltaX, minX, maxX - initialFigure.width - scrollX);\n    const deltaY = mouseY - mouseInitialY;\n    const newY = clip(initialFigure.y + deltaY, minY, maxY - initialFigure.height - scrollY);\n    return { ...initialFigure, x: newX, y: newY };\n}\nfunction dragFigureForResize(initialFigure, dirX, dirY, { x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, keepRatio, minFigSize, { scrollX, scrollY }) {\n    let { x, y, width, height } = initialFigure;\n    if (keepRatio && dirX != 0 && dirY != 0) {\n        const deltaX = Math.min(dirX * (mouseInitialX - mouseX), initialFigure.width - minFigSize);\n        const deltaY = Math.min(dirY * (mouseInitialY - mouseY), initialFigure.height - minFigSize);\n        const fraction = Math.min(deltaX / initialFigure.width, deltaY / initialFigure.height);\n        width = initialFigure.width * (1 - fraction);\n        height = initialFigure.height * (1 - fraction);\n        if (dirX < 0) {\n            x = initialFigure.x + initialFigure.width * fraction;\n        }\n        if (dirY < 0) {\n            y = initialFigure.y + initialFigure.height * fraction;\n        }\n    }\n    else {\n        const deltaX = Math.max(dirX * (mouseX - mouseInitialX), minFigSize - initialFigure.width);\n        const deltaY = Math.max(dirY * (mouseY - mouseInitialY), minFigSize - initialFigure.height);\n        width = initialFigure.width + deltaX;\n        height = initialFigure.height + deltaY;\n        if (dirX < 0) {\n            x = initialFigure.x - deltaX;\n        }\n        if (dirY < 0) {\n            y = initialFigure.y - deltaY;\n        }\n    }\n    // Adjusts figure dimensions to ensure it remains within header boundaries and viewport during resizing.\n    if (x + scrollX <= 0) {\n        width = width + x + scrollX;\n        x = -scrollX;\n    }\n    if (y + scrollY <= 0) {\n        height = height + y + scrollY;\n        y = -scrollY;\n    }\n    return { ...initialFigure, x, y, width, height };\n}\n\nconst SNAP_MARGIN = 5;\n/**\n * Try to snap the given figure to other figures when moving the figure, and return the snapped\n * figure and the possible snap lines, if any were found\n */\nfunction snapForMove(getters, figureToSnap, otherFigures) {\n    const snappedFigure = { ...figureToSnap };\n    const verticalSnapLine = getSnapLine(getters, snappedFigure, [\"hCenter\", \"right\", \"left\"], otherFigures, [\"hCenter\", \"right\", \"left\"]);\n    const horizontalSnapLine = getSnapLine(getters, snappedFigure, [\"vCenter\", \"bottom\", \"top\"], otherFigures, [\"vCenter\", \"bottom\", \"top\"]);\n    const { y: viewportY, x: viewportX } = getters.getMainViewportCoordinates();\n    const { scrollY, scrollX } = getters.getActiveSheetScrollInfo();\n    // If the snap cause the figure to change pane, we need to also apply the scroll as an offset\n    if (horizontalSnapLine) {\n        snappedFigure.y -= horizontalSnapLine.snapOffset;\n        const isBaseFigFrozenY = figureToSnap.y < viewportY;\n        const isSnappedFrozenY = snappedFigure.y < viewportY;\n        if (isBaseFigFrozenY && !isSnappedFrozenY)\n            snappedFigure.y += scrollY;\n        else if (!isBaseFigFrozenY && isSnappedFrozenY)\n            snappedFigure.y -= scrollY;\n    }\n    if (verticalSnapLine) {\n        snappedFigure.x -= verticalSnapLine.snapOffset;\n        const isBaseFigFrozenX = figureToSnap.x < viewportX;\n        const isSnappedFrozenX = snappedFigure.x < viewportX;\n        if (isBaseFigFrozenX && !isSnappedFrozenX)\n            snappedFigure.x += scrollX;\n        else if (!isBaseFigFrozenX && isSnappedFrozenX)\n            snappedFigure.x -= scrollX;\n    }\n    return { snappedFigure, verticalSnapLine, horizontalSnapLine };\n}\n/**\n * Try to snap the given figure to the other figures when resizing the figure, and return the snapped\n * figure and the possible snap lines, if any were found\n */\nfunction snapForResize(getters, resizeDirX, resizeDirY, figureToSnap, otherFigures) {\n    const snappedFigure = { ...figureToSnap };\n    // Vertical snap line\n    const verticalSnapLine = getSnapLine(getters, snappedFigure, [resizeDirX === -1 ? \"left\" : \"right\"], otherFigures, [\"right\", \"left\"]);\n    if (verticalSnapLine) {\n        if (resizeDirX === 1) {\n            snappedFigure.width -= verticalSnapLine.snapOffset;\n        }\n        else if (resizeDirX === -1) {\n            snappedFigure.x -= verticalSnapLine.snapOffset;\n            snappedFigure.width += verticalSnapLine.snapOffset;\n        }\n    }\n    // Horizontal snap line\n    const horizontalSnapLine = getSnapLine(getters, snappedFigure, [resizeDirY === -1 ? \"top\" : \"bottom\"], otherFigures, [\"bottom\", \"top\"]);\n    if (horizontalSnapLine) {\n        if (resizeDirY === 1) {\n            snappedFigure.height -= horizontalSnapLine.snapOffset;\n        }\n        else if (resizeDirY === -1) {\n            snappedFigure.y -= horizontalSnapLine.snapOffset;\n            snappedFigure.height += horizontalSnapLine.snapOffset;\n        }\n    }\n    snappedFigure.x = Math.round(snappedFigure.x);\n    snappedFigure.y = Math.round(snappedFigure.y);\n    snappedFigure.height = Math.round(snappedFigure.height);\n    snappedFigure.width = Math.round(snappedFigure.width);\n    return { snappedFigure, verticalSnapLine, horizontalSnapLine };\n}\n/**\n * Get the position of snap axes for the given figure\n *\n * @param figure the figure\n * @param axesTypes the list of axis types to return the positions of\n */\nfunction getVisibleAxes(getters, figure, axesTypes) {\n    const axes = axesTypes.map((axisType) => getAxis(figure, axisType));\n    return axes\n        .filter((axis) => isAxisVisible(getters, figure, axis))\n        .map((axis) => getAxisScreenPosition(getters, figure, axis));\n}\n/**\n * We need two positions for the figure axis :\n *  - the position (core) of the axis in the figure. This is used to know whether or not the axis is\n *      displayed, or is hidden by the scroll/the frozen panes\n *  - the position in the screen, which is used to find snap matches. We cannot use the core position for this,\n *      because figures partially in frozen panes aren't displayed at their actual coordinates\n */\nfunction getAxisScreenPosition(getters, figure, figureAxis) {\n    const screenFigure = internalFigureToScreen(getters, figure);\n    return getAxis(screenFigure, figureAxis.axisType);\n}\nfunction isAxisVisible(getters, figure, axis) {\n    const { x: mainViewportX, y: mainViewportY } = getters.getMainViewportCoordinates();\n    const axisStartEndPositions = [];\n    switch (axis.axisType) {\n        case \"top\":\n        case \"bottom\":\n        case \"vCenter\":\n            if (figure.y < mainViewportY)\n                return true;\n            axisStartEndPositions.push({ x: figure.x, y: axis.position });\n            axisStartEndPositions.push({ x: figure.x + figure.width, y: axis.position });\n            break;\n        case \"left\":\n        case \"right\":\n        case \"hCenter\":\n            if (figure.x < mainViewportX)\n                return true;\n            axisStartEndPositions.push({ x: axis.position, y: figure.y });\n            axisStartEndPositions.push({ x: axis.position, y: figure.y + figure.height });\n            break;\n    }\n    return axisStartEndPositions.some(getters.isPositionVisible);\n}\n/**\n * Get a snap line for the given figure, if the figure can snap to any other figure\n *\n * @param figureToSnap figure to get the snap line for\n * @param figAxesTypes figure axes of the given figure to be considered to find a snap line\n * @param otherFigures figures to match against the snapped figure to find a snap line\n * @param otherAxesTypes figure axes of the other figures to be considered to find a snap line\n */\nfunction getSnapLine(getters, figureToSnap, figAxesTypes, otherFigures, otherAxesTypes) {\n    const axesOfFigure = getVisibleAxes(getters, figureToSnap, figAxesTypes);\n    let closestMatch = undefined;\n    for (const otherFigure of otherFigures) {\n        const axesOfOtherFig = getVisibleAxes(getters, otherFigure, otherAxesTypes);\n        for (const axisOfFigure of axesOfFigure) {\n            for (const axisOfOtherFig of axesOfOtherFig) {\n                if (!canSnap(axisOfFigure.position, axisOfOtherFig.position))\n                    continue;\n                const snapOffset = axisOfFigure.position - axisOfOtherFig.position;\n                if (closestMatch && snapOffset === closestMatch.snapOffset) {\n                    closestMatch.matchedFigIds.push(otherFigure.id);\n                }\n                else if (!closestMatch || Math.abs(snapOffset) <= Math.abs(closestMatch.snapOffset)) {\n                    closestMatch = {\n                        matchedFigIds: [otherFigure.id],\n                        snapOffset,\n                        snappedAxisType: axisOfFigure.axisType,\n                        position: axisOfOtherFig.position,\n                    };\n                }\n            }\n        }\n    }\n    return closestMatch;\n}\n/** Check if two axes are close enough to snap */\nfunction canSnap(axisPosition1, axisPosition2) {\n    return Math.abs(axisPosition1 - axisPosition2) <= SNAP_MARGIN;\n}\nfunction getAxis(fig, axisType) {\n    let position = 0;\n    switch (axisType) {\n        case \"top\":\n            position = fig.y;\n            break;\n        case \"bottom\":\n            position = fig.y + fig.height - FIGURE_BORDER_WIDTH;\n            break;\n        case \"vCenter\":\n            position = fig.y + Math.floor(fig.height / 2) - FIGURE_BORDER_WIDTH;\n            break;\n        case \"left\":\n            position = fig.x;\n            break;\n        case \"right\":\n            position = fig.x + fig.width - FIGURE_BORDER_WIDTH;\n            break;\n        case \"hCenter\":\n            position = fig.x + Math.floor(fig.width / 2) - FIGURE_BORDER_WIDTH;\n            break;\n    }\n    return { position, axisType: axisType };\n}\n\ncss /*SCSS*/ `\n  .o-figure-snap-line {\n    position: relative;\n    z-index: ${ComponentsImportance.FigureSnapLine};\n    &.vertical {\n      width: 0px;\n      border-left: 1px dashed black;\n    }\n    &.horizontal {\n      border-top: 1px dashed black;\n      height: 0px;\n    }\n  }\n  .o-figure-container {\n    -webkit-user-select: none; // safari\n    user-select: none;\n  }\n`;\n/**\n * Each figure \u2b50 is positioned inside a container `div` placed and sized\n * according to the split pane the figure is part of, or a separate container for the figure\n * currently drag & dropped. Any part of the figure outside of the container is hidden\n * thanks to its `overflow: hidden` property.\n *\n * Additionally, the figure is placed inside a \"inverse viewport\" `div` \ud83d\udfe5.\n * Its position represents the viewport position in the grid: its top/left\n * corner represents the top/left corner of the grid.\n *\n * It allows to position the figure inside this div regardless of the\n * (possibly freezed) viewports and the scrolling position.\n *\n * --: container limits\n * \ud83d\udfe5: inverse viewport\n * \u2b50: figure top/left position\n *\n *                     container\n *                         \u2193\n * |\ud83d\udfe5--------------------------------------------\n * |  \\                                          |\n * |   \\                                         |\n * |    \\                                        |\n * |     \\          visible area                 |  no scroll\n * |      \u2b50                                     |\n * |                                             |\n * |                                             |\n * -----------------------------------------------\n *\n * the scrolling of the pane is applied as an inverse offset\n * to the div which will in turn move the figure up and down\n * inside the container.\n * Hence, once the figure position is (resp. partly) out of\n * the container dimensions, it will be (resp. partly) hidden.\n *\n * The same reasoning applies to the horizontal axis.\n *\n *  \ud83d\udfe5 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\n *    \\                       \u2191\n *     \\                      |\n *      \\                     | inverse viewport = -1 * scroll of pane\n *       \\                    |\n *        \u2b50 <- not visible   |\n *                            \u2193\n * -----------------------------------------------\n * |                                             |\n * |                                             |\n * |                                             |\n * |               visible area                  |\n * |                                             |\n * |                                             |\n * |                                             |\n * -----------------------------------------------\n *\n * In the case the d&d figure container, the container is the same as the \"topLeft\" container for\n * frozen pane (unaffected by scroll and always visible). The figure coordinates are transformed\n * for this container at the start of the d&d, and transformed back at the end to adapt to the scroll\n * that occurred during the drag & drop, and to position the figure on the correct pane.\n *\n */\nclass FiguresContainer extends Component {\n    static template = \"o-spreadsheet-FiguresContainer\";\n    static props = {\n        onFigureDeleted: Function,\n    };\n    static components = { FigureComponent };\n    dnd = useState({\n        draggedFigure: undefined,\n        horizontalSnap: undefined,\n        verticalSnap: undefined,\n        cancelDnd: undefined,\n    });\n    setup() {\n        onMounted(() => {\n            // horrible, but necessary\n            // the following line ensures that we render the figures with the correct\n            // viewport.  The reason is that whenever we initialize the grid\n            // component, we do not know yet the actual size of the viewport, so the\n            // first owl rendering is done with an empty viewport.  Only then we can\n            // compute which figures should be displayed, so we have to force a\n            // new rendering\n            this.render();\n        });\n        onWillUpdateProps(() => {\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            const draggedFigureId = this.dnd.draggedFigure?.id;\n            if (draggedFigureId && !this.env.model.getters.getFigure(sheetId, draggedFigureId)) {\n                if (this.dnd.cancelDnd) {\n                    this.dnd.cancelDnd();\n                }\n                this.dnd.draggedFigure = undefined;\n                this.dnd.horizontalSnap = undefined;\n                this.dnd.verticalSnap = undefined;\n                this.dnd.cancelDnd = undefined;\n            }\n        });\n    }\n    getVisibleFigures() {\n        const visibleFigures = this.env.model.getters.getVisibleFigures();\n        if (this.dnd.draggedFigure &&\n            !visibleFigures.some((figure) => figure.id === this.dnd.draggedFigure?.id)) {\n            const draggedFigure = this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id);\n            if (draggedFigure) {\n                visibleFigures.push(draggedFigure);\n            }\n        }\n        return visibleFigures;\n    }\n    get containers() {\n        const visibleFigures = this.getVisibleFigures();\n        const containers = [];\n        for (const containerType of [\n            \"topLeft\",\n            \"topRight\",\n            \"bottomLeft\",\n            \"bottomRight\",\n        ]) {\n            const containerFigures = visibleFigures.filter((figure) => this.getFigureContainer(figure) === containerType);\n            if (containerFigures.length > 0) {\n                containers.push({\n                    type: containerType,\n                    figures: containerFigures,\n                    style: this.getContainerStyle(containerType),\n                    inverseViewportStyle: this.getInverseViewportPositionStyle(containerType),\n                });\n            }\n        }\n        if (this.dnd.draggedFigure) {\n            containers.push({\n                type: \"dnd\",\n                figures: [this.getDndFigure()],\n                style: this.getContainerStyle(\"dnd\"),\n                inverseViewportStyle: this.getInverseViewportPositionStyle(\"dnd\"),\n            });\n        }\n        return containers;\n    }\n    getContainerStyle(container) {\n        return this.rectToCss(this.getContainerRect(container));\n    }\n    rectToCss(rect) {\n        return cssPropertiesToCss({\n            left: `${rect.x}px`,\n            top: `${rect.y}px`,\n            width: `${rect.width}px`,\n            height: `${rect.height}px`,\n        });\n    }\n    getContainerRect(container) {\n        const { width: viewWidth, height: viewHeight } = this.env.model.getters.getSheetViewDimension();\n        const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();\n        const x = [\"bottomRight\", \"topRight\"].includes(container) ? viewportX : 0;\n        const width = viewWidth - x;\n        const y = [\"bottomRight\", \"bottomLeft\"].includes(container) ? viewportY : 0;\n        const height = viewHeight - y;\n        return { x, y, width, height };\n    }\n    getInverseViewportPositionStyle(container) {\n        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();\n        const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();\n        const left = [\"bottomRight\", \"topRight\"].includes(container) ? -(viewportX + scrollX) : 0;\n        const top = [\"bottomRight\", \"bottomLeft\"].includes(container) ? -(viewportY + scrollY) : 0;\n        return cssPropertiesToCss({\n            left: `${left}px`,\n            top: `${top}px`,\n        });\n    }\n    getFigureContainer(figure) {\n        const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();\n        if (figure.id === this.dnd.draggedFigure?.id) {\n            return \"dnd\";\n        }\n        else if (figure.x < viewportX && figure.y < viewportY) {\n            return \"topLeft\";\n        }\n        else if (figure.x < viewportX) {\n            return \"bottomLeft\";\n        }\n        else if (figure.y < viewportY) {\n            return \"topRight\";\n        }\n        else {\n            return \"bottomRight\";\n        }\n    }\n    startDraggingFigure(figure, ev) {\n        if (ev.button > 0 || this.env.model.getters.isReadonly()) {\n            // not main button, probably a context menu and no d&d in readonly mode\n            return;\n        }\n        const selectResult = this.env.model.dispatch(\"SELECT_FIGURE\", { id: figure.id });\n        if (!selectResult.isSuccessful) {\n            return;\n        }\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const initialMousePosition = { x: ev.clientX, y: ev.clientY };\n        const maxDimensions = {\n            maxX: this.env.model.getters.getColDimensions(sheetId, this.env.model.getters.getNumberCols(sheetId) - 1).end,\n            maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end,\n        };\n        const { x, y } = internalFigureToScreen(this.env.model.getters, figure);\n        const initialFig = { ...figure, x, y };\n        const onMouseMove = (ev) => {\n            const getters = this.env.model.getters;\n            const currentMousePosition = { x: ev.clientX, y: ev.clientY };\n            const draggedFigure = dragFigureForMove(currentMousePosition, initialMousePosition, initialFig, this.env.model.getters.getMainViewportCoordinates(), maxDimensions, getters.getActiveSheetScrollInfo());\n            const otherFigures = this.getOtherFigures(figure.id);\n            const internalDragged = screenFigureToInternal(getters, draggedFigure);\n            const snapResult = snapForMove(getters, internalDragged, otherFigures);\n            this.dnd.draggedFigure = internalFigureToScreen(getters, snapResult.snappedFigure);\n            this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine);\n            this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine);\n        };\n        const onMouseUp = (ev) => {\n            if (!this.dnd.draggedFigure) {\n                return;\n            }\n            let { x, y } = screenFigureToInternal(this.env.model.getters, this.dnd.draggedFigure);\n            this.dnd.draggedFigure = undefined;\n            this.dnd.horizontalSnap = undefined;\n            this.dnd.verticalSnap = undefined;\n            this.env.model.dispatch(\"UPDATE_FIGURE\", { sheetId, id: figure.id, x, y });\n        };\n        this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);\n    }\n    /**\n     * Initialize the resize of a figure with mouse movements\n     *\n     * @param dirX X direction of the resize. -1 : resize from the left border of the figure, 0 : no resize in X, 1 :\n     * resize from the right border of the figure\n     * @param dirY Y direction of the resize. -1 : resize from the top border of the figure, 0 : no resize in Y, 1 :\n     * resize from the bottom border of the figure\n     * @param ev Mouse Event\n     */\n    startResize(figure, dirX, dirY, ev) {\n        ev.stopPropagation();\n        const initialMousePosition = { x: ev.clientX, y: ev.clientY };\n        const { x, y } = internalFigureToScreen(this.env.model.getters, figure);\n        const initialFig = { ...figure, x, y };\n        const keepRatio = figureRegistry.get(figure.tag).keepRatio || false;\n        const minFigSize = figureRegistry.get(figure.tag).minFigSize || MIN_FIG_SIZE;\n        const onMouseMove = (ev) => {\n            const currentMousePosition = { x: ev.clientX, y: ev.clientY };\n            const draggedFigure = dragFigureForResize(initialFig, dirX, dirY, currentMousePosition, initialMousePosition, keepRatio, minFigSize, this.env.model.getters.getActiveSheetScrollInfo());\n            const otherFigures = this.getOtherFigures(figure.id);\n            const snapResult = snapForResize(this.env.model.getters, dirX, dirY, draggedFigure, otherFigures);\n            this.dnd.draggedFigure = snapResult.snappedFigure;\n            this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine);\n            this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine);\n        };\n        const onMouseUp = (ev) => {\n            if (!this.dnd.draggedFigure) {\n                return;\n            }\n            let { x, y } = screenFigureToInternal(this.env.model.getters, this.dnd.draggedFigure);\n            const update = { x, y };\n            if (dirX) {\n                update.width = this.dnd.draggedFigure.width;\n            }\n            if (dirY) {\n                update.height = this.dnd.draggedFigure.height;\n            }\n            this.env.model.dispatch(\"UPDATE_FIGURE\", {\n                sheetId: this.env.model.getters.getActiveSheetId(),\n                id: figure.id,\n                ...update,\n            });\n            this.dnd.draggedFigure = undefined;\n            this.dnd.horizontalSnap = undefined;\n            this.dnd.verticalSnap = undefined;\n        };\n        this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);\n    }\n    getOtherFigures(figId) {\n        return this.getVisibleFigures().filter((f) => f.id !== figId);\n    }\n    getDndFigure() {\n        const figure = this.getVisibleFigures().find((fig) => fig.id === this.dnd.draggedFigure?.id);\n        if (!figure)\n            throw new Error(\"Dnd figure not found\");\n        return {\n            ...figure,\n            ...this.dnd.draggedFigure,\n        };\n    }\n    getFigureStyle(figure) {\n        if (figure.id !== this.dnd.draggedFigure?.id)\n            return \"\";\n        return cssPropertiesToCss({\n            opacity: \"0.9\",\n            cursor: \"grabbing\",\n        });\n    }\n    getSnap(snapLine) {\n        if (!snapLine || !this.dnd.draggedFigure)\n            return undefined;\n        const figureVisibleRects = snapLine.matchedFigIds\n            .map((id) => this.getVisibleFigures().find((fig) => fig.id === id))\n            .filter(isDefined)\n            .map((fig) => {\n            const figOnSCreen = internalFigureToScreen(this.env.model.getters, fig);\n            const container = this.getFigureContainer(fig);\n            return rectIntersection(figOnSCreen, this.getContainerRect(container));\n        })\n            .filter(isDefined);\n        const containerRect = rectUnion(this.dnd.draggedFigure, ...figureVisibleRects);\n        return {\n            line: snapLine,\n            containerStyle: this.rectToCss(containerRect),\n            lineStyle: this.getSnapLineStyle(snapLine, containerRect),\n        };\n    }\n    getSnapLineStyle(snapLine, containerRect) {\n        if (!snapLine)\n            return \"\";\n        if ([\"top\", \"vCenter\", \"bottom\"].includes(snapLine.snappedAxisType)) {\n            return cssPropertiesToCss({\n                top: `${snapLine.position - containerRect.y}px`,\n                left: `0px`,\n                width: `100%`,\n            });\n        }\n        else {\n            return cssPropertiesToCss({\n                top: `0px`,\n                left: `${snapLine.position - containerRect.x}px`,\n                height: `100%`,\n            });\n        }\n    }\n}\n\ncss /* scss */ `\n  .o-filter-icon {\n    color: ${FILTERS_COLOR};\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: ${GRID_ICON_EDGE_LENGTH}px;\n    height: ${GRID_ICON_EDGE_LENGTH}px;\n\n    &:hover {\n      background: ${FILTERS_COLOR};\n      color: #fff;\n    }\n\n    &.o-high-contrast {\n      color: #defade;\n    }\n    &.o-high-contrast:hover {\n      color: ${FILTERS_COLOR};\n      background: #fff;\n    }\n  }\n  .o-filter-icon:hover {\n    background: ${FILTERS_COLOR};\n    color: #fff;\n  }\n`;\nclass FilterIcon extends Component {\n    static template = \"o-spreadsheet-FilterIcon\";\n    static props = {\n        cellPosition: Object,\n    };\n    cellPopovers;\n    setup() {\n        this.cellPopovers = useStore(CellPopoverStore);\n    }\n    onClick() {\n        const position = this.props.cellPosition;\n        const activePopover = this.cellPopovers.persistentCellPopover;\n        const { col, row } = position;\n        if (activePopover.isOpen &&\n            activePopover.col === col &&\n            activePopover.row === row &&\n            activePopover.type === \"FilterMenu\") {\n            this.cellPopovers.close();\n            return;\n        }\n        this.cellPopovers.open({ col, row }, \"FilterMenu\");\n    }\n    get isFilterActive() {\n        return this.env.model.getters.isFilterActive(this.props.cellPosition);\n    }\n    get iconClass() {\n        const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);\n        const luminance = relativeLuminance(cellStyle.fillColor || \"#fff\");\n        return luminance < 0.45 ? \"o-high-contrast\" : \"\";\n    }\n}\n\nclass FilterIconsOverlay extends Component {\n    static template = \"o-spreadsheet-FilterIconsOverlay\";\n    static props = {};\n    static components = {\n        GridCellIcon,\n        FilterIcon,\n    };\n    getFilterHeadersPositions() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.env.model.getters.getFilterHeaders(sheetId);\n    }\n}\n\ncss /* scss */ `\n  .o-grid-add-rows {\n    input.o-input {\n      box-sizing: border-box;\n      width: 60px;\n      height: 30px;\n    }\n\n    .o-validation-error {\n      display: inline-block !important;\n      margin-top: 0;\n      margin-left: 8px;\n    }\n  }\n`;\nclass GridAddRowsFooter extends Component {\n    static template = \"o-spreadsheet-GridAddRowsFooter\";\n    static props = {\n        focusGrid: Function,\n    };\n    static components = { ValidationMessages };\n    inputRef = useRef(\"inputRef\");\n    state = useState({\n        inputValue: \"100\",\n        errorFlag: false,\n    });\n    setup() {\n        useExternalListener(window, \"click\", this.onExternalClick, { capture: true });\n    }\n    get addRowsPosition() {\n        const activeSheetId = this.env.model.getters.getActiveSheetId();\n        const { numberOfRows } = this.env.model.getters.getSheetSize(activeSheetId);\n        const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();\n        const rowDimensions = this.env.model.getters.getRowDimensions(activeSheetId, numberOfRows - 1);\n        const top = rowDimensions.end - scrollY;\n        return cssPropertiesToCss({\n            top: `${top}px`,\n        });\n    }\n    get errorMessages() {\n        return [_t(\"Please enter a number between 0 and 10000.\")];\n    }\n    onKeydown(ev) {\n        if (ev.key.toUpperCase() === \"ESCAPE\") {\n            this.props.focusGrid();\n        }\n        else if (ev.key.toUpperCase() === \"ENTER\") {\n            this.onConfirm();\n        }\n    }\n    onInput(ev) {\n        const value = ev.target.value;\n        this.state.inputValue = value;\n        const quantity = Number(value);\n        this.state.errorFlag = Number.isNaN(quantity) || quantity <= 0 || quantity > 10000;\n    }\n    onConfirm() {\n        if (this.state.errorFlag) {\n            return;\n        }\n        const quantity = Number(this.state.inputValue);\n        const activeSheetId = this.env.model.getters.getActiveSheetId();\n        const rowNumber = this.env.model.getters.getNumberRows(activeSheetId);\n        this.env.model.dispatch(\"ADD_COLUMNS_ROWS\", {\n            sheetId: activeSheetId,\n            position: \"after\",\n            base: rowNumber - 1,\n            quantity,\n            dimension: \"ROW\",\n        });\n        this.props.focusGrid();\n        // After adding new rows, scroll down to the new last row\n        const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n        const { end } = this.env.model.getters.getRowDimensions(activeSheetId, rowNumber + quantity - 1);\n        this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n            offsetX: scrollX,\n            offsetY: end,\n        });\n    }\n    onExternalClick(ev) {\n        if (this.inputRef.el !== document.activeElement || ev.target === this.inputRef.el) {\n            return;\n        }\n        this.props.focusGrid();\n    }\n}\n\nclass PaintFormatStore extends SpreadsheetStore {\n    mutators = [\"activate\", \"cancel\", \"pasteFormat\"];\n    highlightStore = this.get(HighlightStore);\n    clipboardHandlers = [\n        new CellClipboardHandler(this.getters, this.model.dispatch),\n        new BorderClipboardHandler(this.getters, this.model.dispatch),\n        new TableClipboardHandler(this.getters, this.model.dispatch),\n        new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),\n    ];\n    status = \"inactive\";\n    copiedData;\n    constructor(get) {\n        super(get);\n        this.highlightStore.register(this);\n        this.onDispose(() => {\n            this.highlightStore.unRegister(this);\n        });\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"PAINT_FORMAT\":\n                this.paintFormat(cmd.sheetId, cmd.target);\n                break;\n        }\n    }\n    activate(args) {\n        this.copiedData = this.copyFormats();\n        this.status = args.persistent ? \"persistent\" : \"oneOff\";\n    }\n    cancel() {\n        this.status = \"inactive\";\n        this.copiedData = undefined;\n    }\n    pasteFormat(target) {\n        this.model.dispatch(\"PAINT_FORMAT\", { target, sheetId: this.getters.getActiveSheetId() });\n    }\n    get isActive() {\n        return this.status !== \"inactive\";\n    }\n    copyFormats() {\n        const sheetId = this.getters.getActiveSheetId();\n        const zones = this.getters.getSelectedZones();\n        const copiedData = {};\n        for (const handler of this.clipboardHandlers) {\n            Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones)));\n        }\n        return copiedData;\n    }\n    paintFormat(sheetId, target) {\n        if (this.copiedData) {\n            for (const handler of this.clipboardHandlers) {\n                handler.paste({ zones: target, sheetId }, this.copiedData, {\n                    isCutOperation: false,\n                    pasteOption: \"onlyFormat\",\n                });\n            }\n        }\n        if (this.status === \"oneOff\") {\n            this.cancel();\n        }\n    }\n    get highlights() {\n        const data = this.copiedData;\n        if (!data) {\n            return [];\n        }\n        return data.zones.map((zone) => ({\n            zone,\n            color: SELECTION_BORDER_COLOR,\n            dashed: true,\n            sheetId: data.sheetId,\n            noFill: true,\n            thinLine: true,\n            interactive: false,\n        }));\n    }\n}\n\nconst CURSOR_SVG = /*xml*/ `\n<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"14\" height=\"16\"><path d=\"M6.5.4c1.3-.8 2.9-.1 3.8 1.4l2.9 5.1c.2.4.9 1.6-.4 2.3l-1.6.9 1.8 3.1c.2.4.1 1-.2 1.2l-1.6 1c-.3.1-.9 0-1.1-.4l-1.8-3.1-1.6 1c-.6.4-1.7 0-2.2-.8L0 4.3\"/><path fill=\"#fff\" d=\"M9.1 2a1.4 1.1 60 0 0-1.7-.6L5.5 2.5l.9 1.6-1 .6-.9-1.6-.6.4 1.8 3.1-1.3.7-1.8-3.1-1 .6 3.8 6.6 6.8-3.98M3.9 8.8 10.82 5l.795 1.4-6.81 3.96\"/></svg>\n`;\ncss /* scss */ `\n  .o-paint-format-cursor {\n    cursor: url(\"data:image/svg+xml,${encodeURIComponent(CURSOR_SVG)}\"), auto;\n  }\n`;\nfunction useCellHovered(env, gridRef, callback) {\n    let hoveredPosition = {\n        col: undefined,\n        row: undefined,\n    };\n    const { Date } = window;\n    let x = undefined;\n    let y = undefined;\n    let lastMoved = 0;\n    function getPosition() {\n        if (x === undefined || y === undefined) {\n            return { col: -1, row: -1 };\n        }\n        const col = env.model.getters.getColIndex(x);\n        const row = env.model.getters.getRowIndex(y);\n        return { col, row };\n    }\n    const { pause, resume } = useInterval(checkTiming, 200);\n    function checkTiming() {\n        const { col, row } = getPosition();\n        const delta = Date.now() - lastMoved;\n        if (delta > 300 && (col !== hoveredPosition.col || row !== hoveredPosition.row)) {\n            setPosition(undefined, undefined);\n        }\n        if (delta > 300) {\n            if (col < 0 || row < 0) {\n                return;\n            }\n            setPosition(col, row);\n        }\n    }\n    function updateMousePosition(e) {\n        if (gridRef.el === e.target) {\n            x = e.offsetX;\n            y = e.offsetY;\n            lastMoved = Date.now();\n        }\n    }\n    function recompute() {\n        const { col, row } = getPosition();\n        if (col !== hoveredPosition.col || row !== hoveredPosition.row) {\n            setPosition(undefined, undefined);\n        }\n    }\n    function onMouseLeave(e) {\n        const x = e.offsetX;\n        const y = e.offsetY;\n        const gridRect = getBoundingRectAsPOJO(gridRef.el);\n        if (y < 0 || y > gridRect.height || x < 0 || x > gridRect.width) {\n            return updateMousePosition(e);\n        }\n        else {\n            return pause();\n        }\n    }\n    useRefListener(gridRef, \"pointermove\", updateMousePosition);\n    useRefListener(gridRef, \"mouseleave\", onMouseLeave);\n    useRefListener(gridRef, \"mouseenter\", resume);\n    useRefListener(gridRef, \"pointerdown\", recompute);\n    useExternalListener(window, \"click\", handleGlobalClick);\n    function handleGlobalClick(e) {\n        const target = e.target;\n        const grid = gridRef.el;\n        if (!grid.contains(target)) {\n            setPosition(undefined, undefined);\n        }\n    }\n    function setPosition(col, row) {\n        if (col !== hoveredPosition.col || row !== hoveredPosition.row) {\n            hoveredPosition.col = col;\n            hoveredPosition.row = row;\n            callback({ col, row });\n        }\n    }\n    return hoveredPosition;\n}\nfunction useTouchMove(gridRef, handler, canMoveUp) {\n    let x = null;\n    let y = null;\n    function onTouchStart(ev) {\n        if (ev.touches.length !== 1)\n            return;\n        x = ev.touches[0].clientX;\n        y = ev.touches[0].clientY;\n    }\n    function onTouchEnd() {\n        x = null;\n        y = null;\n    }\n    function onTouchMove(ev) {\n        if (ev.touches.length !== 1)\n            return;\n        // On mobile browsers, swiping down is often associated with \"pull to refresh\".\n        // We only want this behavior if the grid is already at the top.\n        // Otherwise we only want to move the canvas up, without triggering any refresh.\n        if (canMoveUp()) {\n            ev.preventDefault();\n            ev.stopPropagation();\n        }\n        const currentX = ev.touches[0].clientX;\n        const currentY = ev.touches[0].clientY;\n        handler(x - currentX, y - currentY);\n        x = currentX;\n        y = currentY;\n    }\n    useRefListener(gridRef, \"touchstart\", onTouchStart);\n    useRefListener(gridRef, \"touchend\", onTouchEnd);\n    useRefListener(gridRef, \"touchmove\", onTouchMove);\n}\nclass GridOverlay extends Component {\n    static template = \"o-spreadsheet-GridOverlay\";\n    static props = {\n        onCellHovered: { type: Function, optional: true },\n        onCellDoubleClicked: { type: Function, optional: true },\n        onCellClicked: { type: Function, optional: true },\n        onCellRightClicked: { type: Function, optional: true },\n        onGridResized: { type: Function, optional: true },\n        onFigureDeleted: { type: Function, optional: true },\n        onGridMoved: Function,\n        gridOverlayDimensions: String,\n    };\n    static components = {\n        FiguresContainer,\n        DataValidationOverlay,\n        GridAddRowsFooter,\n        FilterIconsOverlay,\n    };\n    static defaultProps = {\n        onCellHovered: () => { },\n        onCellDoubleClicked: () => { },\n        onCellClicked: () => { },\n        onCellRightClicked: () => { },\n        onGridResized: () => { },\n        onFigureDeleted: () => { },\n    };\n    gridOverlay = useRef(\"gridOverlay\");\n    gridOverlayRect = useAbsoluteBoundingRect(this.gridOverlay);\n    cellPopovers;\n    paintFormatStore;\n    setup() {\n        useCellHovered(this.env, this.gridOverlay, this.props.onCellHovered);\n        const resizeObserver = new ResizeObserver(() => {\n            const boundingRect = this.gridOverlayEl.getBoundingClientRect();\n            this.props.onGridResized({\n                x: boundingRect.left,\n                y: boundingRect.top,\n                height: this.gridOverlayEl.clientHeight,\n                width: this.gridOverlayEl.clientWidth,\n            });\n        });\n        onMounted(() => {\n            resizeObserver.observe(this.gridOverlayEl);\n        });\n        onWillUnmount(() => {\n            resizeObserver.disconnect();\n        });\n        useTouchMove(this.gridOverlay, this.props.onGridMoved, () => {\n            const { scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n            return scrollY > 0;\n        });\n        this.cellPopovers = useStore(CellPopoverStore);\n        this.paintFormatStore = useStore(PaintFormatStore);\n    }\n    get gridOverlayEl() {\n        if (!this.gridOverlay.el) {\n            throw new Error(\"GridOverlay el is not defined.\");\n        }\n        return this.gridOverlay.el;\n    }\n    get style() {\n        return this.props.gridOverlayDimensions;\n    }\n    get isPaintingFormat() {\n        return this.paintFormatStore.isActive;\n    }\n    onMouseDown(ev) {\n        if (ev.button > 0) {\n            // not main button, probably a context menu\n            return;\n        }\n        if (ev.target === this.gridOverlay.el && this.cellPopovers.isOpen) {\n            this.cellPopovers.close();\n        }\n        const [col, row] = this.getCartesianCoordinates(ev);\n        this.props.onCellClicked(col, row, {\n            expandZone: ev.shiftKey,\n            addZone: isCtrlKey(ev),\n        });\n    }\n    onDoubleClick(ev) {\n        const [col, row] = this.getCartesianCoordinates(ev);\n        this.props.onCellDoubleClicked(col, row);\n    }\n    onContextMenu(ev) {\n        const [col, row] = this.getCartesianCoordinates(ev);\n        this.props.onCellRightClicked(col, row, { x: ev.clientX, y: ev.clientY });\n    }\n    getCartesianCoordinates(ev) {\n        const x = ev.clientX - this.gridOverlayRect.x;\n        const y = ev.clientY - this.gridOverlayRect.y;\n        const colIndex = this.env.model.getters.getColIndex(x);\n        const rowIndex = this.env.model.getters.getRowIndex(y);\n        return [colIndex, rowIndex];\n    }\n}\n\nclass GridPopover extends Component {\n    static template = \"o-spreadsheet-GridPopover\";\n    static props = {\n        onClosePopover: Function,\n        onMouseWheel: Function,\n        gridRect: Object,\n    };\n    static components = { Popover };\n    cellPopovers;\n    zIndex = ComponentsImportance.GridPopover;\n    setup() {\n        this.cellPopovers = useStore(CellPopoverStore);\n    }\n    get cellPopover() {\n        const popover = this.cellPopovers.cellPopover;\n        if (!popover.isOpen) {\n            return { isOpen: false };\n        }\n        const anchorRect = popover.anchorRect;\n        return {\n            ...popover,\n            // transform from the \"canvas coordinate system\" to the \"body coordinate system\"\n            anchorRect: {\n                ...anchorRect,\n                x: anchorRect.x + this.props.gridRect.x,\n                y: anchorRect.y + this.props.gridRect.y,\n            },\n        };\n    }\n}\n\nclass AbstractResizer extends Component {\n    static props = {\n        onOpenContextMenu: Function,\n    };\n    composerFocusStore;\n    PADDING = 0;\n    MAX_SIZE_MARGIN = 0;\n    MIN_ELEMENT_SIZE = 0;\n    lastSelectedElementIndex = null;\n    state = useState({\n        resizerIsActive: false,\n        isResizing: false,\n        isMoving: false,\n        isSelecting: false,\n        waitingForMove: false,\n        activeElement: 0,\n        draggerLinePosition: 0,\n        draggerShadowPosition: 0,\n        draggerShadowThickness: 0,\n        delta: 0,\n        base: 0,\n        position: \"before\",\n    });\n    setup() {\n        this.composerFocusStore = useStore(ComposerFocusStore);\n    }\n    _computeHandleDisplay(ev) {\n        const position = this._getEvOffset(ev);\n        const elementIndex = this._getElementIndex(position);\n        if (elementIndex < 0) {\n            return;\n        }\n        const dimensions = this._getDimensionsInViewport(elementIndex);\n        if (position - dimensions.start < this.PADDING && elementIndex !== this._getViewportOffset()) {\n            this.state.resizerIsActive = true;\n            this.state.draggerLinePosition = dimensions.start;\n            this.state.activeElement = this._getPreviousVisibleElement(elementIndex);\n        }\n        else if (dimensions.end - position < this.PADDING) {\n            this.state.resizerIsActive = true;\n            this.state.draggerLinePosition = dimensions.end;\n            this.state.activeElement = elementIndex;\n        }\n        else {\n            this.state.resizerIsActive = false;\n        }\n    }\n    _computeGrabDisplay(ev) {\n        const index = this._getElementIndex(this._getEvOffset(ev));\n        const activeElements = this._getActiveElements();\n        const selectedZoneStart = this._getSelectedZoneStart();\n        const selectedZoneEnd = this._getSelectedZoneEnd();\n        if (activeElements.has(selectedZoneStart)) {\n            if (selectedZoneStart <= index && index <= selectedZoneEnd) {\n                this.state.waitingForMove = true;\n                return;\n            }\n        }\n        this.state.waitingForMove = false;\n    }\n    onMouseMove(ev) {\n        if (this.state.isResizing || this.state.isMoving || this.state.isSelecting) {\n            return;\n        }\n        this._computeHandleDisplay(ev);\n        this._computeGrabDisplay(ev);\n    }\n    onMouseLeave() {\n        this.state.resizerIsActive = this.state.isResizing;\n        this.state.waitingForMove = false;\n    }\n    onDblClick(ev) {\n        this._fitElementSize(this.state.activeElement);\n        this.state.isResizing = false;\n        this._computeHandleDisplay(ev);\n        this._computeGrabDisplay(ev);\n    }\n    onMouseDown(ev) {\n        this.state.isResizing = true;\n        this.state.delta = 0;\n        const initialPosition = this._getClientPosition(ev);\n        const styleValue = this.state.draggerLinePosition;\n        const size = this._getElementSize(this.state.activeElement);\n        const minSize = styleValue - size + this.MIN_ELEMENT_SIZE;\n        const maxSize = this._getMaxSize();\n        const onMouseUp = (ev) => {\n            this.state.isResizing = false;\n            if (this.state.delta !== 0) {\n                this._updateSize();\n            }\n        };\n        const onMouseMove = (ev) => {\n            this.state.delta = this._getClientPosition(ev) - initialPosition;\n            this.state.draggerLinePosition = styleValue + this.state.delta;\n            if (this.state.draggerLinePosition < minSize) {\n                this.state.draggerLinePosition = minSize;\n                this.state.delta = this.MIN_ELEMENT_SIZE - size;\n            }\n            if (this.state.draggerLinePosition > maxSize) {\n                this.state.draggerLinePosition = maxSize;\n                this.state.delta = maxSize - styleValue;\n            }\n        };\n        startDnd(onMouseMove, onMouseUp);\n    }\n    select(ev) {\n        if (ev.button > 0) {\n            // not main button, probably a context menu\n            return;\n        }\n        const index = this._getElementIndex(this._getEvOffset(ev));\n        if (index < 0) {\n            return;\n        }\n        if (this.state.waitingForMove === true) {\n            if (!this.env.model.getters.isGridSelectionActive()) {\n                this._selectElement(index, false);\n            }\n            else {\n                // FIXME: Consider reintroducing this feature for all type of selection if we find\n                // a way to have the grid selection follow the other selections evolution\n                this.startMovement(ev);\n            }\n            return;\n        }\n        if (this.composerFocusStore.activeComposer.editionMode === \"editing\") {\n            this.env.model.selection.getBackToDefault();\n        }\n        this.startSelection(ev, index);\n    }\n    startMovement(ev) {\n        this.state.waitingForMove = false;\n        this.state.isMoving = true;\n        const startDimensions = this._getDimensionsInViewport(this._getSelectedZoneStart());\n        const endDimensions = this._getDimensionsInViewport(this._getSelectedZoneEnd());\n        const defaultPosition = startDimensions.start;\n        this.state.draggerLinePosition = defaultPosition;\n        this.state.base = this._getSelectedZoneStart();\n        this.state.draggerShadowPosition = defaultPosition;\n        this.state.draggerShadowThickness = endDimensions.end - startDimensions.start;\n        const mouseMoveMovement = (col, row) => {\n            let elementIndex = this._getType() === \"COL\" ? col : row;\n            if (elementIndex >= 0) {\n                // define draggerLinePosition\n                const dimensions = this._getDimensionsInViewport(elementIndex);\n                if (elementIndex <= this._getSelectedZoneStart()) {\n                    this.state.draggerLinePosition = dimensions.start;\n                    this.state.draggerShadowPosition = dimensions.start;\n                    this.state.base = elementIndex;\n                    this.state.position = \"before\";\n                }\n                else if (this._getSelectedZoneEnd() < elementIndex) {\n                    this.state.draggerLinePosition = dimensions.end;\n                    this.state.draggerShadowPosition = dimensions.end - this.state.draggerShadowThickness;\n                    this.state.base = elementIndex;\n                    this.state.position = \"after\";\n                }\n                else {\n                    this.state.draggerLinePosition = startDimensions.start;\n                    this.state.draggerShadowPosition = startDimensions.start;\n                    this.state.base = this._getSelectedZoneStart();\n                }\n            }\n        };\n        const mouseUpMovement = () => {\n            this.state.isMoving = false;\n            if (this.state.base !== this._getSelectedZoneStart()) {\n                this._moveElements();\n            }\n            this._computeGrabDisplay(ev);\n        };\n        dragAndDropBeyondTheViewport(this.env, mouseMoveMovement, mouseUpMovement);\n    }\n    startSelection(ev, index) {\n        this.state.isSelecting = true;\n        if (ev.shiftKey) {\n            this._increaseSelection(index);\n        }\n        else {\n            this._selectElement(index, isCtrlKey(ev));\n        }\n        this.lastSelectedElementIndex = index;\n        const mouseMoveSelect = (col, row) => {\n            let newIndex = this._getType() === \"COL\" ? col : row;\n            if (newIndex !== this.lastSelectedElementIndex && newIndex !== -1) {\n                this._increaseSelection(newIndex);\n                this.lastSelectedElementIndex = newIndex;\n            }\n        };\n        const mouseUpSelect = () => {\n            this.state.isSelecting = false;\n            this.lastSelectedElementIndex = null;\n            this._computeGrabDisplay(ev);\n        };\n        dragAndDropBeyondTheViewport(this.env, mouseMoveSelect, mouseUpSelect);\n    }\n    onMouseUp(ev) {\n        this.lastSelectedElementIndex = null;\n    }\n    onContextMenu(ev) {\n        ev.preventDefault();\n        const index = this._getElementIndex(this._getEvOffset(ev));\n        if (index < 0)\n            return;\n        if (!this._getActiveElements().has(index)) {\n            this._selectElement(index, false);\n        }\n        const type = this._getType();\n        this.props.onOpenContextMenu(type, ev.clientX, ev.clientY);\n    }\n}\ncss /* scss */ `\n  .o-col-resizer {\n    position: absolute;\n    top: 0;\n    left: ${HEADER_WIDTH}px;\n    right: 0;\n    height: ${HEADER_HEIGHT}px;\n    &.o-dragging {\n      cursor: grabbing;\n    }\n    &.o-grab {\n      cursor: grab;\n    }\n    .dragging-col-line {\n      top: ${HEADER_HEIGHT}px;\n      position: absolute;\n      width: 2px;\n      height: 10000px;\n      background-color: black;\n    }\n    .dragging-col-shadow {\n      top: ${HEADER_HEIGHT}px;\n      position: absolute;\n      height: 10000px;\n      background-color: black;\n      opacity: 0.1;\n    }\n    .o-handle {\n      position: absolute;\n      height: ${HEADER_HEIGHT}px;\n      width: 4px;\n      cursor: e-resize;\n      background-color: ${SELECTION_BORDER_COLOR};\n    }\n    .dragging-resizer {\n      top: ${HEADER_HEIGHT}px;\n      position: absolute;\n      margin-left: 2px;\n      width: 1px;\n      height: 10000px;\n      background-color: ${SELECTION_BORDER_COLOR};\n    }\n    .o-unhide {\n      color: ${ICONS_COLOR};\n    }\n    .o-unhide:hover {\n      z-index: ${ComponentsImportance.Grid + 1};\n      background-color: lightgrey;\n    }\n  }\n`;\nclass ColResizer extends AbstractResizer {\n    static props = {\n        onOpenContextMenu: Function,\n    };\n    static template = \"o-spreadsheet-ColResizer\";\n    colResizerRef;\n    setup() {\n        super.setup();\n        this.colResizerRef = useRef(\"colResizer\");\n        this.PADDING = 15;\n        this.MAX_SIZE_MARGIN = 90;\n        this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;\n    }\n    _getEvOffset(ev) {\n        return ev.offsetX;\n    }\n    _getViewportOffset() {\n        return this.env.model.getters.getActiveMainViewport().left;\n    }\n    _getClientPosition(ev) {\n        return ev.clientX;\n    }\n    _getElementIndex(position) {\n        return this.env.model.getters.getColIndex(position);\n    }\n    _getSelectedZoneStart() {\n        return this.env.model.getters.getSelectedZone().left;\n    }\n    _getSelectedZoneEnd() {\n        return this.env.model.getters.getSelectedZone().right;\n    }\n    _getEdgeScroll(position) {\n        return this.env.model.getters.getEdgeScrollCol(position, position, position);\n    }\n    _getDimensionsInViewport(index) {\n        return this.env.model.getters.getColDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);\n    }\n    _getElementSize(index) {\n        return this.env.model.getters.getColSize(this.env.model.getters.getActiveSheetId(), index);\n    }\n    _getMaxSize() {\n        return this.colResizerRef.el.clientWidth;\n    }\n    _updateSize() {\n        const index = this.state.activeElement;\n        const size = this.state.delta + this._getElementSize(index);\n        const cols = this.env.model.getters.getActiveCols();\n        this.env.model.dispatch(\"RESIZE_COLUMNS_ROWS\", {\n            dimension: \"COL\",\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            elements: cols.has(index) ? [...cols] : [index],\n            size,\n        });\n    }\n    _moveElements() {\n        const elements = [];\n        const start = this._getSelectedZoneStart();\n        const end = this._getSelectedZoneEnd();\n        for (let colIndex = start; colIndex <= end; colIndex++) {\n            elements.push(colIndex);\n        }\n        const result = this.env.model.dispatch(\"MOVE_COLUMNS_ROWS\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            dimension: \"COL\",\n            base: this.state.base,\n            elements,\n            position: this.state.position,\n        });\n        if (!result.isSuccessful && result.reasons.includes(\"WillRemoveExistingMerge\" /* CommandResult.WillRemoveExistingMerge */)) {\n            this.env.raiseError(MergeErrorMessage);\n        }\n    }\n    _selectElement(index, addDistinctHeader) {\n        this.env.model.selection.selectColumn(index, addDistinctHeader ? \"newAnchor\" : \"overrideSelection\");\n    }\n    _increaseSelection(index) {\n        this.env.model.selection.selectColumn(index, \"updateAnchor\");\n    }\n    _fitElementSize(index) {\n        const cols = this.env.model.getters.getActiveCols();\n        this.env.model.dispatch(\"AUTORESIZE_COLUMNS\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            cols: cols.has(index) ? [...cols] : [index],\n        });\n    }\n    _getType() {\n        return \"COL\";\n    }\n    _getActiveElements() {\n        return this.env.model.getters.getActiveCols();\n    }\n    _getPreviousVisibleElement(index) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        let row;\n        for (row = index - 1; row >= 0; row--) {\n            if (!this.env.model.getters.isColHidden(sheetId, row)) {\n                break;\n            }\n        }\n        return row;\n    }\n    unhide(hiddenElements) {\n        this.env.model.dispatch(\"UNHIDE_COLUMNS_ROWS\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            elements: hiddenElements,\n            dimension: \"COL\",\n        });\n    }\n    getUnhideButtonStyle(hiddenIndex) {\n        return cssPropertiesToCss({ left: this._getDimensionsInViewport(hiddenIndex).start + \"px\" });\n    }\n}\ncss /* scss */ `\n  .o-row-resizer {\n    position: absolute;\n    top: ${HEADER_HEIGHT}px;\n    left: 0;\n    right: 0;\n    width: ${HEADER_WIDTH}px;\n    height: 100%;\n    &.o-dragging {\n      cursor: grabbing;\n    }\n    &.o-grab {\n      cursor: grab;\n    }\n    .dragging-row-line {\n      left: ${HEADER_WIDTH}px;\n      position: absolute;\n      width: 10000px;\n      height: 2px;\n      background-color: black;\n    }\n    .dragging-row-shadow {\n      left: ${HEADER_WIDTH}px;\n      position: absolute;\n      width: 10000px;\n      background-color: black;\n      opacity: 0.1;\n    }\n    .o-handle {\n      position: absolute;\n      height: 4px;\n      width: ${HEADER_WIDTH}px;\n      cursor: n-resize;\n      background-color: ${SELECTION_BORDER_COLOR};\n    }\n    .dragging-resizer {\n      left: ${HEADER_WIDTH}px;\n      position: absolute;\n      margin-top: 2px;\n      width: 10000px;\n      height: 1px;\n      background-color: ${SELECTION_BORDER_COLOR};\n    }\n    .o-unhide {\n      color: ${ICONS_COLOR};\n    }\n    .o-unhide:hover {\n      z-index: ${ComponentsImportance.Grid + 1};\n      background-color: lightgrey;\n    }\n  }\n`;\nclass RowResizer extends AbstractResizer {\n    static props = {\n        onOpenContextMenu: Function,\n    };\n    static template = \"o-spreadsheet-RowResizer\";\n    setup() {\n        super.setup();\n        this.rowResizerRef = useRef(\"rowResizer\");\n        this.PADDING = 5;\n        this.MAX_SIZE_MARGIN = 60;\n        this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;\n    }\n    rowResizerRef;\n    _getEvOffset(ev) {\n        return ev.offsetY;\n    }\n    _getViewportOffset() {\n        return this.env.model.getters.getActiveMainViewport().top;\n    }\n    _getClientPosition(ev) {\n        return ev.clientY;\n    }\n    _getElementIndex(position) {\n        return this.env.model.getters.getRowIndex(position);\n    }\n    _getSelectedZoneStart() {\n        return this.env.model.getters.getSelectedZone().top;\n    }\n    _getSelectedZoneEnd() {\n        return this.env.model.getters.getSelectedZone().bottom;\n    }\n    _getEdgeScroll(position) {\n        return this.env.model.getters.getEdgeScrollRow(position, position, position);\n    }\n    _getDimensionsInViewport(index) {\n        return this.env.model.getters.getRowDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);\n    }\n    _getElementSize(index) {\n        return this.env.model.getters.getRowSize(this.env.model.getters.getActiveSheetId(), index);\n    }\n    _getMaxSize() {\n        return this.rowResizerRef.el.clientHeight;\n    }\n    _updateSize() {\n        const index = this.state.activeElement;\n        const size = this.state.delta + this._getElementSize(index);\n        const rows = this.env.model.getters.getActiveRows();\n        this.env.model.dispatch(\"RESIZE_COLUMNS_ROWS\", {\n            dimension: \"ROW\",\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            elements: rows.has(index) ? [...rows] : [index],\n            size,\n        });\n    }\n    _moveElements() {\n        const elements = [];\n        const start = this._getSelectedZoneStart();\n        const end = this._getSelectedZoneEnd();\n        for (let rowIndex = start; rowIndex <= end; rowIndex++) {\n            elements.push(rowIndex);\n        }\n        const result = this.env.model.dispatch(\"MOVE_COLUMNS_ROWS\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            dimension: \"ROW\",\n            base: this.state.base,\n            elements,\n            position: this.state.position,\n        });\n        if (!result.isSuccessful && result.reasons.includes(\"WillRemoveExistingMerge\" /* CommandResult.WillRemoveExistingMerge */)) {\n            this.env.raiseError(MergeErrorMessage);\n        }\n    }\n    _selectElement(index, addDistinctHeader) {\n        this.env.model.selection.selectRow(index, addDistinctHeader ? \"newAnchor\" : \"overrideSelection\");\n    }\n    _increaseSelection(index) {\n        this.env.model.selection.selectRow(index, \"updateAnchor\");\n    }\n    _fitElementSize(index) {\n        const rows = this.env.model.getters.getActiveRows();\n        this.env.model.dispatch(\"AUTORESIZE_ROWS\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            rows: rows.has(index) ? [...rows] : [index],\n        });\n    }\n    _getType() {\n        return \"ROW\";\n    }\n    _getActiveElements() {\n        return this.env.model.getters.getActiveRows();\n    }\n    _getPreviousVisibleElement(index) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        let row;\n        for (row = index - 1; row >= 0; row--) {\n            if (!this.env.model.getters.isRowHidden(sheetId, row)) {\n                break;\n            }\n        }\n        return row;\n    }\n    unhide(hiddenElements) {\n        this.env.model.dispatch(\"UNHIDE_COLUMNS_ROWS\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            dimension: \"ROW\",\n            elements: hiddenElements,\n        });\n    }\n    getUnhideButtonStyle(hiddenIndex) {\n        return cssPropertiesToCss({ top: this._getDimensionsInViewport(hiddenIndex).start + \"px\" });\n    }\n}\ncss /* scss */ `\n  .o-overlay {\n    .all {\n      position: absolute;\n      top: 0;\n      left: 0;\n      right: 0;\n      width: ${HEADER_WIDTH}px;\n      height: ${HEADER_HEIGHT}px;\n    }\n  }\n`;\nclass HeadersOverlay extends Component {\n    static props = {\n        onOpenContextMenu: Function,\n    };\n    static template = \"o-spreadsheet-HeadersOverlay\";\n    static components = { ColResizer, RowResizer };\n    selectAll() {\n        this.env.model.selection.selectAll();\n    }\n}\n\nclass GridRenderer {\n    getters;\n    renderer;\n    constructor(get) {\n        this.getters = get(ModelStore).getters;\n        this.renderer = get(RendererStore);\n        this.renderer.register(this);\n    }\n    get renderingLayers() {\n        return [\"Background\", \"Headers\"];\n    }\n    /**\n     * Get the offset of a header (see getColRowOffsetInViewport), adjusted with the header\n     * size (HEADER_HEIGHT and HEADER_WIDTH)\n     */\n    getHeaderOffset(dimension, start, index) {\n        let size = this.getters.getColRowOffsetInViewport(dimension, start, index);\n        if (!this.getters.isDashboard()) {\n            size += dimension === \"ROW\" ? HEADER_HEIGHT : HEADER_WIDTH;\n        }\n        return size;\n    }\n    // ---------------------------------------------------------------------------\n    // Grid rendering\n    // ---------------------------------------------------------------------------\n    drawLayer(renderingContext, layer) {\n        switch (layer) {\n            case \"Background\":\n                const boxes = this.getGridBoxes();\n                this.drawBackground(renderingContext, boxes);\n                this.drawOverflowingCellBackground(renderingContext, boxes);\n                this.drawCellBackground(renderingContext, boxes);\n                this.drawBorders(renderingContext, boxes);\n                this.drawTexts(renderingContext, boxes);\n                this.drawIcon(renderingContext, boxes);\n                this.drawFrozenPanes(renderingContext);\n                break;\n            case \"Headers\":\n                if (!this.getters.isDashboard()) {\n                    this.drawHeaders(renderingContext);\n                    this.drawFrozenPanesHeaders(renderingContext);\n                }\n                break;\n        }\n    }\n    drawBackground(renderingContext, boxes) {\n        const { ctx, thinLineWidth } = renderingContext;\n        const { width, height } = this.getters.getSheetViewDimensionWithHeaders();\n        // white background\n        ctx.fillStyle = \"#ffffff\";\n        ctx.fillRect(0, 0, width + CANVAS_SHIFT, height + CANVAS_SHIFT);\n        const areGridLinesVisible = !this.getters.isDashboard() &&\n            this.getters.getGridLinesVisibility(this.getters.getActiveSheetId());\n        const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;\n        if (areGridLinesVisible) {\n            for (const box of boxes) {\n                ctx.strokeStyle = CELL_BORDER_COLOR;\n                ctx.lineWidth = thinLineWidth;\n                ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);\n            }\n        }\n    }\n    drawCellBackground(renderingContext, boxes) {\n        const { ctx } = renderingContext;\n        for (const box of boxes) {\n            let style = box.style;\n            if (style.fillColor && style.fillColor !== \"#ffffff\") {\n                ctx.fillStyle = style.fillColor || \"#ffffff\";\n                ctx.fillRect(box.x, box.y, box.width, box.height);\n            }\n            if (box.dataBarFill) {\n                ctx.fillStyle = box.dataBarFill.color;\n                const percentage = box.dataBarFill.percentage;\n                const width = box.width * (percentage / 100);\n                ctx.fillRect(box.x, box.y, width, box.height);\n            }\n            if (box.isError) {\n                ctx.fillStyle = \"red\";\n                ctx.beginPath();\n                ctx.moveTo(box.x + box.width - 5, box.y);\n                ctx.lineTo(box.x + box.width, box.y);\n                ctx.lineTo(box.x + box.width, box.y + 5);\n                ctx.fill();\n            }\n        }\n    }\n    drawOverflowingCellBackground(renderingContext, boxes) {\n        const { ctx, thinLineWidth } = renderingContext;\n        for (const box of boxes) {\n            if (box.content && box.isOverflow) {\n                const align = box.content.align || \"left\";\n                let x;\n                let width;\n                const y = box.y + thinLineWidth / 2;\n                const height = box.height - thinLineWidth;\n                const clipWidth = Math.min(box.clipRect?.width || Infinity, box.content.width);\n                if (align === \"left\") {\n                    x = box.x + thinLineWidth / 2;\n                    width = clipWidth - 2 * thinLineWidth;\n                }\n                else if (align === \"right\") {\n                    x = box.x + box.width - thinLineWidth / 2;\n                    width = -clipWidth + 2 * thinLineWidth;\n                }\n                else {\n                    x =\n                        (box.clipRect?.x || box.x + box.width / 2 - box.content.width / 2) + thinLineWidth / 2;\n                    width = clipWidth - 2 * thinLineWidth;\n                }\n                ctx.fillStyle = \"#ffffff\";\n                ctx.fillRect(x, y, width, height);\n            }\n        }\n    }\n    drawBorders(renderingContext, boxes) {\n        const { ctx } = renderingContext;\n        for (let box of boxes) {\n            const border = box.border;\n            if (border) {\n                const { x, y, width, height } = box;\n                if (border.left) {\n                    drawBorder(border.left, x, y, x, y + height);\n                }\n                if (border.top) {\n                    drawBorder(border.top, x, y, x + width, y);\n                }\n                if (border.right) {\n                    drawBorder(border.right, x + width, y, x + width, y + height);\n                }\n                if (border.bottom) {\n                    drawBorder(border.bottom, x, y + height, x + width, y + height);\n                }\n            }\n        }\n        /**\n         * Following https://usefulangle.com/post/17/html5-canvas-drawing-1px-crisp-straight-lines,\n         * we need to make sure that a \"single\" pixel line is drawn on a \"half\" pixel coordinate,\n         * while a \"double\" pixel line is drawn on a \"full\" pixel coordinate. As, in the rendering\n         * process, we always had 0.5 before rendering line (to make sure it is drawn on a \"half\"\n         * pixel), we need to correct this behavior for the \"medium\" and the \"dotted\" styles, as\n         * they are drawing a two pixels width line.\n         * We also adapt here the coordinates of the line to make sure corner are correctly drawn,\n         * avoiding a \"round corners\" effect. This is done by subtracting 1 pixel to the origin of\n         * each line and adding 1 pixel to the end of each line (depending on the direction of the\n         * line).\n         */\n        function drawBorder({ style, color }, x1, y1, x2, y2) {\n            ctx.strokeStyle = color;\n            switch (style) {\n                case \"medium\":\n                    ctx.lineWidth = 2;\n                    x1 += y1 === y2 ? -0.5 : 0.5;\n                    x2 += y1 === y2 ? 1.5 : 0.5;\n                    y1 += x1 === x2 ? -0.5 : 0.5;\n                    y2 += x1 === x2 ? 1.5 : 0.5;\n                    break;\n                case \"thick\":\n                    ctx.lineWidth = 3;\n                    if (y1 === y2) {\n                        x1--;\n                        x2++;\n                    }\n                    if (x1 === x2) {\n                        y1--;\n                        y2++;\n                    }\n                    break;\n                case \"dashed\":\n                    ctx.lineWidth = 1;\n                    ctx.setLineDash([1, 3]);\n                    break;\n                case \"dotted\":\n                    ctx.lineWidth = 1;\n                    if (y1 === y2) {\n                        x1 += 0.5;\n                        x2 += 0.5;\n                    }\n                    if (x1 === x2) {\n                        y1 += 0.5;\n                        y2 += 0.5;\n                    }\n                    ctx.setLineDash([1, 1]);\n                    break;\n                case \"thin\":\n                default:\n                    ctx.lineWidth = 1;\n                    break;\n            }\n            ctx.beginPath();\n            ctx.moveTo(x1, y1);\n            ctx.lineTo(x2, y2);\n            ctx.stroke();\n            ctx.lineWidth = 1;\n            ctx.setLineDash([]);\n        }\n    }\n    drawTexts(renderingContext, boxes) {\n        const { ctx } = renderingContext;\n        ctx.textBaseline = \"top\";\n        let currentFont;\n        for (let box of boxes) {\n            if (box.content) {\n                const style = box.style || {};\n                const align = box.content.align || \"left\";\n                // compute font and textColor\n                const font = computeTextFont(style);\n                if (font !== currentFont) {\n                    currentFont = font;\n                    ctx.font = font;\n                }\n                ctx.fillStyle = style.textColor || \"#000\";\n                // compute horizontal align start point parameter\n                let x = box.x;\n                if (align === \"left\") {\n                    x += MIN_CELL_TEXT_MARGIN + (box.image ? box.image.size + MIN_CF_ICON_MARGIN : 0);\n                }\n                else if (align === \"right\") {\n                    x +=\n                        box.width -\n                            MIN_CELL_TEXT_MARGIN -\n                            (box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0);\n                }\n                else {\n                    x += box.width / 2;\n                }\n                // horizontal align text direction\n                ctx.textAlign = align;\n                // clip rect if needed\n                if (box.clipRect) {\n                    ctx.save();\n                    ctx.beginPath();\n                    const { x, y, width, height } = box.clipRect;\n                    ctx.rect(x, y, width, height);\n                    ctx.clip();\n                }\n                // compute vertical align start point parameter:\n                const textLineHeight = computeTextFontSizeInPixels(style);\n                const numberOfLines = box.content.textLines.length;\n                let y = this.computeTextYCoordinate(box, textLineHeight, numberOfLines);\n                // use the horizontal and the vertical start points to:\n                // fill text / fill strikethrough / fill underline\n                for (let brokenLine of box.content.textLines) {\n                    drawDecoratedText(ctx, brokenLine, { x: Math.round(x), y: Math.round(y) }, style.underline, style.strikethrough);\n                    y += MIN_CELL_TEXT_MARGIN + textLineHeight;\n                }\n                if (box.clipRect) {\n                    ctx.restore();\n                }\n            }\n        }\n    }\n    drawIcon(renderingContext, boxes) {\n        const { ctx } = renderingContext;\n        for (const box of boxes) {\n            if (box.image) {\n                const icon = box.image.image;\n                if (box.image.clipIcon) {\n                    ctx.save();\n                    ctx.beginPath();\n                    const { x, y, width, height } = box.image.clipIcon;\n                    ctx.rect(x, y, width, height);\n                    ctx.clip();\n                }\n                const iconSize = box.image.size;\n                const y = this.computeTextYCoordinate(box, iconSize);\n                ctx.drawImage(icon, box.x + MIN_CF_ICON_MARGIN, y, iconSize, iconSize);\n                if (box.image.clipIcon) {\n                    ctx.restore();\n                }\n            }\n        }\n    }\n    /** Computes the vertical start point from which a text line should be draw.\n     *\n     * Note that in case the cell does not have enough spaces to display its text lines,\n     * (wrapping cell case) then the vertical align should be at the top.\n     * */\n    computeTextYCoordinate(box, textLineHeight, numberOfLines = 1) {\n        const y = box.y + 1;\n        const textHeight = computeTextLinesHeight(textLineHeight, numberOfLines);\n        const hasEnoughSpaces = box.height > textHeight + MIN_CELL_TEXT_MARGIN * 2;\n        const verticalAlign = box.verticalAlign || DEFAULT_VERTICAL_ALIGN;\n        if (hasEnoughSpaces) {\n            if (verticalAlign === \"middle\") {\n                return y + (box.height - textHeight) / 2;\n            }\n            if (verticalAlign === \"bottom\") {\n                return y + box.height - textHeight - MIN_CELL_TEXT_MARGIN;\n            }\n        }\n        return y + MIN_CELL_TEXT_MARGIN;\n    }\n    drawHeaders(renderingContext) {\n        const { ctx, thinLineWidth } = renderingContext;\n        const visibleCols = this.getters.getSheetViewVisibleCols();\n        const left = visibleCols[0];\n        const right = visibleCols[visibleCols.length - 1];\n        const visibleRows = this.getters.getSheetViewVisibleRows();\n        const top = visibleRows[0];\n        const bottom = visibleRows[visibleRows.length - 1];\n        const { width, height } = this.getters.getSheetViewDimensionWithHeaders();\n        const selection = this.getters.getSelectedZones();\n        const selectedCols = getZonesCols(selection);\n        const selectedRows = getZonesRows(selection);\n        const sheetId = this.getters.getActiveSheetId();\n        const numberOfCols = this.getters.getNumberCols(sheetId);\n        const numberOfRows = this.getters.getNumberRows(sheetId);\n        const activeCols = this.getters.getActiveCols();\n        const activeRows = this.getters.getActiveRows();\n        ctx.font = `400 ${HEADER_FONT_SIZE}px ${DEFAULT_FONT}`;\n        ctx.textAlign = \"center\";\n        ctx.textBaseline = \"middle\";\n        ctx.lineWidth = thinLineWidth;\n        ctx.strokeStyle = \"#333\";\n        // Columns headers background\n        for (let col = left; col <= right; col++) {\n            const colZone = { left: col, right: col, top: 0, bottom: numberOfRows - 1 };\n            const { x, width } = this.getters.getVisibleRect(colZone);\n            const isColActive = activeCols.has(col);\n            const isColSelected = selectedCols.has(col);\n            if (isColActive) {\n                ctx.fillStyle = BACKGROUND_HEADER_ACTIVE_COLOR;\n            }\n            else if (isColSelected) {\n                ctx.fillStyle = BACKGROUND_HEADER_SELECTED_COLOR;\n            }\n            else {\n                ctx.fillStyle = BACKGROUND_HEADER_COLOR;\n            }\n            ctx.fillRect(x, 0, width, HEADER_HEIGHT);\n        }\n        // Rows headers background\n        for (let row = top; row <= bottom; row++) {\n            const rowZone = { top: row, bottom: row, left: 0, right: numberOfCols - 1 };\n            const { y, height } = this.getters.getVisibleRect(rowZone);\n            const isRowActive = activeRows.has(row);\n            const isRowSelected = selectedRows.has(row);\n            if (isRowActive) {\n                ctx.fillStyle = BACKGROUND_HEADER_ACTIVE_COLOR;\n            }\n            else if (isRowSelected) {\n                ctx.fillStyle = BACKGROUND_HEADER_SELECTED_COLOR;\n            }\n            else {\n                ctx.fillStyle = BACKGROUND_HEADER_COLOR;\n            }\n            ctx.fillRect(0, y, HEADER_WIDTH, height);\n        }\n        // 2 main lines\n        ctx.beginPath();\n        ctx.moveTo(HEADER_WIDTH, 0);\n        ctx.lineTo(HEADER_WIDTH, height);\n        ctx.moveTo(0, HEADER_HEIGHT);\n        ctx.lineTo(width, HEADER_HEIGHT);\n        ctx.strokeStyle = HEADER_BORDER_COLOR;\n        ctx.stroke();\n        ctx.beginPath();\n        // column text + separator\n        for (const i of visibleCols) {\n            const colSize = this.getters.getColSize(sheetId, i);\n            const colName = numberToLetters(i);\n            ctx.fillStyle = activeCols.has(i) ? \"#fff\" : TEXT_HEADER_COLOR;\n            let colStart = this.getHeaderOffset(\"COL\", left, i);\n            ctx.fillText(colName, colStart + colSize / 2, HEADER_HEIGHT / 2);\n            ctx.moveTo(colStart + colSize, 0);\n            ctx.lineTo(colStart + colSize, HEADER_HEIGHT);\n        }\n        // row text + separator\n        for (const i of visibleRows) {\n            const rowSize = this.getters.getRowSize(sheetId, i);\n            ctx.fillStyle = activeRows.has(i) ? \"#fff\" : TEXT_HEADER_COLOR;\n            let rowStart = this.getHeaderOffset(\"ROW\", top, i);\n            ctx.fillText(String(i + 1), HEADER_WIDTH / 2, rowStart + rowSize / 2);\n            ctx.moveTo(0, rowStart + rowSize);\n            ctx.lineTo(HEADER_WIDTH, rowStart + rowSize);\n        }\n        ctx.stroke();\n    }\n    drawFrozenPanesHeaders(renderingContext) {\n        const { ctx, thinLineWidth } = renderingContext;\n        const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();\n        const widthCorrection = this.getters.isDashboard() ? 0 : HEADER_WIDTH;\n        const heightCorrection = this.getters.isDashboard() ? 0 : HEADER_HEIGHT;\n        ctx.lineWidth = 6 * thinLineWidth;\n        ctx.strokeStyle = FROZEN_PANE_HEADER_BORDER_COLOR;\n        ctx.beginPath();\n        if (offsetCorrectionX) {\n            ctx.moveTo(widthCorrection + offsetCorrectionX, 0);\n            ctx.lineTo(widthCorrection + offsetCorrectionX, heightCorrection);\n        }\n        if (offsetCorrectionY) {\n            ctx.moveTo(0, heightCorrection + offsetCorrectionY);\n            ctx.lineTo(widthCorrection, heightCorrection + offsetCorrectionY);\n        }\n        ctx.stroke();\n    }\n    drawFrozenPanes(renderingContext) {\n        const { ctx, thinLineWidth } = renderingContext;\n        const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();\n        const visibleCols = this.getters.getSheetViewVisibleCols();\n        const left = visibleCols[0];\n        const right = visibleCols[visibleCols.length - 1];\n        const visibleRows = this.getters.getSheetViewVisibleRows();\n        const top = visibleRows[0];\n        const bottom = visibleRows[visibleRows.length - 1];\n        const viewport = { left, right, top, bottom };\n        const rect = this.getters.getVisibleRect(viewport);\n        const widthCorrection = this.getters.isDashboard() ? 0 : HEADER_WIDTH;\n        const heightCorrection = this.getters.isDashboard() ? 0 : HEADER_HEIGHT;\n        ctx.lineWidth = 6 * thinLineWidth;\n        ctx.strokeStyle = FROZEN_PANE_BORDER_COLOR;\n        ctx.beginPath();\n        if (offsetCorrectionX) {\n            ctx.moveTo(widthCorrection + offsetCorrectionX, heightCorrection);\n            ctx.lineTo(widthCorrection + offsetCorrectionX, rect.height + heightCorrection);\n        }\n        if (offsetCorrectionY) {\n            ctx.moveTo(widthCorrection, heightCorrection + offsetCorrectionY);\n            ctx.lineTo(rect.width + widthCorrection, heightCorrection + offsetCorrectionY);\n        }\n        ctx.stroke();\n    }\n    findNextEmptyCol(base, max, row) {\n        const sheetId = this.getters.getActiveSheetId();\n        let col = base;\n        while (col < max) {\n            const position = { sheetId, col: col + 1, row };\n            const nextCell = this.getters.getEvaluatedCell(position);\n            const nextCellBorder = this.getters.getCellComputedBorder(position);\n            const cellHasIcon = this.getters.doesCellHaveGridIcon(position);\n            const cellHasCheckbox = this.getters.isCellValidCheckbox(position);\n            if (nextCell.type !== CellValueType.empty ||\n                this.getters.isInMerge(position) ||\n                nextCellBorder?.left ||\n                cellHasIcon ||\n                cellHasCheckbox) {\n                return col;\n            }\n            col++;\n        }\n        return col;\n    }\n    findPreviousEmptyCol(base, min, row) {\n        const sheetId = this.getters.getActiveSheetId();\n        let col = base;\n        while (col > min) {\n            const position = { sheetId, col: col - 1, row };\n            const previousCell = this.getters.getEvaluatedCell(position);\n            const previousCellBorder = this.getters.getCellComputedBorder(position);\n            const cellHasIcon = this.getters.doesCellHaveGridIcon(position);\n            const cellHasCheckbox = this.getters.isCellValidCheckbox(position);\n            if (previousCell.type !== CellValueType.empty ||\n                this.getters.isInMerge(position) ||\n                previousCellBorder?.right ||\n                cellHasIcon ||\n                cellHasCheckbox) {\n                return col;\n            }\n            col--;\n        }\n        return col;\n    }\n    computeCellAlignment(position, isOverflowing) {\n        const cell = this.getters.getCell(position);\n        if (cell?.isFormula && this.getters.shouldShowFormulas()) {\n            return \"left\";\n        }\n        const { align } = this.getters.getCellStyle(position);\n        const evaluatedCell = this.getters.getEvaluatedCell(position);\n        if (isOverflowing && evaluatedCell.type === CellValueType.number) {\n            return align !== \"center\" ? \"left\" : align;\n        }\n        return align || evaluatedCell.defaultAlign;\n    }\n    createZoneBox(sheetId, zone, viewport) {\n        const { left, right } = viewport;\n        const col = zone.left;\n        const row = zone.top;\n        const position = { sheetId, col, row };\n        const cell = this.getters.getEvaluatedCell(position);\n        const showFormula = this.getters.shouldShowFormulas();\n        const { x, y, width, height } = this.getters.getVisibleRect(zone);\n        const { verticalAlign } = this.getters.getCellStyle(position);\n        const box = {\n            x,\n            y,\n            width,\n            height,\n            border: this.getters.getCellComputedBorder(position) || undefined,\n            style: this.getters.getCellComputedStyle(position),\n            dataBarFill: this.getters.getConditionalDataBar(position),\n            verticalAlign,\n            isError: (cell.type === CellValueType.error && !!cell.message) ||\n                this.getters.isDataValidationInvalid(position),\n        };\n        /** Icon */\n        const iconSrc = this.getters.getCellIconSrc(position);\n        const fontSizePX = computeTextFontSizeInPixels(box.style);\n        const iconBoxWidth = iconSrc ? MIN_CF_ICON_MARGIN + fontSizePX : 0;\n        if (iconSrc) {\n            const imageHtmlElement = loadIconImage(iconSrc);\n            box.image = {\n                type: \"icon\",\n                size: fontSizePX,\n                clipIcon: { x: box.x, y: box.y, width: Math.min(iconBoxWidth, width), height },\n                image: imageHtmlElement,\n            };\n        }\n        if (cell.type === CellValueType.empty || this.getters.isCellValidCheckbox(position)) {\n            return box;\n        }\n        /** Filter Header or data validation icon */\n        box.hasIcon = this.getters.doesCellHaveGridIcon(position);\n        const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;\n        /** Content */\n        const style = this.getters.getCellComputedStyle(position);\n        const wrapping = style.wrapping || \"overflow\";\n        const wrapText = wrapping === \"wrap\" && !showFormula;\n        const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;\n        const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });\n        const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));\n        const contentWidth = iconBoxWidth + textWidth + headerIconWidth;\n        const align = this.computeCellAlignment(position, contentWidth > width);\n        box.content = {\n            textLines: multiLineText,\n            width: wrapping === \"overflow\" ? textWidth : width,\n            align,\n        };\n        /** ClipRect */\n        const isOverflowing = contentWidth > width || fontSizePX > height;\n        if (iconSrc || box.hasIcon) {\n            box.clipRect = {\n                x: box.x + iconBoxWidth,\n                y: box.y,\n                width: Math.max(0, width - iconBoxWidth - headerIconWidth),\n                height,\n            };\n        }\n        else if (isOverflowing && wrapping === \"overflow\") {\n            let nextColIndex, previousColIndex;\n            const isCellInMerge = this.getters.isInMerge(position);\n            if (isCellInMerge) {\n                // Always clip merges\n                nextColIndex = this.getters.getMerge(position).right;\n                previousColIndex = col;\n            }\n            else {\n                nextColIndex = this.findNextEmptyCol(col, right, row);\n                previousColIndex = this.findPreviousEmptyCol(col, left, row);\n                box.isOverflow = true;\n            }\n            switch (align) {\n                case \"left\": {\n                    const emptyZoneOnTheLeft = positionToZone({ col: nextColIndex, row });\n                    const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheLeft));\n                    if (width < contentWidth || fontSizePX > height || multiLineText.length > 1) {\n                        box.clipRect = { x, y, width, height };\n                    }\n                    break;\n                }\n                case \"right\": {\n                    const emptyZoneOnTheRight = positionToZone({ col: previousColIndex, row });\n                    const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheRight));\n                    if (width < contentWidth || fontSizePX > height || multiLineText.length > 1) {\n                        box.clipRect = { x, y, width, height };\n                    }\n                    break;\n                }\n                case \"center\": {\n                    const emptyZone = {\n                        ...zone,\n                        left: previousColIndex,\n                        right: nextColIndex,\n                    };\n                    const { x, y, height, width } = this.getters.getVisibleRect(emptyZone);\n                    const halfContentWidth = contentWidth / 2;\n                    const boxMiddle = box.x + box.width / 2;\n                    if (x + width < boxMiddle + halfContentWidth ||\n                        x > boxMiddle - halfContentWidth ||\n                        fontSizePX > height ||\n                        multiLineText.length > 1) {\n                        const clipX = x > boxMiddle - halfContentWidth ? x : boxMiddle - halfContentWidth;\n                        const clipWidth = x + width - clipX;\n                        box.clipRect = { x: clipX, y, width: clipWidth, height };\n                    }\n                    break;\n                }\n            }\n        }\n        else if (wrapping === \"clip\" || wrapping === \"wrap\" || multiLineText.length > 1) {\n            box.clipRect = {\n                x: box.x,\n                y: box.y,\n                width,\n                height,\n            };\n        }\n        return box;\n    }\n    getGridBoxes() {\n        const boxes = [];\n        const visibleCols = this.getters.getSheetViewVisibleCols();\n        const left = visibleCols[0];\n        const right = visibleCols[visibleCols.length - 1];\n        const visibleRows = this.getters.getSheetViewVisibleRows();\n        const top = visibleRows[0];\n        const bottom = visibleRows[visibleRows.length - 1];\n        const viewport = { left, right, top, bottom };\n        const sheetId = this.getters.getActiveSheetId();\n        for (const row of visibleRows) {\n            for (const col of visibleCols) {\n                const position = { sheetId, col, row };\n                if (this.getters.isInMerge(position)) {\n                    continue;\n                }\n                boxes.push(this.createZoneBox(sheetId, positionToZone(position), viewport));\n            }\n        }\n        for (const merge of this.getters.getMerges(sheetId)) {\n            if (this.getters.isMergeHidden(sheetId, merge)) {\n                continue;\n            }\n            if (overlap(merge, viewport)) {\n                const box = this.createZoneBox(sheetId, merge, viewport);\n                const borderBottomRight = this.getters.getCellComputedBorder({\n                    sheetId,\n                    col: merge.right,\n                    row: merge.bottom,\n                });\n                box.border = {\n                    ...box.border,\n                    bottom: borderBottomRight ? borderBottomRight.bottom : undefined,\n                    right: borderBottomRight ? borderBottomRight.right : undefined,\n                };\n                box.isMerge = true;\n                boxes.push(box);\n            }\n        }\n        return boxes;\n    }\n}\nconst loadIconImage = memoize(function loadIconImage(src) {\n    const image = new Image();\n    image.src = src;\n    return image;\n});\n\nfunction useGridDrawing(refName, model, canvasSize) {\n    const canvasRef = useRef(refName);\n    useEffect(drawGrid);\n    const rendererStore = useStore(RendererStore);\n    useStore(GridRenderer);\n    function drawGrid() {\n        const canvas = canvasRef.el;\n        const dpr = window.devicePixelRatio || 1;\n        const ctx = canvas.getContext(\"2d\", { alpha: false });\n        const thinLineWidth = 0.4 * dpr;\n        const renderingContext = {\n            ctx,\n            dpr,\n            thinLineWidth,\n        };\n        const { width, height } = canvasSize();\n        canvas.style.width = `${width}px`;\n        canvas.style.height = `${height}px`;\n        canvas.width = width * dpr;\n        canvas.height = height * dpr;\n        canvas.setAttribute(\"style\", `width:${width}px;height:${height}px;`);\n        // Imagine each pixel as a large square. The whole-number coordinates (0, 1, 2\u2026)\n        // are the edges of the squares. If you draw a one-unit-wide line between whole-number\n        // coordinates, it will overlap opposite sides of the pixel square, and the resulting\n        // line will be drawn two pixels wide. To draw a line that is only one pixel wide,\n        // you need to shift the coordinates by 0.5 perpendicular to the line's direction.\n        // http://diveintohtml5.info/canvas.html#pixel-madness\n        ctx.translate(-CANVAS_SHIFT, -CANVAS_SHIFT);\n        ctx.scale(dpr, dpr);\n        for (const layer of OrderedLayers()) {\n            model.drawLayer(renderingContext, layer);\n            // @ts-ignore 'drawLayer' is not declated as a mutator because:\n            // it does not mutate anything. Most importantly it's used\n            // during rendering. Invoking a mutator during rendering would\n            // trigger another rendering, ultimately resulting in an infinite loop.\n            rendererStore.drawLayer(renderingContext, layer);\n        }\n    }\n}\n\nfunction useWheelHandler(handler) {\n    function normalize(val, deltaMode) {\n        return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT);\n    }\n    const onMouseWheel = (ev) => {\n        const deltaX = normalize(ev.shiftKey && !isMacOS() ? ev.deltaY : ev.deltaX, ev.deltaMode);\n        const deltaY = normalize(ev.shiftKey && !isMacOS() ? ev.deltaX : ev.deltaY, ev.deltaMode);\n        handler(deltaX, deltaY);\n    };\n    return onMouseWheel;\n}\n\ncss /* scss */ `\n  .o-border {\n    position: absolute;\n    &:hover {\n      cursor: grab;\n    }\n  }\n  .o-moving {\n    cursor: grabbing;\n  }\n`;\nclass Border extends Component {\n    static template = \"o-spreadsheet-Border\";\n    static props = {\n        zone: Object,\n        orientation: String,\n        isMoving: Boolean,\n        onMoveHighlight: Function,\n    };\n    get style() {\n        const isTop = [\"n\", \"w\", \"e\"].includes(this.props.orientation);\n        const isLeft = [\"n\", \"w\", \"s\"].includes(this.props.orientation);\n        const isHorizontal = [\"n\", \"s\"].includes(this.props.orientation);\n        const isVertical = [\"w\", \"e\"].includes(this.props.orientation);\n        const z = this.props.zone;\n        const margin = 2;\n        const rect = this.env.model.getters.getVisibleRect(z);\n        const left = rect.x;\n        const right = rect.x + rect.width - 2 * margin;\n        const top = rect.y;\n        const bottom = rect.y + rect.height - 2 * margin;\n        const lineWidth = 4;\n        const leftValue = isLeft ? left : right;\n        const topValue = isTop ? top : bottom;\n        const widthValue = isHorizontal ? right - left : lineWidth;\n        const heightValue = isVertical ? bottom - top : lineWidth;\n        return cssPropertiesToCss({\n            left: `${leftValue}px`,\n            top: `${topValue}px`,\n            width: `${widthValue}px`,\n            height: `${heightValue}px`,\n        });\n    }\n    onMouseDown(ev) {\n        this.props.onMoveHighlight(ev.clientX, ev.clientY);\n    }\n}\n\ncss /* scss */ `\n  .o-corner {\n    position: absolute;\n    height: 6px;\n    width: 6px;\n    border: 1px solid white;\n  }\n  .o-corner-nw,\n  .o-corner-se {\n    &:hover {\n      cursor: nwse-resize;\n    }\n  }\n  .o-corner-ne,\n  .o-corner-sw {\n    &:hover {\n      cursor: nesw-resize;\n    }\n  }\n  .o-resizing {\n    cursor: grabbing;\n  }\n`;\nclass Corner extends Component {\n    static template = \"o-spreadsheet-Corner\";\n    static props = {\n        zone: Object,\n        color: String,\n        orientation: String,\n        isResizing: Boolean,\n        onResizeHighlight: Function,\n    };\n    isTop = this.props.orientation[0] === \"n\";\n    isLeft = this.props.orientation[1] === \"w\";\n    get style() {\n        const z = this.props.zone;\n        const col = this.isLeft ? z.left : z.right;\n        const row = this.isTop ? z.top : z.bottom;\n        const rect = this.env.model.getters.getVisibleRect({\n            left: col,\n            right: col,\n            top: row,\n            bottom: row,\n        });\n        // Don't show if not visible in the viewport\n        if (rect.width * rect.height === 0) {\n            return `display:none`;\n        }\n        const leftValue = this.isLeft ? rect.x : rect.x + rect.width;\n        const topValue = this.isTop ? rect.y : rect.y + rect.height;\n        return cssPropertiesToCss({\n            left: `${leftValue - AUTOFILL_EDGE_LENGTH / 2}px`,\n            top: `${topValue - AUTOFILL_EDGE_LENGTH / 2}px`,\n            \"background-color\": this.props.color,\n        });\n    }\n    onMouseDown(ev) {\n        this.props.onResizeHighlight(this.isLeft, this.isTop);\n    }\n}\n\ncss /*SCSS*/ `\n  .o-highlight {\n    z-index: ${ComponentsImportance.Highlight};\n  }\n`;\nclass Highlight extends Component {\n    static template = \"o-spreadsheet-Highlight\";\n    static props = {\n        zone: Object,\n        color: String,\n    };\n    static components = {\n        Corner,\n        Border,\n    };\n    highlightState = useState({\n        shiftingMode: \"none\",\n    });\n    onResizeHighlight(isLeft, isTop) {\n        const activeSheetId = this.env.model.getters.getActiveSheetId();\n        this.highlightState.shiftingMode = \"isResizing\";\n        const z = this.props.zone;\n        const pivotCol = isLeft ? z.right : z.left;\n        const pivotRow = isTop ? z.bottom : z.top;\n        let lastCol = isLeft ? z.left : z.right;\n        let lastRow = isTop ? z.top : z.bottom;\n        let currentZone = z;\n        this.env.model.dispatch(\"START_CHANGE_HIGHLIGHT\", { zone: currentZone });\n        const mouseMove = (col, row) => {\n            if (lastCol !== col || lastRow !== row) {\n                lastCol = clip(col === -1 ? lastCol : col, 0, this.env.model.getters.getNumberCols(activeSheetId) - 1);\n                lastRow = clip(row === -1 ? lastRow : row, 0, this.env.model.getters.getNumberRows(activeSheetId) - 1);\n                let newZone = {\n                    left: Math.min(pivotCol, lastCol),\n                    top: Math.min(pivotRow, lastRow),\n                    right: Math.max(pivotCol, lastCol),\n                    bottom: Math.max(pivotRow, lastRow),\n                };\n                if (!isEqual(newZone, currentZone)) {\n                    this.env.model.selection.selectZone({\n                        cell: { col: newZone.left, row: newZone.top },\n                        zone: newZone,\n                    }, { unbounded: true });\n                    currentZone = newZone;\n                }\n            }\n        };\n        const mouseUp = () => {\n            this.highlightState.shiftingMode = \"none\";\n        };\n        dragAndDropBeyondTheViewport(this.env, mouseMove, mouseUp);\n    }\n    onMoveHighlight(clientX, clientY) {\n        this.highlightState.shiftingMode = \"isMoving\";\n        const z = this.props.zone;\n        const position = gridOverlayPosition();\n        const activeSheetId = this.env.model.getters.getActiveSheetId();\n        const initCol = this.env.model.getters.getColIndex(clientX - position.left);\n        const initRow = this.env.model.getters.getRowIndex(clientY - position.top);\n        const deltaColMin = -z.left;\n        const deltaColMax = this.env.model.getters.getNumberCols(activeSheetId) - z.right - 1;\n        const deltaRowMin = -z.top;\n        const deltaRowMax = this.env.model.getters.getNumberRows(activeSheetId) - z.bottom - 1;\n        let currentZone = z;\n        this.env.model.dispatch(\"START_CHANGE_HIGHLIGHT\", { zone: currentZone });\n        let lastCol = initCol;\n        let lastRow = initRow;\n        const mouseMove = (col, row) => {\n            if (lastCol !== col || lastRow !== row) {\n                lastCol = col === -1 ? lastCol : col;\n                lastRow = row === -1 ? lastRow : row;\n                const deltaCol = clip(lastCol - initCol, deltaColMin, deltaColMax);\n                const deltaRow = clip(lastRow - initRow, deltaRowMin, deltaRowMax);\n                let newZone = {\n                    left: z.left + deltaCol,\n                    top: z.top + deltaRow,\n                    right: z.right + deltaCol,\n                    bottom: z.bottom + deltaRow,\n                };\n                if (!isEqual(newZone, currentZone)) {\n                    this.env.model.selection.selectZone({\n                        cell: { col: newZone.left, row: newZone.top },\n                        zone: newZone,\n                    }, { unbounded: true });\n                    currentZone = newZone;\n                }\n            }\n        };\n        const mouseUp = () => {\n            this.highlightState.shiftingMode = \"none\";\n        };\n        dragAndDropBeyondTheViewport(this.env, mouseMove, mouseUp);\n    }\n}\n\nlet ScrollBar$1 = class ScrollBar {\n    direction;\n    el;\n    constructor(el, direction) {\n        this.el = el;\n        this.direction = direction;\n    }\n    get scroll() {\n        return this.direction === \"horizontal\" ? this.el.scrollLeft : this.el.scrollTop;\n    }\n    set scroll(value) {\n        if (this.direction === \"horizontal\") {\n            this.el.scrollLeft = value;\n        }\n        else {\n            this.el.scrollTop = value;\n        }\n    }\n};\n\ncss /* scss */ `\n  .o-scrollbar {\n    position: absolute;\n    overflow: auto;\n    z-index: ${ComponentsImportance.ScrollBar};\n    background-color: ${BACKGROUND_GRAY_COLOR};\n\n    &.corner {\n      right: 0px;\n      bottom: 0px;\n      height: ${SCROLLBAR_WIDTH}px;\n      width: ${SCROLLBAR_WIDTH}px;\n      border-top: 1px solid #e2e3e3;\n      border-left: 1px solid #e2e3e3;\n    }\n  }\n`;\nclass ScrollBar extends Component {\n    static props = {\n        width: { type: Number, optional: true },\n        height: { type: Number, optional: true },\n        direction: String,\n        position: Object,\n        offset: Number,\n        onScroll: Function,\n    };\n    static template = xml /*xml*/ `\n    <div\n        t-attf-class=\"o-scrollbar {{props.direction}}\"\n        t-on-scroll=\"onScroll\"\n        t-ref=\"scrollbar\"\n        t-att-style=\"positionCss\">\n      <div t-att-style=\"sizeCss\"/>\n    </div>\n  `;\n    static defaultProps = {\n        width: 1,\n        height: 1,\n    };\n    scrollbarRef;\n    scrollbar;\n    setup() {\n        this.scrollbarRef = useRef(\"scrollbar\");\n        this.scrollbar = new ScrollBar$1(this.scrollbarRef.el, this.props.direction);\n        onMounted(() => {\n            this.scrollbar.el = this.scrollbarRef.el;\n        });\n        // TODO improve useEffect dependencies typing in owl\n        useEffect(() => {\n            if (this.scrollbar.scroll !== this.props.offset) {\n                this.scrollbar.scroll = this.props.offset;\n            }\n        }, () => [this.scrollbar.scroll, this.props.offset]);\n    }\n    get sizeCss() {\n        return cssPropertiesToCss({\n            width: `${this.props.width}px`,\n            height: `${this.props.height}px`,\n        });\n    }\n    get positionCss() {\n        return cssPropertiesToCss(this.props.position);\n    }\n    onScroll(ev) {\n        if (this.props.offset !== this.scrollbar.scroll) {\n            this.props.onScroll(this.scrollbar.scroll);\n        }\n    }\n}\n\nclass HorizontalScrollBar extends Component {\n    static props = {\n        leftOffset: { type: Number, optional: true },\n    };\n    static components = { ScrollBar };\n    static template = xml /*xml*/ `\n      <ScrollBar\n        t-if=\"isDisplayed\"\n        width=\"width\"\n        position=\"position\"\n        offset=\"offset\"\n        direction=\"'horizontal'\"\n        onScroll.bind=\"onScroll\"\n      />`;\n    static defaultProps = {\n        leftOffset: 0,\n    };\n    get offset() {\n        return this.env.model.getters.getActiveSheetDOMScrollInfo().scrollX;\n    }\n    get width() {\n        return this.env.model.getters.getMainViewportRect().width;\n    }\n    get isDisplayed() {\n        const { xRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());\n        return xRatio < 1;\n    }\n    get position() {\n        const { x } = this.env.model.getters.getMainViewportRect();\n        return {\n            left: `${this.props.leftOffset + x}px`,\n            bottom: \"0px\",\n            height: `${SCROLLBAR_WIDTH}px`,\n            right: `0px`,\n        };\n    }\n    onScroll(offset) {\n        const { scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n        this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n            offsetX: offset,\n            offsetY: scrollY, // offsetY is the same\n        });\n    }\n}\n\nclass VerticalScrollBar extends Component {\n    static props = {\n        topOffset: { type: Number, optional: true },\n    };\n    static components = { ScrollBar };\n    static template = xml /*xml*/ `\n    <ScrollBar\n      t-if=\"isDisplayed\"\n      height=\"height\"\n      position=\"position\"\n      offset=\"offset\"\n      direction=\"'vertical'\"\n      onScroll.bind=\"onScroll\"\n    />`;\n    static defaultProps = {\n        topOffset: 0,\n    };\n    get offset() {\n        return this.env.model.getters.getActiveSheetDOMScrollInfo().scrollY;\n    }\n    get height() {\n        return this.env.model.getters.getMainViewportRect().height;\n    }\n    get isDisplayed() {\n        const { yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());\n        return yRatio < 1;\n    }\n    get position() {\n        const { y } = this.env.model.getters.getMainViewportRect();\n        return {\n            top: `${this.props.topOffset + y}px`,\n            right: \"0px\",\n            width: `${SCROLLBAR_WIDTH}px`,\n            bottom: `0px`,\n        };\n    }\n    onScroll(offset) {\n        const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n        this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n            offsetX: scrollX, // offsetX is the same\n            offsetY: offset,\n        });\n    }\n}\n\nconst DEFAULT_SIDE_PANEL_SIZE = 350;\nconst MIN_SHEET_VIEW_WIDTH = 150;\nclass SidePanelStore extends SpreadsheetStore {\n    mutators = [\"open\", \"toggle\", \"close\", \"changePanelSize\", \"resetPanelSize\"];\n    initialPanelProps = {};\n    componentTag = \"\";\n    panelSize = DEFAULT_SIDE_PANEL_SIZE;\n    get isOpen() {\n        if (!this.componentTag) {\n            return false;\n        }\n        return this.computeState(this.componentTag, this.initialPanelProps).isOpen;\n    }\n    get panelProps() {\n        const state = this.computeState(this.componentTag, this.initialPanelProps);\n        if (state.isOpen) {\n            return state.props ?? {};\n        }\n        return {};\n    }\n    get panelKey() {\n        const state = this.computeState(this.componentTag, this.initialPanelProps);\n        if (state.isOpen) {\n            return state.key;\n        }\n        return undefined;\n    }\n    open(componentTag, panelProps = {}) {\n        const state = this.computeState(componentTag, panelProps);\n        if (state.isOpen === false) {\n            return;\n        }\n        if (this.isOpen && componentTag !== this.componentTag) {\n            this.initialPanelProps?.onCloseSidePanel?.();\n        }\n        this.componentTag = componentTag;\n        this.initialPanelProps = state.props ?? {};\n    }\n    toggle(componentTag, panelProps) {\n        if (this.isOpen && componentTag === this.componentTag) {\n            this.close();\n        }\n        else {\n            this.open(componentTag, panelProps);\n        }\n    }\n    close() {\n        this.initialPanelProps.onCloseSidePanel?.();\n        this.initialPanelProps = {};\n        this.componentTag = \"\";\n    }\n    changePanelSize(size, spreadsheetElWidth) {\n        if (size < DEFAULT_SIDE_PANEL_SIZE) {\n            this.panelSize = DEFAULT_SIDE_PANEL_SIZE;\n        }\n        else if (size > spreadsheetElWidth - MIN_SHEET_VIEW_WIDTH) {\n            this.panelSize = Math.max(spreadsheetElWidth - MIN_SHEET_VIEW_WIDTH, DEFAULT_SIDE_PANEL_SIZE);\n        }\n        else {\n            this.panelSize = size;\n        }\n    }\n    resetPanelSize() {\n        this.panelSize = DEFAULT_SIDE_PANEL_SIZE;\n    }\n    computeState(componentTag, panelProps) {\n        const customComputeState = sidePanelRegistry.get(componentTag).computeState;\n        if (!customComputeState) {\n            return {\n                isOpen: true,\n                props: panelProps,\n            };\n        }\n        else {\n            return customComputeState(this.getters, panelProps);\n        }\n    }\n}\n\nconst SIZE = 3;\nconst COLOR = \"#777\";\ncss /* scss */ `\n  .o-table-resizer {\n    width: ${SIZE}px;\n    height: ${SIZE}px;\n    border-bottom: ${SIZE}px solid ${COLOR};\n    border-right: ${SIZE}px solid ${COLOR};\n    cursor: nwse-resize;\n  }\n`;\nclass TableResizer extends Component {\n    static template = \"o-spreadsheet-TableResizer\";\n    static props = { table: Object };\n    state = useState({ highlightZone: undefined });\n    setup() {\n        useHighlights(this);\n    }\n    get containerStyle() {\n        const tableZone = this.props.table.range.zone;\n        const bottomRight = { ...tableZone, left: tableZone.right, top: tableZone.bottom };\n        const rect = this.env.model.getters.getVisibleRect(bottomRight);\n        if (rect.height === 0 || rect.width === 0) {\n            return cssPropertiesToCss({ display: \"none\" });\n        }\n        return cssPropertiesToCss({\n            top: `${rect.y + rect.height - SIZE * 2}px`,\n            left: `${rect.x + rect.width - SIZE * 2}px`,\n        });\n    }\n    onMouseDown(ev) {\n        const tableZone = this.props.table.range.zone;\n        const topLeft = { col: tableZone.left, row: tableZone.top };\n        document.body.style.cursor = \"nwse-resize\";\n        const onMouseUp = () => {\n            document.body.style.cursor = \"\";\n            const newTableZone = this.state.highlightZone;\n            if (!newTableZone)\n                return;\n            const sheetId = this.props.table.range.sheetId;\n            this.env.model.dispatch(\"RESIZE_TABLE\", {\n                sheetId,\n                zone: this.props.table.range.zone,\n                newTableRange: this.env.model.getters.getRangeDataFromZone(sheetId, newTableZone),\n            });\n            this.state.highlightZone = undefined;\n        };\n        const onMouseMove = (col, row, ev) => {\n            this.state.highlightZone = {\n                left: topLeft.col,\n                top: topLeft.row,\n                right: Math.max(col, topLeft.col),\n                bottom: Math.max(row, topLeft.row),\n            };\n        };\n        dragAndDropBeyondTheViewport(this.env, onMouseMove, onMouseUp);\n    }\n    get highlights() {\n        if (!this.state.highlightZone)\n            return [];\n        return [\n            {\n                zone: this.state.highlightZone,\n                sheetId: this.props.table.range.sheetId,\n                color: COLOR,\n                noFill: true,\n            },\n        ];\n    }\n}\n\nconst registries$1 = {\n    ROW: rowMenuRegistry,\n    COL: colMenuRegistry,\n    CELL: cellMenuRegistry,\n    GROUP_HEADERS: groupHeadersMenuRegistry,\n    UNGROUP_HEADERS: unGroupHeadersMenuRegistry,\n};\n// -----------------------------------------------------------------------------\n// JS\n// -----------------------------------------------------------------------------\nclass Grid extends Component {\n    static template = \"o-spreadsheet-Grid\";\n    static props = {\n        exposeFocus: Function,\n    };\n    static components = {\n        GridComposer,\n        GridOverlay,\n        GridPopover,\n        HeadersOverlay,\n        Menu,\n        Autofill,\n        ClientTag,\n        Highlight,\n        Popover,\n        VerticalScrollBar,\n        HorizontalScrollBar,\n        TableResizer,\n    };\n    HEADER_HEIGHT = HEADER_HEIGHT;\n    HEADER_WIDTH = HEADER_WIDTH;\n    menuState;\n    gridRef;\n    highlightStore;\n    cellPopovers;\n    composerFocusStore;\n    DOMFocusableElementStore;\n    paintFormatStore;\n    onMouseWheel;\n    canvasPosition;\n    hoveredCell;\n    sidePanel;\n    setup() {\n        this.highlightStore = useStore(HighlightStore);\n        this.menuState = useState({\n            isOpen: false,\n            position: null,\n            menuItems: [],\n        });\n        this.gridRef = useRef(\"grid\");\n        this.canvasPosition = useAbsoluteBoundingRect(this.gridRef);\n        this.hoveredCell = useStore(HoveredCellStore);\n        this.composerFocusStore = useStore(ComposerFocusStore);\n        this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);\n        this.sidePanel = useStore(SidePanelStore);\n        this.paintFormatStore = useStore(PaintFormatStore);\n        useStore(ArrayFormulaHighlight);\n        useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });\n        useExternalListener(document.body, \"cut\", this.copy.bind(this, true));\n        useExternalListener(document.body, \"copy\", this.copy.bind(this, false));\n        useExternalListener(document.body, \"paste\", this.paste);\n        onMounted(() => this.focusDefaultElement());\n        this.props.exposeFocus(() => this.focusDefaultElement());\n        useGridDrawing(\"canvas\", this.env.model, () => this.env.model.getters.getSheetViewDimensionWithHeaders());\n        this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {\n            this.moveCanvas(deltaX, deltaY);\n            this.hoveredCell.clear();\n        });\n        this.cellPopovers = useStore(CellPopoverStore);\n        useEffect(() => {\n            if (!this.sidePanel.isOpen) {\n                this.DOMFocusableElementStore.focus();\n            }\n        }, () => [this.sidePanel.isOpen]);\n    }\n    onCellHovered({ col, row }) {\n        this.hoveredCell.hover({ col, row });\n    }\n    get highlights() {\n        return this.highlightStore.highlights;\n    }\n    get gridOverlayDimensions() {\n        return cssPropertiesToCss({\n            top: `${HEADER_HEIGHT}px`,\n            left: `${HEADER_WIDTH}px`,\n            height: `calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px)`,\n            width: `calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH}px)`,\n        });\n    }\n    onClosePopover() {\n        if (this.cellPopovers.isOpen) {\n            this.cellPopovers.close();\n        }\n        this.focusDefaultElement();\n    }\n    // this map will handle most of the actions that should happen on key down. The arrow keys are managed in the key\n    // down itself\n    keyDownMapping = {\n        Enter: () => {\n            const cell = this.env.model.getters.getActiveCell();\n            cell.type === CellValueType.empty\n                ? this.onComposerCellFocused()\n                : this.onComposerContentFocused();\n        },\n        Tab: () => this.env.model.selection.moveAnchorCell(\"right\", 1),\n        \"Shift+Tab\": () => this.env.model.selection.moveAnchorCell(\"left\", 1),\n        F2: () => {\n            const cell = this.env.model.getters.getActiveCell();\n            cell.type === CellValueType.empty\n                ? this.onComposerCellFocused()\n                : this.onComposerContentFocused();\n        },\n        Delete: () => {\n            this.env.model.dispatch(\"DELETE_CONTENT\", {\n                sheetId: this.env.model.getters.getActiveSheetId(),\n                target: this.env.model.getters.getSelectedZones(),\n            });\n        },\n        Backspace: () => {\n            this.env.model.dispatch(\"DELETE_CONTENT\", {\n                sheetId: this.env.model.getters.getActiveSheetId(),\n                target: this.env.model.getters.getSelectedZones(),\n            });\n        },\n        Escape: () => {\n            /** TODO: Clean once we introduce proper focus on sub components. Grid should not have to handle all this logic */\n            if (this.cellPopovers.isOpen) {\n                this.cellPopovers.close();\n            }\n            else if (this.menuState.isOpen) {\n                this.closeMenu();\n            }\n            else if (this.paintFormatStore.isActive) {\n                this.paintFormatStore.cancel();\n            }\n            else {\n                this.env.model.dispatch(\"CLEAN_CLIPBOARD_HIGHLIGHT\");\n            }\n        },\n        \"Ctrl+A\": () => this.env.model.selection.loopSelection(),\n        \"Ctrl+Z\": () => this.env.model.dispatch(\"REQUEST_UNDO\"),\n        \"Ctrl+Y\": () => this.env.model.dispatch(\"REQUEST_REDO\"),\n        F4: () => this.env.model.dispatch(\"REQUEST_REDO\"),\n        \"Ctrl+B\": () => this.env.model.dispatch(\"SET_FORMATTING\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            target: this.env.model.getters.getSelectedZones(),\n            style: { bold: !this.env.model.getters.getCurrentStyle().bold },\n        }),\n        \"Ctrl+I\": () => this.env.model.dispatch(\"SET_FORMATTING\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            target: this.env.model.getters.getSelectedZones(),\n            style: { italic: !this.env.model.getters.getCurrentStyle().italic },\n        }),\n        \"Ctrl+U\": () => this.env.model.dispatch(\"SET_FORMATTING\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            target: this.env.model.getters.getSelectedZones(),\n            style: { underline: !this.env.model.getters.getCurrentStyle().underline },\n        }),\n        \"Ctrl+O\": () => CREATE_IMAGE(this.env),\n        \"Alt+=\": () => {\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            const mainSelectedZone = this.env.model.getters.getSelectedZone();\n            const { anchor } = this.env.model.getters.getSelection();\n            const sums = this.env.model.getters.getAutomaticSums(sheetId, mainSelectedZone, anchor.cell);\n            if (this.env.model.getters.isSingleCellOrMerge(sheetId, mainSelectedZone) ||\n                (this.env.model.getters.isEmpty(sheetId, mainSelectedZone) && sums.length <= 1)) {\n                const zone = sums[0]?.zone;\n                const zoneXc = zone ? this.env.model.getters.zoneToXC(sheetId, sums[0].zone) : \"\";\n                const formula = `=SUM(${zoneXc})`;\n                this.onComposerCellFocused(formula, { start: 5, end: 5 + zoneXc.length });\n            }\n            else {\n                this.env.model.dispatch(\"SUM_SELECTION\");\n            }\n        },\n        \"Alt+Enter\": () => {\n            const cell = this.env.model.getters.getActiveCell();\n            if (cell.link) {\n                openLink(cell.link, this.env);\n            }\n        },\n        \"Ctrl+Home\": () => {\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            const { col, row } = this.env.model.getters.getNextVisibleCellPosition({\n                sheetId,\n                col: 0,\n                row: 0,\n            });\n            this.env.model.selection.selectCell(col, row);\n        },\n        \"Ctrl+End\": () => {\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            const col = this.env.model.getters.findVisibleHeader(sheetId, \"COL\", this.env.model.getters.getNumberCols(sheetId) - 1, 0);\n            const row = this.env.model.getters.findVisibleHeader(sheetId, \"ROW\", this.env.model.getters.getNumberRows(sheetId) - 1, 0);\n            this.env.model.selection.selectCell(col, row);\n        },\n        \"Shift+ \": () => {\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            const newZone = {\n                ...this.env.model.getters.getSelectedZone(),\n                left: 0,\n                right: this.env.model.getters.getNumberCols(sheetId) - 1,\n            };\n            const position = this.env.model.getters.getActivePosition();\n            this.env.model.selection.selectZone({ cell: position, zone: newZone });\n        },\n        \"Ctrl+ \": () => {\n            const sheetId = this.env.model.getters.getActiveSheetId();\n            const newZone = {\n                ...this.env.model.getters.getSelectedZone(),\n                top: 0,\n                bottom: this.env.model.getters.getNumberRows(sheetId) - 1,\n            };\n            const position = this.env.model.getters.getActivePosition();\n            this.env.model.selection.selectZone({ cell: position, zone: newZone });\n        },\n        \"Ctrl+D\": async () => this.env.model.dispatch(\"COPY_PASTE_CELLS_ABOVE\"),\n        \"Ctrl+R\": async () => this.env.model.dispatch(\"COPY_PASTE_CELLS_ON_LEFT\"),\n        \"Ctrl+Shift+E\": () => this.setHorizontalAlign(\"center\"),\n        \"Ctrl+Shift+L\": () => this.setHorizontalAlign(\"left\"),\n        \"Ctrl+Shift+R\": () => this.setHorizontalAlign(\"right\"),\n        \"Ctrl+Shift+V\": () => PASTE_AS_VALUE_ACTION(this.env),\n        \"Ctrl+Shift+<\": () => this.clearFormatting(), // for qwerty\n        \"Ctrl+<\": () => this.clearFormatting(), // for azerty\n        \"Ctrl+Shift+ \": () => {\n            this.env.model.selection.selectAll();\n        },\n        \"Ctrl+Alt+=\": () => {\n            const activeCols = this.env.model.getters.getActiveCols();\n            const activeRows = this.env.model.getters.getActiveRows();\n            const isSingleSelection = this.env.model.getters.getSelectedZones().length === 1;\n            const areFullCols = activeCols.size > 0 && isSingleSelection;\n            const areFullRows = activeRows.size > 0 && isSingleSelection;\n            if (areFullCols && !areFullRows) {\n                INSERT_COLUMNS_BEFORE_ACTION(this.env);\n            }\n            else if (areFullRows && !areFullCols) {\n                INSERT_ROWS_BEFORE_ACTION(this.env);\n            }\n        },\n        \"Ctrl+Alt+-\": () => {\n            const columns = [...this.env.model.getters.getActiveCols()];\n            const rows = [...this.env.model.getters.getActiveRows()];\n            if (columns.length > 0 && rows.length === 0) {\n                this.env.model.dispatch(\"REMOVE_COLUMNS_ROWS\", {\n                    sheetId: this.env.model.getters.getActiveSheetId(),\n                    dimension: \"COL\",\n                    elements: columns,\n                });\n            }\n            else if (rows.length > 0 && columns.length === 0) {\n                this.env.model.dispatch(\"REMOVE_COLUMNS_ROWS\", {\n                    sheetId: this.env.model.getters.getActiveSheetId(),\n                    dimension: \"ROW\",\n                    elements: rows,\n                });\n            }\n        },\n        \"Shift+PageDown\": () => {\n            this.env.model.dispatch(\"ACTIVATE_NEXT_SHEET\");\n        },\n        \"Shift+PageUp\": () => {\n            this.env.model.dispatch(\"ACTIVATE_PREVIOUS_SHEET\");\n        },\n        PageDown: () => this.env.model.dispatch(\"SHIFT_VIEWPORT_DOWN\"),\n        PageUp: () => this.env.model.dispatch(\"SHIFT_VIEWPORT_UP\"),\n        \"Ctrl+K\": () => INSERT_LINK(this.env),\n        \"Alt+Shift+ArrowRight\": () => this.processHeaderGroupingKey(\"right\"),\n        \"Alt+Shift+ArrowLeft\": () => this.processHeaderGroupingKey(\"left\"),\n        \"Alt+Shift+ArrowUp\": () => this.processHeaderGroupingKey(\"up\"),\n        \"Alt+Shift+ArrowDown\": () => this.processHeaderGroupingKey(\"down\"),\n    };\n    focusDefaultElement() {\n        if (!this.env.model.getters.getSelectedFigureId() &&\n            this.composerFocusStore.activeComposer.editionMode === \"inactive\") {\n            this.DOMFocusableElementStore.focus();\n        }\n    }\n    get gridEl() {\n        if (!this.gridRef.el) {\n            throw new Error(\"Grid el is not defined.\");\n        }\n        return this.gridRef.el;\n    }\n    getAutofillPosition() {\n        const zone = this.env.model.getters.getSelectedZone();\n        const rect = this.env.model.getters.getVisibleRect(zone);\n        return {\n            left: rect.x + rect.width - AUTOFILL_EDGE_LENGTH / 2,\n            top: rect.y + rect.height - AUTOFILL_EDGE_LENGTH / 2,\n        };\n    }\n    get isAutofillVisible() {\n        const zone = this.env.model.getters.getSelectedZone();\n        const rect = this.env.model.getters.getVisibleRect({\n            left: zone.right,\n            right: zone.right,\n            top: zone.bottom,\n            bottom: zone.bottom,\n        });\n        return !(rect.width === 0 || rect.height === 0);\n    }\n    onGridResized({ height, width }) {\n        this.env.model.dispatch(\"RESIZE_SHEETVIEW\", {\n            width: width,\n            height: height,\n            gridOffsetX: HEADER_WIDTH,\n            gridOffsetY: HEADER_HEIGHT,\n        });\n    }\n    moveCanvas(deltaX, deltaY) {\n        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n        this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n            offsetX: scrollX + deltaX,\n            offsetY: scrollY + deltaY,\n        });\n    }\n    getClientPositionKey(client) {\n        return `${client.id}-${client.position?.sheetId}-${client.position?.col}-${client.position?.row}`;\n    }\n    isCellHovered(col, row) {\n        return this.hoveredCell.col === col && this.hoveredCell.row === row;\n    }\n    getGridRect() {\n        return { ...this.canvasPosition, ...this.env.model.getters.getSheetViewDimensionWithHeaders() };\n    }\n    // ---------------------------------------------------------------------------\n    // Zone selection with mouse\n    // ---------------------------------------------------------------------------\n    onCellClicked(col, row, modifiers) {\n        if (this.composerFocusStore.activeComposer.editionMode === \"editing\") {\n            this.composerFocusStore.activeComposer.stopEdition();\n        }\n        if (modifiers.expandZone) {\n            this.env.model.selection.setAnchorCorner(col, row);\n        }\n        else if (modifiers.addZone) {\n            this.env.model.selection.addCellToSelection(col, row);\n        }\n        else {\n            this.env.model.selection.selectCell(col, row);\n        }\n        let prevCol = col;\n        let prevRow = row;\n        const onMouseMove = (col, row, ev) => {\n            // When selecting cells during the edition, we don't want to avoid the default\n            // browser behaviour that will select the text inside the composer\n            // (see related commit msg for more information)\n            ev.preventDefault();\n            if ((col !== prevCol && col != -1) || (row !== prevRow && row != -1)) {\n                prevCol = col === -1 ? prevCol : col;\n                prevRow = row === -1 ? prevRow : row;\n                this.env.model.selection.setAnchorCorner(prevCol, prevRow);\n            }\n        };\n        const onMouseUp = () => {\n            if (this.paintFormatStore.isActive) {\n                this.paintFormatStore.pasteFormat(this.env.model.getters.getSelectedZones());\n            }\n        };\n        dragAndDropBeyondTheViewport(this.env, onMouseMove, onMouseUp);\n    }\n    onCellDoubleClicked(col, row) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        ({ col, row } = this.env.model.getters.getMainCellPosition({ sheetId, col, row }));\n        const cell = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });\n        if (cell.type === CellValueType.empty) {\n            this.onComposerCellFocused();\n        }\n        else {\n            this.onComposerContentFocused();\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Keyboard interactions\n    // ---------------------------------------------------------------------------\n    processArrows(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        if (this.cellPopovers.isOpen) {\n            this.cellPopovers.close();\n        }\n        updateSelectionWithArrowKeys(ev, this.env.model.selection);\n        if (this.paintFormatStore.isActive) {\n            this.paintFormatStore.pasteFormat(this.env.model.getters.getSelectedZones());\n        }\n    }\n    onKeydown(ev) {\n        const keyDownString = keyboardEventToShortcutString(ev);\n        const handler = this.keyDownMapping[keyDownString];\n        if (handler) {\n            ev.preventDefault();\n            ev.stopPropagation();\n            handler();\n            return;\n        }\n        if (ev.key.startsWith(\"Arrow\")) {\n            this.processArrows(ev);\n            return;\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Context Menu\n    // ---------------------------------------------------------------------------\n    onInputContextMenu(ev) {\n        ev.preventDefault();\n        const lastZone = this.env.model.getters.getSelectedZone();\n        const { left: col, top: row } = lastZone;\n        let type = \"CELL\";\n        this.composerFocusStore.activeComposer.stopEdition();\n        if (this.env.model.getters.getActiveCols().has(col)) {\n            type = \"COL\";\n        }\n        else if (this.env.model.getters.getActiveRows().has(row)) {\n            type = \"ROW\";\n        }\n        const { x, y, width } = this.env.model.getters.getVisibleRect(lastZone);\n        const gridRect = this.getGridRect();\n        this.toggleContextMenu(type, gridRect.x + x + width, gridRect.y + y);\n    }\n    onCellRightClicked(col, row, { x, y }) {\n        const zones = this.env.model.getters.getSelectedZones();\n        const lastZone = zones[zones.length - 1];\n        let type = \"CELL\";\n        if (!isInside(col, row, lastZone)) {\n            this.env.model.selection.getBackToDefault();\n            this.env.model.selection.selectCell(col, row);\n        }\n        else {\n            if (this.env.model.getters.getActiveCols().has(col)) {\n                type = \"COL\";\n            }\n            else if (this.env.model.getters.getActiveRows().has(row)) {\n                type = \"ROW\";\n            }\n        }\n        this.toggleContextMenu(type, x, y);\n    }\n    toggleContextMenu(type, x, y) {\n        if (this.cellPopovers.isOpen) {\n            this.cellPopovers.close();\n        }\n        this.menuState.isOpen = true;\n        this.menuState.position = { x, y };\n        this.menuState.menuItems = registries$1[type].getMenuItems();\n    }\n    async copy(cut, ev) {\n        if (!this.gridEl.contains(document.activeElement)) {\n            return;\n        }\n        /* If we are currently editing a cell, let the default behavior */\n        if (this.composerFocusStore.activeComposer.editionMode !== \"inactive\") {\n            return;\n        }\n        if (cut) {\n            interactiveCut(this.env);\n        }\n        else {\n            this.env.model.dispatch(\"COPY\");\n        }\n        const content = this.env.model.getters.getClipboardContent();\n        const clipboardData = ev.clipboardData;\n        for (const type in content) {\n            clipboardData?.setData(type, content[type]);\n        }\n        ev.preventDefault();\n    }\n    async paste(ev) {\n        if (!this.gridEl.contains(document.activeElement)) {\n            return;\n        }\n        ev.preventDefault();\n        const clipboardData = ev.clipboardData;\n        if (!clipboardData) {\n            return;\n        }\n        const osClipboard = {\n            content: {\n                [ClipboardMIMEType.PlainText]: clipboardData?.getData(ClipboardMIMEType.PlainText),\n                [ClipboardMIMEType.Html]: clipboardData?.getData(ClipboardMIMEType.Html),\n            },\n        };\n        const target = this.env.model.getters.getSelectedZones();\n        const isCutOperation = this.env.model.getters.isCutOperation();\n        const clipboardContent = parseOSClipboardContent(osClipboard.content);\n        const clipboardId = clipboardContent.data?.clipboardId;\n        if (this.env.model.getters.getClipboardId() === clipboardId) {\n            interactivePaste(this.env, target);\n        }\n        else {\n            interactivePasteFromOS(this.env, target, clipboardContent);\n        }\n        if (isCutOperation) {\n            await this.env.clipboard.write({ [ClipboardMIMEType.PlainText]: \"\" });\n        }\n    }\n    clearFormatting() {\n        this.env.model.dispatch(\"CLEAR_FORMATTING\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            target: this.env.model.getters.getSelectedZones(),\n        });\n    }\n    setHorizontalAlign(align) {\n        this.env.model.dispatch(\"SET_FORMATTING\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            target: this.env.model.getters.getSelectedZones(),\n            style: { align },\n        });\n    }\n    closeMenu() {\n        this.menuState.isOpen = false;\n        this.focusDefaultElement();\n    }\n    processHeaderGroupingKey(direction) {\n        if (this.env.model.getters.getSelectedZones().length !== 1) {\n            return;\n        }\n        const selectingRows = this.env.model.getters.getActiveRows().size > 0;\n        const selectingCols = this.env.model.getters.getActiveCols().size > 0;\n        if (selectingCols && selectingRows) {\n            this.processHeaderGroupingEventOnWholeSheet(direction);\n        }\n        else if (selectingCols) {\n            this.processHeaderGroupingEventOnHeaders(direction, \"COL\");\n        }\n        else if (selectingRows) {\n            this.processHeaderGroupingEventOnHeaders(direction, \"ROW\");\n        }\n        else {\n            this.processHeaderGroupingEventOnGrid(direction);\n        }\n    }\n    processHeaderGroupingEventOnHeaders(direction, dimension) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const zone = this.env.model.getters.getSelectedZone();\n        const start = dimension === \"COL\" ? zone.left : zone.top;\n        const end = dimension === \"COL\" ? zone.right : zone.bottom;\n        switch (direction) {\n            case \"right\":\n                this.env.model.dispatch(\"GROUP_HEADERS\", { sheetId, dimension: dimension, start, end });\n                break;\n            case \"left\":\n                this.env.model.dispatch(\"UNGROUP_HEADERS\", { sheetId, dimension: dimension, start, end });\n                break;\n            case \"down\":\n                this.env.model.dispatch(\"UNFOLD_HEADER_GROUPS_IN_ZONE\", { sheetId, dimension, zone });\n                break;\n            case \"up\":\n                this.env.model.dispatch(\"FOLD_HEADER_GROUPS_IN_ZONE\", { sheetId, dimension, zone });\n                break;\n        }\n    }\n    processHeaderGroupingEventOnWholeSheet(direction) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        if (direction === \"up\") {\n            this.env.model.dispatch(\"FOLD_ALL_HEADER_GROUPS\", { sheetId, dimension: \"ROW\" });\n            this.env.model.dispatch(\"FOLD_ALL_HEADER_GROUPS\", { sheetId, dimension: \"COL\" });\n        }\n        else if (direction === \"down\") {\n            this.env.model.dispatch(\"UNFOLD_ALL_HEADER_GROUPS\", { sheetId, dimension: \"ROW\" });\n            this.env.model.dispatch(\"UNFOLD_ALL_HEADER_GROUPS\", { sheetId, dimension: \"COL\" });\n        }\n    }\n    processHeaderGroupingEventOnGrid(direction) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const zone = this.env.model.getters.getSelectedZone();\n        switch (direction) {\n            case \"down\":\n                this.env.model.dispatch(\"UNFOLD_HEADER_GROUPS_IN_ZONE\", {\n                    sheetId,\n                    dimension: \"ROW\",\n                    zone: zone,\n                });\n                this.env.model.dispatch(\"UNFOLD_HEADER_GROUPS_IN_ZONE\", {\n                    sheetId,\n                    dimension: \"COL\",\n                    zone: zone,\n                });\n                break;\n            case \"up\":\n                this.env.model.dispatch(\"FOLD_HEADER_GROUPS_IN_ZONE\", {\n                    sheetId,\n                    dimension: \"ROW\",\n                    zone: zone,\n                });\n                this.env.model.dispatch(\"FOLD_HEADER_GROUPS_IN_ZONE\", {\n                    sheetId,\n                    dimension: \"COL\",\n                    zone: zone,\n                });\n                break;\n            case \"right\": {\n                const { x, y, width } = this.env.model.getters.getVisibleRect(zone);\n                const gridRect = this.getGridRect();\n                this.toggleContextMenu(\"GROUP_HEADERS\", x + width + gridRect.x, y + gridRect.y);\n                break;\n            }\n            case \"left\": {\n                if (!canUngroupHeaders(this.env, \"COL\") && !canUngroupHeaders(this.env, \"ROW\")) {\n                    return;\n                }\n                const { x, y, width } = this.env.model.getters.getVisibleRect(zone);\n                const gridRect = this.getGridRect();\n                this.toggleContextMenu(\"UNGROUP_HEADERS\", x + width + gridRect.x, y + gridRect.y);\n                break;\n            }\n        }\n    }\n    onComposerCellFocused(content, selection) {\n        this.composerFocusStore.focusActiveComposer({ content, selection, focusMode: \"cellFocus\" });\n    }\n    onComposerContentFocused() {\n        this.composerFocusStore.focusActiveComposer({ focusMode: \"contentFocus\" });\n    }\n    get staticTables() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.env.model.getters.getCoreTables(sheetId).filter(isStaticTable);\n    }\n}\n\n/** @odoo-module */\nclass EditableName extends Component {\n    static template = \"o-spreadsheet-EditableName\";\n    static props = {\n        name: String,\n        displayName: String,\n        onChanged: Function,\n    };\n    state;\n    setup() {\n        this.state = useState({\n            isEditing: false,\n            name: \"\",\n        });\n    }\n    rename() {\n        this.state.isEditing = true;\n        this.state.name = this.props.name;\n    }\n    save() {\n        this.props.onChanged(this.state.name.trim());\n        this.state.isEditing = false;\n    }\n}\n\n/**\n * BasePlugin\n *\n * Since the spreadsheet internal state is quite complex, it is split into\n * multiple parts, each managing a specific concern.\n *\n * This file introduce the BasePlugin, which is the common class that defines\n * how each of these model sub parts should interact with each other.\n * There are two kind of plugins: core plugins handling persistent data\n * and UI plugins handling transient data.\n */\nclass BasePlugin {\n    static getters = [];\n    history;\n    dispatch;\n    canDispatch;\n    constructor(stateObserver, dispatch, canDispatch) {\n        this.history = Object.assign(Object.create(stateObserver), {\n            update: stateObserver.addChange.bind(stateObserver, this),\n            selectCell: () => { },\n        });\n        this.dispatch = dispatch;\n        this.canDispatch = canDispatch;\n    }\n    /**\n     * Export for excel should be available for all plugins, even for the UI.\n     * In some case, we need to export evaluated value, which is available from\n     * UI plugin only.\n     */\n    exportForExcel(data) { }\n    // ---------------------------------------------------------------------------\n    // Command handling\n    // ---------------------------------------------------------------------------\n    /**\n     * Before a command is accepted, the model will ask each plugin if the command\n     * is allowed.  If all of then return true, then we can proceed. Otherwise,\n     * the command is cancelled.\n     *\n     * There should not be any side effects in this method.\n     */\n    allowDispatch(command) {\n        return \"Success\" /* CommandResult.Success */;\n    }\n    /**\n     * This method is useful when a plugin need to perform some action before a\n     * command is handled in another plugin. This should only be used if it is not\n     * possible to do the work in the handle method.\n     */\n    beforeHandle(command) { }\n    /**\n     * This is the standard place to handle any command. Most of the plugin\n     * command handling work should take place here.\n     */\n    handle(command) { }\n    /**\n     * Sometimes, it is useful to perform some work after a command (and all its\n     * subcommands) has been completely handled.  For example, when we paste\n     * multiple cells, we only want to reevaluate the cell values once at the end.\n     */\n    finalize() { }\n    /**\n     * Combine multiple validation functions into a single function\n     * returning the list of result of every validation.\n     */\n    batchValidations(...validations) {\n        return (toValidate) => validations.map((validation) => validation.call(this, toValidate)).flat();\n    }\n    /**\n     * Combine multiple validation functions. Every validation is executed one after\n     * the other. As soon as one validation fails, it stops and the cancelled reason\n     * is returned.\n     */\n    chainValidations(...validations) {\n        return (toValidate) => {\n            for (const validation of validations) {\n                let results = validation.call(this, toValidate);\n                if (!Array.isArray(results)) {\n                    results = [results];\n                }\n                const cancelledReasons = results.filter((result) => result !== \"Success\" /* CommandResult.Success */);\n                if (cancelledReasons.length) {\n                    return cancelledReasons;\n                }\n            }\n            return \"Success\" /* CommandResult.Success */;\n        };\n    }\n    checkValidations(command, ...validations) {\n        return this.batchValidations(...validations)(command);\n    }\n}\n\n/**\n * Core plugins handle spreadsheet data.\n * They are responsible to import, export and maintain the spreadsheet\n * persisted state.\n * They should not be concerned about UI parts or transient state.\n */\nclass CorePlugin extends BasePlugin {\n    getters;\n    uuidGenerator;\n    constructor({ getters, stateObserver, range, dispatch, canDispatch, uuidGenerator, }) {\n        super(stateObserver, dispatch, canDispatch);\n        range.addRangeProvider(this.adaptRanges.bind(this));\n        this.getters = getters;\n        this.uuidGenerator = uuidGenerator;\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n    import(data) { }\n    export(data) { }\n    /**\n     * This method can be implemented in any plugin, to loop over the plugin's data structure and adapt the plugin's ranges.\n     * To adapt them, the implementation of the function must have a perfect knowledge of the data structure, thus\n     * implementing the loops over it makes sense in the plugin itself.\n     * When calling the method applyChange, the range will be adapted if necessary, then a copy will be returned along with\n     * the type of change that occurred.\n     *\n     * @param applyChange a function that, when called, will adapt the range according to the change on the grid\n     * @param sheetId an optional sheetId to adapt either range of that sheet specifically, or ranges pointing to that sheet\n     */\n    adaptRanges(applyChange, sheetId) { }\n    /**\n     * Implement this method to clean unused external resources, such as images\n     * stored on a server which have been deleted.\n     */\n    garbageCollectExternalResources() { }\n}\n\n/**\n * Formatting plugin.\n *\n * This plugin manages all things related to a cell look:\n * - borders\n */\nclass BordersPlugin extends CorePlugin {\n    static getters = [\"getCellBorder\", \"getBordersColors\"];\n    borders = {};\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"SET_BORDER\":\n                return this.checkBordersUnchanged(cmd);\n            default:\n                return \"Success\" /* CommandResult.Success */;\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_MERGE\":\n                for (const zone of cmd.target) {\n                    this.addBordersToMerge(cmd.sheetId, zone);\n                }\n                break;\n            case \"DUPLICATE_SHEET\":\n                const borders = this.borders[cmd.sheetId];\n                if (borders) {\n                    // borders is a sparse 2D array.\n                    // map and slice preserve empty values and do not set `undefined` instead\n                    const bordersCopy = borders\n                        .slice()\n                        .map((col) => col?.slice().map((border) => ({ ...border })));\n                    this.history.update(\"borders\", cmd.sheetIdTo, bordersCopy);\n                }\n                break;\n            case \"DELETE_SHEET\":\n                const allBorders = { ...this.borders };\n                delete allBorders[cmd.sheetId];\n                this.history.update(\"borders\", allBorders);\n                break;\n            case \"SET_BORDER\":\n                this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border);\n                break;\n            case \"SET_ZONE_BORDERS\":\n                if (cmd.border) {\n                    const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));\n                    this.setBorders(cmd.sheetId, target, cmd.border.position, cmd.border.color === \"\"\n                        ? undefined\n                        : {\n                            style: cmd.border.style || DEFAULT_BORDER_DESC.style,\n                            color: cmd.border.color || DEFAULT_BORDER_DESC.color,\n                        });\n                }\n                break;\n            case \"CLEAR_FORMATTING\":\n                this.clearBorders(cmd.sheetId, cmd.target);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\":\n                const elements = [...cmd.elements].sort((a, b) => b - a);\n                for (const group of groupConsecutive(elements)) {\n                    if (cmd.dimension === \"COL\") {\n                        this.shiftBordersHorizontally(cmd.sheetId, group[group.length - 1] + 1, -group.length);\n                    }\n                    else {\n                        this.shiftBordersVertically(cmd.sheetId, group[group.length - 1] + 1, -group.length);\n                    }\n                }\n                break;\n            case \"ADD_COLUMNS_ROWS\":\n                if (cmd.dimension === \"COL\") {\n                    this.handleAddColumns(cmd);\n                }\n                else {\n                    this.handleAddRows(cmd);\n                }\n                break;\n        }\n    }\n    /**\n     * Move borders according to the inserted columns.\n     * Ensure borders continuity.\n     */\n    handleAddColumns(cmd) {\n        // The new columns have already been inserted in the sheet at this point.\n        let colLeftOfInsertion;\n        let colRightOfInsertion;\n        if (cmd.position === \"before\") {\n            this.shiftBordersHorizontally(cmd.sheetId, cmd.base, cmd.quantity, {\n                moveFirstLeftBorder: true,\n            });\n            colLeftOfInsertion = cmd.base - 1;\n            colRightOfInsertion = cmd.base + cmd.quantity;\n        }\n        else {\n            this.shiftBordersHorizontally(cmd.sheetId, cmd.base + 1, cmd.quantity, {\n                moveFirstLeftBorder: false,\n            });\n            colLeftOfInsertion = cmd.base;\n            colRightOfInsertion = cmd.base + cmd.quantity + 1;\n        }\n        this.ensureColumnBorderContinuity(cmd.sheetId, colLeftOfInsertion, colRightOfInsertion);\n    }\n    /**\n     * Move borders according to the inserted rows.\n     * Ensure borders continuity.\n     */\n    handleAddRows(cmd) {\n        // The new rows have already been inserted at this point.\n        let rowAboveInsertion;\n        let rowBelowInsertion;\n        if (cmd.position === \"before\") {\n            this.shiftBordersVertically(cmd.sheetId, cmd.base, cmd.quantity, {\n                moveFirstTopBorder: true,\n            });\n            rowAboveInsertion = cmd.base - 1;\n            rowBelowInsertion = cmd.base + cmd.quantity;\n        }\n        else {\n            this.shiftBordersVertically(cmd.sheetId, cmd.base + 1, cmd.quantity, {\n                moveFirstTopBorder: false,\n            });\n            rowAboveInsertion = cmd.base;\n            rowBelowInsertion = cmd.base + cmd.quantity + 1;\n        }\n        this.ensureRowBorderContinuity(cmd.sheetId, rowAboveInsertion, rowBelowInsertion);\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getCellBorder({ sheetId, col, row }) {\n        const border = {\n            top: this.borders[sheetId]?.[col]?.[row]?.horizontal,\n            bottom: this.borders[sheetId]?.[col]?.[row + 1]?.horizontal,\n            left: this.borders[sheetId]?.[col]?.[row]?.vertical,\n            right: this.borders[sheetId]?.[col + 1]?.[row]?.vertical,\n        };\n        if (!border.bottom && !border.left && !border.right && !border.top) {\n            return null;\n        }\n        return border;\n    }\n    getBordersColors(sheetId) {\n        const colors = [];\n        const sheetBorders = this.borders[sheetId];\n        if (sheetBorders) {\n            for (const borders of sheetBorders.filter(isDefined)) {\n                for (const cellBorder of borders) {\n                    if (cellBorder?.horizontal) {\n                        colors.push(cellBorder.horizontal.color);\n                    }\n                    if (cellBorder?.vertical) {\n                        colors.push(cellBorder.vertical.color);\n                    }\n                }\n            }\n        }\n        return colors;\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    /**\n     * Ensure border continuity between two columns.\n     * If the two columns have the same borders (at each row respectively),\n     * the same borders are applied to each cell in between.\n     */\n    ensureColumnBorderContinuity(sheetId, leftColumn, rightColumn) {\n        const targetCols = range(leftColumn + 1, rightColumn);\n        for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {\n            const leftBorder = this.getCellBorder({ sheetId, col: leftColumn, row });\n            const rightBorder = this.getCellBorder({ sheetId, col: rightColumn, row });\n            if (leftBorder && rightBorder) {\n                const commonSides = this.getCommonSides(leftBorder, rightBorder);\n                for (let col of targetCols) {\n                    this.addBorder(sheetId, col, row, commonSides);\n                }\n            }\n        }\n    }\n    /**\n     * Ensure border continuity between two rows.\n     * If the two rows have the same borders (at each column respectively),\n     * the same borders are applied to each cell in between.\n     */\n    ensureRowBorderContinuity(sheetId, topRow, bottomRow) {\n        const targetRows = range(topRow + 1, bottomRow);\n        for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {\n            const aboveBorder = this.getCellBorder({ sheetId, col, row: topRow });\n            const belowBorder = this.getCellBorder({ sheetId, col, row: bottomRow });\n            if (aboveBorder && belowBorder) {\n                const commonSides = this.getCommonSides(aboveBorder, belowBorder);\n                for (let row of targetRows) {\n                    this.addBorder(sheetId, col, row, commonSides);\n                }\n            }\n        }\n    }\n    /**\n     * From two borders, return a new border with sides defined in both borders.\n     * i.e. the intersection of two borders.\n     */\n    getCommonSides(border1, border2) {\n        const commonBorder = {};\n        for (let side of [\"top\", \"bottom\", \"left\", \"right\"]) {\n            if (border1[side] && border1[side] === border2[side]) {\n                commonBorder[side] = border1[side];\n            }\n        }\n        return commonBorder;\n    }\n    /**\n     * Get all the columns which contains at least a border\n     */\n    getColumnsWithBorders(sheetId) {\n        const sheetBorders = this.borders[sheetId];\n        if (!sheetBorders)\n            return [];\n        return Object.keys(sheetBorders).map((index) => parseInt(index, 10));\n    }\n    /**\n     * Get all the rows which contains at least a border\n     */\n    getRowsWithBorders(sheetId) {\n        const sheetBorders = this.borders[sheetId]?.filter(isDefined);\n        if (!sheetBorders)\n            return [];\n        const rowsWithBorders = new Set();\n        for (const rowBorders of sheetBorders) {\n            for (const rowBorder in rowBorders) {\n                rowsWithBorders.add(parseInt(rowBorder, 10));\n            }\n        }\n        return Array.from(rowsWithBorders);\n    }\n    /**\n     * Get the range of all the rows in the sheet\n     */\n    getRowsRange(sheetId) {\n        const sheetBorders = this.borders[sheetId];\n        if (!sheetBorders)\n            return [];\n        return range(0, this.getters.getNumberRows(sheetId) + 1);\n    }\n    /**\n     * Move borders of a sheet horizontally.\n     * @param sheetId\n     * @param start starting column (included)\n     * @param delta how much borders will be moved (negative if moved to the left)\n     */\n    shiftBordersHorizontally(sheetId, start, delta, { moveFirstLeftBorder } = {}) {\n        const borders = this.borders[sheetId];\n        if (!borders)\n            return;\n        if (delta < 0) {\n            this.moveBordersOfColumn(sheetId, start, delta, \"vertical\", {\n                destructive: false,\n            });\n        }\n        this.getColumnsWithBorders(sheetId)\n            .filter((col) => col >= start)\n            .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up\n            .forEach((col) => {\n            if ((col === start && moveFirstLeftBorder) || col !== start) {\n                this.moveBordersOfColumn(sheetId, col, delta, \"vertical\");\n            }\n            this.moveBordersOfColumn(sheetId, col, delta, \"horizontal\");\n        });\n    }\n    /**\n     * Move borders of a sheet vertically.\n     * @param sheetId\n     * @param start starting row (included)\n     * @param delta how much borders will be moved (negative if moved to the above)\n     */\n    shiftBordersVertically(sheetId, start, delta, { moveFirstTopBorder } = {}) {\n        const borders = this.borders[sheetId];\n        if (!borders)\n            return;\n        if (delta < 0) {\n            this.moveBordersOfRow(sheetId, start, delta, \"horizontal\", {\n                destructive: false,\n            });\n        }\n        this.getRowsWithBorders(sheetId)\n            .filter((row) => row >= start)\n            .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up\n            .forEach((row) => {\n            if ((row === start && moveFirstTopBorder) || row !== start) {\n                this.moveBordersOfRow(sheetId, row, delta, \"horizontal\");\n            }\n            this.moveBordersOfRow(sheetId, row, delta, \"vertical\");\n        });\n    }\n    /**\n     * Moves the borders (left if `vertical` or top if `horizontal` depending on\n     * `borderDirection`) of all cells in an entire row `delta` rows to the right\n     * (`delta` > 0) or to the left (`delta` < 0).\n     * Note that as the left of a cell is the right of the cell-1, if the left is\n     * moved the right is also moved. However, if `horizontal`, the bottom border\n     * is not moved.\n     * It does it by replacing the target border by the moved border. If the\n     * argument `destructive` is given false, the target border is preserved if\n     * the moved border is empty\n     */\n    moveBordersOfRow(sheetId, row, delta, borderDirection, { destructive } = { destructive: true }) {\n        const borders = this.borders[sheetId];\n        if (!borders)\n            return;\n        this.getColumnsWithBorders(sheetId).forEach((col) => {\n            const targetBorder = borders[col]?.[row + delta]?.[borderDirection];\n            const movedBorder = borders[col]?.[row]?.[borderDirection];\n            this.history.update(\"borders\", sheetId, col, row + delta, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);\n            this.history.update(\"borders\", sheetId, col, row, borderDirection, undefined);\n        });\n    }\n    /**\n     * Moves the borders (left if `vertical` or top if `horizontal` depending on\n     * `borderDirection`) of all cells in an entire column `delta` columns below\n     * (`delta` > 0) or above (`delta` < 0).\n     * Note that as the top of a cell is the bottom of the cell-1, if the top is\n     * moved the bottom is also moved. However, if `vertical`, the right border\n     * is not moved.\n     * It does it by replacing the target border by the moved border. If the\n     * argument `destructive` is given false, the target border is preserved if\n     * the moved border is empty\n     */\n    moveBordersOfColumn(sheetId, col, delta, borderDirection, { destructive } = { destructive: true }) {\n        const borders = this.borders[sheetId];\n        if (!borders)\n            return;\n        this.getRowsRange(sheetId).forEach((row) => {\n            const targetBorder = borders[col + delta]?.[row]?.[borderDirection];\n            const movedBorder = borders[col]?.[row]?.[borderDirection];\n            this.history.update(\"borders\", sheetId, col + delta, row, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);\n            this.history.update(\"borders\", sheetId, col, row, borderDirection, undefined);\n        });\n    }\n    /**\n     * Set the borders of a cell.\n     * It overrides the current border if override === true.\n     */\n    setBorder(sheetId, col, row, border, override = true) {\n        if (override || !this.borders?.[sheetId]?.[col]?.[row]?.vertical) {\n            this.history.update(\"borders\", sheetId, col, row, \"vertical\", border?.left);\n        }\n        if (override || !this.borders?.[sheetId]?.[col]?.[row]?.horizontal) {\n            this.history.update(\"borders\", sheetId, col, row, \"horizontal\", border?.top);\n        }\n        if (override || !this.borders?.[sheetId]?.[col + 1]?.[row]?.vertical) {\n            this.history.update(\"borders\", sheetId, col + 1, row, \"vertical\", border?.right);\n        }\n        if (override || !this.borders?.[sheetId]?.[col]?.[row + 1]?.horizontal) {\n            this.history.update(\"borders\", sheetId, col, row + 1, \"horizontal\", border?.bottom);\n        }\n    }\n    /**\n     * Remove the borders of a zone\n     */\n    clearBorders(sheetId, zones) {\n        for (let zone of recomputeZones(zones)) {\n            for (let row = zone.top; row <= zone.bottom; row++) {\n                this.history.update(\"borders\", sheetId, zone.right + 1, row, \"vertical\", undefined);\n                for (let col = zone.left; col <= zone.right; col++) {\n                    this.history.update(\"borders\", sheetId, col, row, undefined);\n                }\n            }\n            for (let col = zone.left; col <= zone.right; col++) {\n                this.history.update(\"borders\", sheetId, col, zone.bottom + 1, \"horizontal\", undefined);\n            }\n        }\n    }\n    /**\n     * Add a border to the existing one to a cell\n     */\n    addBorder(sheetId, col, row, border) {\n        this.setBorder(sheetId, col, row, {\n            ...this.getCellBorder({ sheetId, col, row }),\n            ...border,\n        });\n    }\n    /**\n     * Set the borders of a zone by computing the borders to add from the given\n     * command\n     */\n    setBorders(sheetId, zones, position, border) {\n        if (position === \"clear\") {\n            return this.clearBorders(sheetId, zones);\n        }\n        for (let zone of recomputeZones(zones)) {\n            if (position === \"h\" || position === \"hv\" || position === \"all\") {\n                for (let row = zone.top + 1; row <= zone.bottom; row++) {\n                    for (let col = zone.left; col <= zone.right; col++) {\n                        this.addBorder(sheetId, col, row, { top: border });\n                    }\n                }\n            }\n            if (position === \"v\" || position === \"hv\" || position === \"all\") {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    for (let col = zone.left + 1; col <= zone.right; col++) {\n                        this.addBorder(sheetId, col, row, { left: border });\n                    }\n                }\n            }\n            if (position === \"left\" || position === \"all\" || position === \"external\") {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    this.addBorder(sheetId, zone.left, row, { left: border });\n                }\n            }\n            if (position === \"right\" || position === \"all\" || position === \"external\") {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    this.addBorder(sheetId, zone.right + 1, row, { left: border });\n                }\n            }\n            if (position === \"top\" || position === \"all\" || position === \"external\") {\n                for (let col = zone.left; col <= zone.right; col++) {\n                    this.addBorder(sheetId, col, zone.top, { top: border });\n                }\n            }\n            if (position === \"bottom\" || position === \"all\" || position === \"external\") {\n                for (let col = zone.left; col <= zone.right; col++) {\n                    this.addBorder(sheetId, col, zone.bottom + 1, { top: border });\n                }\n            }\n        }\n    }\n    /**\n     * Compute the borders to add to the given zone merged.\n     */\n    addBordersToMerge(sheetId, zone) {\n        const { left, right, top, bottom } = zone;\n        const bordersTopLeft = this.getCellBorder({ sheetId, col: left, row: top });\n        const bordersBottomRight = this.getCellBorder({ sheetId, col: right, row: bottom });\n        this.clearBorders(sheetId, [zone]);\n        if (bordersTopLeft?.top) {\n            this.setBorders(sheetId, [{ ...zone, bottom: top }], \"top\", bordersTopLeft.top);\n        }\n        if (bordersTopLeft?.left) {\n            this.setBorders(sheetId, [{ ...zone, right: left }], \"left\", bordersTopLeft.left);\n        }\n        if (bordersBottomRight?.bottom) {\n            this.setBorders(sheetId, [{ ...zone, top: bottom }], \"bottom\", bordersBottomRight.bottom);\n        }\n        else if (bordersTopLeft?.bottom) {\n            this.setBorders(sheetId, [{ ...zone, top: bottom }], \"bottom\", bordersTopLeft.bottom);\n        }\n        if (bordersBottomRight?.right) {\n            this.setBorders(sheetId, [{ ...zone, left: right }], \"right\", bordersBottomRight.right);\n        }\n        else if (bordersTopLeft?.right) {\n            this.setBorders(sheetId, [{ ...zone, left: right }], \"right\", bordersTopLeft.right);\n        }\n    }\n    checkBordersUnchanged(cmd) {\n        const currentBorder = this.getCellBorder(cmd);\n        const areAllNewBordersUndefined = !cmd.border?.bottom && !cmd.border?.left && !cmd.border?.right && !cmd.border?.top;\n        if ((!currentBorder && areAllNewBordersUndefined) || deepEquals(currentBorder, cmd.border)) {\n            return \"NoChanges\" /* CommandResult.NoChanges */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n    import(data) {\n        // Borders\n        if (Object.keys(data.borders || {}).length) {\n            for (let sheet of data.sheets) {\n                for (const zoneXc in sheet.borders) {\n                    const borderId = sheet.borders[zoneXc];\n                    const border = data.borders[borderId];\n                    const zone = toZone(zoneXc);\n                    for (let row = zone.top; row <= zone.bottom; row++) {\n                        for (let col = zone.left; col <= zone.right; col++) {\n                            this.setBorder(sheet.id, col, row, border, false);\n                        }\n                    }\n                }\n            }\n        }\n        // Merges\n        for (let sheetData of data.sheets) {\n            if (sheetData.merges) {\n                for (let merge of sheetData.merges) {\n                    this.addBordersToMerge(sheetData.id, toZone(merge));\n                }\n            }\n        }\n    }\n    export(data) {\n        const borders = {};\n        for (let sheet of data.sheets) {\n            const positionsByBorder = {};\n            for (let col = 0; col < sheet.colNumber; col++) {\n                for (let row = 0; row < sheet.rowNumber; row++) {\n                    const border = this.getCellBorder({ sheetId: sheet.id, col, row });\n                    if (border) {\n                        const borderId = getItemId(border, borders);\n                        const position = { sheetId: sheet.id, col, row };\n                        positionsByBorder[borderId] ??= [];\n                        positionsByBorder[borderId].push(position);\n                    }\n                }\n            }\n            sheet.borders = groupItemIdsByZones(positionsByBorder);\n        }\n        data.borders = borders;\n    }\n    exportForExcel(data) {\n        for (const sheet of data.sheets) {\n            for (let col = 0; col < sheet.colNumber; col++) {\n                for (let row = 0; row < sheet.rowNumber; row++) {\n                    const border = this.getCellBorder({ sheetId: sheet.id, col, row });\n                    if (border) {\n                        const xc = toXC(col, row);\n                        sheet.cells[xc] ??= {};\n                        sheet.cells[xc].border = getItemId(border, data.borders);\n                    }\n                }\n            }\n        }\n    }\n}\n\nclass PositionMap {\n    map = {};\n    set({ sheetId, col, row }, value) {\n        const map = this.map;\n        if (!map[sheetId]) {\n            map[sheetId] = {};\n        }\n        if (!map[sheetId][col]) {\n            map[sheetId][col] = {};\n        }\n        map[sheetId][col][row] = value;\n    }\n    get({ sheetId, col, row }) {\n        return this.map[sheetId]?.[col]?.[row];\n    }\n    getSheet(sheetId) {\n        return this.map[sheetId];\n    }\n    has({ sheetId, col, row }) {\n        return this.map[sheetId]?.[col]?.[row] !== undefined;\n    }\n    delete({ sheetId, col, row }) {\n        delete this.map[sheetId]?.[col]?.[row];\n    }\n    keys() {\n        const map = this.map;\n        const keys = [];\n        for (const sheetId in map) {\n            for (const col in map[sheetId]) {\n                for (const row in map[sheetId][col]) {\n                    keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });\n                }\n            }\n        }\n        return keys;\n    }\n    keysForSheet(sheetId) {\n        const map = this.map[sheetId];\n        if (!map) {\n            return [];\n        }\n        const keys = [];\n        for (const col in map) {\n            for (const row in map[col]) {\n                keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });\n            }\n        }\n        return keys;\n    }\n}\n\n/**\n * Core Plugin\n *\n * This is the most fundamental of all plugins. It defines how to interact with\n * cell and sheet content.\n */\nclass CellPlugin extends CorePlugin {\n    static getters = [\n        \"zoneToXC\",\n        \"getCells\",\n        \"getTranslatedCellFormula\",\n        \"getCellStyle\",\n        \"getCellById\",\n        \"getFormulaString\",\n        \"getFormulaMovedInSheet\",\n    ];\n    nextId = 1;\n    cells = {};\n    adaptRanges(applyChange, sheetId) {\n        for (const sheet of Object.keys(this.cells)) {\n            for (const cell of Object.values(this.cells[sheet] || {})) {\n                if (cell.isFormula) {\n                    for (const range of cell.compiledFormula.dependencies) {\n                        if (!sheetId || range.sheetId === sheetId) {\n                            const change = applyChange(range);\n                            if (change.changeType !== \"NONE\") {\n                                this.history.update(\"cells\", sheet, cell.id, \"compiledFormula\", \"dependencies\", cell.compiledFormula.dependencies.indexOf(range), change.range);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"UPDATE_CELL\":\n                return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessUpdateCell);\n            case \"CLEAR_CELL\":\n                return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessClearCell);\n            default:\n                return \"Success\" /* CommandResult.Success */;\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SET_FORMATTING\":\n                if (\"style\" in cmd) {\n                    this.setStyle(cmd.sheetId, cmd.target, cmd.style);\n                }\n                if (\"format\" in cmd && cmd.format !== undefined) {\n                    this.setFormatter(cmd.sheetId, cmd.target, cmd.format);\n                }\n                break;\n            case \"CLEAR_FORMATTING\":\n                this.clearFormatting(cmd.sheetId, cmd.target);\n                break;\n            case \"ADD_COLUMNS_ROWS\":\n                if (cmd.dimension === \"COL\") {\n                    this.handleAddColumnsRows(cmd, this.copyColumnStyle.bind(this));\n                }\n                else {\n                    this.handleAddColumnsRows(cmd, this.copyRowStyle.bind(this));\n                }\n                break;\n            case \"UPDATE_CELL\":\n                this.updateCell(cmd.sheetId, cmd.col, cmd.row, cmd);\n                break;\n            case \"CLEAR_CELL\":\n                this.dispatch(\"UPDATE_CELL\", {\n                    sheetId: cmd.sheetId,\n                    col: cmd.col,\n                    row: cmd.row,\n                    content: \"\",\n                    style: null,\n                    format: \"\",\n                });\n                break;\n            case \"CLEAR_CELLS\":\n                this.clearCells(cmd.sheetId, cmd.target);\n                break;\n            case \"DELETE_CONTENT\":\n                this.clearZones(cmd.sheetId, cmd.target);\n                break;\n        }\n    }\n    clearZones(sheetId, zones) {\n        for (let zone of recomputeZones(zones)) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    const cell = this.getters.getCell({ sheetId, col, row });\n                    if (cell) {\n                        this.dispatch(\"UPDATE_CELL\", {\n                            sheetId: sheetId,\n                            content: \"\",\n                            col,\n                            row,\n                        });\n                    }\n                }\n            }\n        }\n    }\n    /**\n     * Set a format to all the cells in a zone\n     */\n    setFormatter(sheetId, zones, format) {\n        for (let zone of recomputeZones(zones)) {\n            for (let row = zone.top; row <= zone.bottom; row++) {\n                for (let col = zone.left; col <= zone.right; col++) {\n                    this.dispatch(\"UPDATE_CELL\", {\n                        sheetId,\n                        col,\n                        row,\n                        format,\n                    });\n                }\n            }\n        }\n    }\n    /**\n     * Clear the styles and format of zones\n     */\n    clearFormatting(sheetId, zones) {\n        for (let zone of recomputeZones(zones)) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    // commandHelpers.updateCell(sheetId, col, row, { style: undefined});\n                    this.dispatch(\"UPDATE_CELL\", {\n                        sheetId,\n                        col,\n                        row,\n                        style: null,\n                        format: \"\",\n                    });\n                }\n            }\n        }\n    }\n    /**\n     * Clear the styles, the format and the content of zones\n     */\n    clearCells(sheetId, zones) {\n        for (const zone of zones) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    this.dispatch(\"UPDATE_CELL\", {\n                        sheetId: sheetId,\n                        col,\n                        row,\n                        content: \"\",\n                        style: null,\n                        format: \"\",\n                    });\n                }\n            }\n        }\n    }\n    /**\n     * Copy the style of the reference column/row to the new columns/rows.\n     */\n    handleAddColumnsRows(cmd, fn) {\n        // The new elements have already been inserted in the sheet at this point.\n        let insertedElements;\n        let styleReference;\n        if (cmd.position === \"before\") {\n            insertedElements = range(cmd.base, cmd.base + cmd.quantity);\n            styleReference = cmd.base + cmd.quantity;\n        }\n        else {\n            insertedElements = range(cmd.base + 1, cmd.base + cmd.quantity + 1);\n            styleReference = cmd.base;\n        }\n        fn(cmd.sheetId, styleReference, insertedElements);\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n    import(data) {\n        for (let sheet of data.sheets) {\n            const sheetId = sheet.id;\n            const cellsData = new PositionMap();\n            // cells content\n            for (const xc in sheet.cells) {\n                if (sheet.cells[xc]?.content) {\n                    const { col, row } = toCartesian(xc);\n                    const position = { sheetId: sheet.id, col, row };\n                    cellsData.set(position, { content: sheet.cells[xc]?.content });\n                }\n            }\n            // cells style and format\n            for (const [cellProperty, valuesByZones] of [\n                [\"style\", sheet.styles],\n                [\"format\", sheet.formats],\n            ]) {\n                for (const zoneXc in valuesByZones) {\n                    const itemId = valuesByZones[zoneXc];\n                    const zone = toZone(zoneXc);\n                    for (let row = zone.top; row <= zone.bottom; row++) {\n                        for (let col = zone.left; col <= zone.right; col++) {\n                            const position = { sheetId, col, row };\n                            const cellData = cellsData.get(position);\n                            if (cellData) {\n                                cellData[cellProperty] = itemId;\n                            }\n                            else {\n                                cellsData.set(position, { [cellProperty]: itemId });\n                            }\n                        }\n                    }\n                }\n            }\n            for (const position of cellsData.keysForSheet(sheetId)) {\n                const cellData = cellsData.get(position);\n                if (cellData?.content || cellData?.format || cellData?.style) {\n                    const cell = this.importCell(sheet.id, cellData?.content, cellData?.style ? data.styles[cellData?.style] : undefined, cellData?.format ? data.formats[cellData?.format] : undefined);\n                    this.history.update(\"cells\", sheet.id, cell.id, cell);\n                    this.dispatch(\"UPDATE_CELL_POSITION\", {\n                        cellId: cell.id,\n                        ...position,\n                    });\n                }\n            }\n        }\n    }\n    export(data) {\n        const styles = {};\n        const formats = {};\n        for (let _sheet of data.sheets) {\n            const positionsByStyle = [];\n            const positionsByFormat = [];\n            const cells = {};\n            const positions = Object.keys(this.cells[_sheet.id] || {})\n                .map((cellId) => this.getters.getCellPosition(cellId))\n                .sort((a, b) => (a.col === b.col ? a.row - b.row : a.col - b.col));\n            for (const position of positions) {\n                const cell = this.getters.getCell(position);\n                const xc = toXC(position.col, position.row);\n                const style = this.removeDefaultStyleValues(cell.style);\n                if (Object.keys(style).length) {\n                    const styleId = getItemId(style, styles);\n                    positionsByStyle[styleId] ??= [];\n                    positionsByStyle[styleId].push(position);\n                }\n                if (cell.format) {\n                    const formatId = getItemId(cell.format, formats);\n                    positionsByFormat[formatId] ??= [];\n                    positionsByFormat[formatId].push(position);\n                }\n                if (cell.content) {\n                    cells[xc] = {\n                        content: cell.content,\n                    };\n                }\n            }\n            _sheet.styles = groupItemIdsByZones(positionsByStyle);\n            _sheet.formats = groupItemIdsByZones(positionsByFormat);\n            _sheet.cells = cells;\n        }\n        data.styles = styles;\n        data.formats = formats;\n    }\n    importCell(sheetId, content, style, format) {\n        const cellId = this.getNextUid();\n        return this.createCell(cellId, content || \"\", format, style, sheetId);\n    }\n    exportForExcel(data) {\n        this.export(data);\n        for (const sheet of data.sheets) {\n            for (const cellId in this.getters.getCells(sheet.id)) {\n                const { col, row } = this.getters.getCellPosition(cellId);\n                const xc = toXC(col, row);\n                const cell = this.getCellById(cellId);\n                sheet.cells[xc] ??= {};\n                if (cell?.format) {\n                    sheet.cells[xc].format = getItemId(cell.format, data.formats);\n                }\n                if (cell?.style) {\n                    sheet.cells[xc] ??= {};\n                    sheet.cells[xc].style = getItemId(this.removeDefaultStyleValues(cell.style), data.styles);\n                }\n            }\n        }\n        const incompatible = [];\n        for (const formatId in data.formats || []) {\n            if (!isExcelCompatible(data.formats[formatId])) {\n                incompatible.push(formatId);\n                delete data.formats[formatId];\n            }\n        }\n        if (incompatible.length) {\n            for (const sheet of data.sheets) {\n                for (const xc in sheet.cells) {\n                    const cell = sheet.cells[xc];\n                    const format = cell?.format;\n                    if (format && incompatible.includes(format.toString())) {\n                        delete cell.format;\n                    }\n                }\n            }\n        }\n    }\n    removeDefaultStyleValues(style) {\n        const cleanedStyle = { ...style };\n        for (const property in DEFAULT_STYLE) {\n            if (cleanedStyle[property] === DEFAULT_STYLE[property]) {\n                delete cleanedStyle[property];\n            }\n        }\n        return cleanedStyle;\n    }\n    // ---------------------------------------------------------------------------\n    // GETTERS\n    // ---------------------------------------------------------------------------\n    getCells(sheetId) {\n        return this.cells[sheetId] || {};\n    }\n    /**\n     * get a cell by ID. Used in evaluation when evaluating an async cell, we need to be able to find it back after\n     * starting an async evaluation even if it has been moved or re-allocated\n     */\n    getCellById(cellId) {\n        // this must be as fast as possible\n        const position = this.getters.getCellPosition(cellId);\n        const sheet = this.cells[position.sheetId];\n        return sheet[cellId];\n    }\n    /*\n     * Reconstructs the original formula string based on new dependencies\n     */\n    getFormulaString(sheetId, tokens, dependencies, useFixedReference = false) {\n        if (!dependencies.length) {\n            return concat(tokens.map((token) => token.value));\n        }\n        let rangeIndex = 0;\n        return concat(tokens.map((token) => {\n            if (token.type === \"REFERENCE\") {\n                const range = dependencies[rangeIndex++];\n                return this.getters.getRangeString(range, sheetId, { useFixedReference });\n            }\n            return token.value;\n        }));\n    }\n    /*\n     * Constructs a formula string based on an initial formula and a translation vector\n     */\n    getTranslatedCellFormula(sheetId, offsetX, offsetY, tokens) {\n        const adaptedDependencies = this.getters.createAdaptedRanges(compileTokens(tokens).dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)), offsetX, offsetY, sheetId);\n        return this.getFormulaString(sheetId, tokens, adaptedDependencies);\n    }\n    getFormulaMovedInSheet(originSheetId, targetSheetId, tokens) {\n        const dependencies = compileTokens(tokens).dependencies.map((d) => this.getters.getRangeFromSheetXC(originSheetId, d));\n        const adaptedDependencies = this.getters.removeRangesSheetPrefix(targetSheetId, dependencies);\n        return this.getFormulaString(targetSheetId, tokens, adaptedDependencies);\n    }\n    getCellStyle(position) {\n        return this.getters.getCell(position)?.style || {};\n    }\n    /**\n     * Converts a zone to a XC coordinate system\n     *\n     * The conversion also treats merges as one single cell\n     *\n     * Examples:\n     * {top:0,left:0,right:0,bottom:0} ==> A1\n     * {top:0,left:0,right:1,bottom:1} ==> A1:B2\n     *\n     * if A1:B2 is a merge:\n     * {top:0,left:0,right:1,bottom:1} ==> A1\n     * {top:1,left:0,right:1,bottom:2} ==> A1:B3\n     *\n     * if A1:B2 and A4:B5 are merges:\n     * {top:1,left:0,right:1,bottom:3} ==> A1:A5\n     */\n    zoneToXC(sheetId, zone, fixedParts = [{ colFixed: false, rowFixed: false }]) {\n        zone = this.getters.expandZone(sheetId, zone);\n        const topLeft = toXC(zone.left, zone.top, fixedParts[0]);\n        const botRight = toXC(zone.right, zone.bottom, fixedParts.length > 1 ? fixedParts[1] : fixedParts[0]);\n        const cellTopLeft = this.getters.getMainCellPosition({\n            sheetId,\n            col: zone.left,\n            row: zone.top,\n        });\n        const cellBotRight = this.getters.getMainCellPosition({\n            sheetId,\n            col: zone.right,\n            row: zone.bottom,\n        });\n        const sameCell = cellTopLeft.col === cellBotRight.col && cellTopLeft.row === cellBotRight.row;\n        if (topLeft != botRight && !sameCell) {\n            return topLeft + \":\" + botRight;\n        }\n        return topLeft;\n    }\n    setStyle(sheetId, target, style) {\n        for (let zone of recomputeZones(target)) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    const cell = this.getters.getCell({ sheetId, col, row });\n                    this.dispatch(\"UPDATE_CELL\", {\n                        sheetId,\n                        col,\n                        row,\n                        style: style ? { ...cell?.style, ...style } : undefined,\n                    });\n                }\n            }\n        }\n    }\n    /**\n     * Copy the style of one column to other columns.\n     */\n    copyColumnStyle(sheetId, refColumn, targetCols) {\n        for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {\n            const format = this.getFormat(sheetId, refColumn, row);\n            if (format.style || format.format) {\n                for (let col of targetCols) {\n                    this.dispatch(\"UPDATE_CELL\", { sheetId, col, row, ...format });\n                }\n            }\n        }\n    }\n    /**\n     * Copy the style of one row to other rows.\n     */\n    copyRowStyle(sheetId, refRow, targetRows) {\n        for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {\n            const format = this.getFormat(sheetId, col, refRow);\n            if (format.style || format.format) {\n                for (let row of targetRows) {\n                    this.dispatch(\"UPDATE_CELL\", { sheetId, col, row, ...format });\n                }\n            }\n        }\n    }\n    /**\n     * gets the currently used style/border of a cell based on it's coordinates\n     */\n    getFormat(sheetId, col, row) {\n        const format = {};\n        const position = this.getters.getMainCellPosition({ sheetId, col, row });\n        const cell = this.getters.getCell(position);\n        if (cell) {\n            if (cell.style) {\n                format[\"style\"] = cell.style;\n            }\n            if (cell.format) {\n                format[\"format\"] = cell.format;\n            }\n        }\n        return format;\n    }\n    getNextUid() {\n        const id = this.nextId.toString();\n        this.history.update(\"nextId\", this.nextId + 1);\n        return id;\n    }\n    updateCell(sheetId, col, row, after) {\n        const before = this.getters.getCell({ sheetId, col, row });\n        const hasContent = \"content\" in after || \"formula\" in after;\n        // Compute the new cell properties\n        const afterContent = hasContent ? replaceNewLines(after?.content) : before?.content || \"\";\n        let style;\n        if (after.style !== undefined) {\n            style = after.style || undefined;\n        }\n        else {\n            style = before ? before.style : undefined;\n        }\n        const format = \"format\" in after ? after.format : before && before.format;\n        /* Read the following IF as:\n         * we need to remove the cell if it is completely empty, but we can know if it completely empty if:\n         * - the command says the new content is empty and has no border/format/style\n         * - the command has no content property, in this case\n         *     - either there wasn't a cell at this place and the command says border/format/style is empty\n         *     - or there was a cell at this place, but it's an empty cell and the command says border/format/style is empty\n         *  */\n        if (((hasContent && !afterContent && !after.formula) ||\n            (!hasContent && (!before || before.content === \"\"))) &&\n            !style &&\n            !format) {\n            if (before) {\n                this.history.update(\"cells\", sheetId, before.id, undefined);\n                this.dispatch(\"UPDATE_CELL_POSITION\", {\n                    cellId: undefined,\n                    col,\n                    row,\n                    sheetId,\n                });\n            }\n            return;\n        }\n        const cellId = before?.id || this.getNextUid();\n        const cell = this.createCell(cellId, afterContent, format, style, sheetId);\n        this.history.update(\"cells\", sheetId, cell.id, cell);\n        this.dispatch(\"UPDATE_CELL_POSITION\", { cellId: cell.id, col, row, sheetId });\n    }\n    createCell(id, content, format, style, sheetId) {\n        if (!content.startsWith(\"=\")) {\n            return this.createLiteralCell(id, content, format, style);\n        }\n        return this.createFormulaCell(id, content, format, style, sheetId);\n    }\n    createLiteralCell(id, content, format, style) {\n        const locale = this.getters.getLocale();\n        const parsedValue = parseLiteral(content, locale);\n        format =\n            format ||\n                (typeof parsedValue === \"number\"\n                    ? detectDateFormat(content, locale) || detectNumberFormat(content)\n                    : undefined);\n        if (!isTextFormat(format) && !isEvaluationError(content)) {\n            content = toString(parsedValue);\n        }\n        return {\n            id,\n            content,\n            style,\n            format,\n            isFormula: false,\n            parsedValue,\n        };\n    }\n    createFormulaCell(id, content, format, style, sheetId) {\n        const compiledFormula = compile(content);\n        if (compiledFormula.dependencies.length) {\n            return this.createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId);\n        }\n        return {\n            id,\n            content,\n            style,\n            format,\n            isFormula: true,\n            compiledFormula: {\n                ...compiledFormula,\n                dependencies: [],\n            },\n        };\n    }\n    /**\n     * Create a new formula cell with the content\n     * being a computed property to rebuild the dependencies XC.\n     */\n    createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId) {\n        const dependencies = [];\n        for (const xc of compiledFormula.dependencies) {\n            dependencies.push(this.getters.getRangeFromSheetXC(sheetId, xc));\n        }\n        return new FormulaCellWithDependencies(id, compiledFormula, format, style, dependencies, sheetId, this.getters.getRangeString);\n    }\n    checkCellOutOfSheet(cmd) {\n        const { sheetId, col, row } = cmd;\n        const sheet = this.getters.tryGetSheet(sheetId);\n        if (!sheet)\n            return \"InvalidSheetId\" /* CommandResult.InvalidSheetId */;\n        const sheetZone = this.getters.getSheetZone(sheetId);\n        return isInside(col, row, sheetZone) ? \"Success\" /* CommandResult.Success */ : \"TargetOutOfSheet\" /* CommandResult.TargetOutOfSheet */;\n    }\n    checkUselessClearCell(cmd) {\n        const cell = this.getters.getCell(cmd);\n        if (!cell)\n            return \"NoChanges\" /* CommandResult.NoChanges */;\n        if (!cell.content && !cell.style && !cell.format) {\n            return \"NoChanges\" /* CommandResult.NoChanges */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkUselessUpdateCell(cmd) {\n        const cell = this.getters.getCell(cmd);\n        const hasContent = \"content\" in cmd || \"formula\" in cmd;\n        const hasStyle = \"style\" in cmd;\n        const hasFormat = \"format\" in cmd;\n        if ((!hasContent || cell?.content === cmd.content) &&\n            (!hasStyle || deepEquals(cell?.style, cmd.style)) &&\n            (!hasFormat || cell?.format === cmd.format)) {\n            return \"NoChanges\" /* CommandResult.NoChanges */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n}\nclass FormulaCellWithDependencies {\n    id;\n    format;\n    style;\n    sheetId;\n    getRangeString;\n    isFormula = true;\n    compiledFormula;\n    constructor(id, compiledFormula, format, style, dependencies, sheetId, getRangeString) {\n        this.id = id;\n        this.format = format;\n        this.style = style;\n        this.sheetId = sheetId;\n        this.getRangeString = getRangeString;\n        let rangeIndex = 0;\n        const tokens = compiledFormula.tokens.map((token) => {\n            if (token.type === \"REFERENCE\") {\n                const index = rangeIndex++;\n                return new RangeReferenceToken(dependencies, index, this.sheetId, this.getRangeString);\n            }\n            return token;\n        });\n        this.compiledFormula = {\n            ...compiledFormula,\n            dependencies,\n            tokens,\n        };\n    }\n    get content() {\n        return concat(this.compiledFormula.tokens.map((token) => token.value));\n    }\n    get contentWithFixedReferences() {\n        let rangeIndex = 0;\n        return concat(this.compiledFormula.tokens.map((token) => {\n            if (token.type === \"REFERENCE\") {\n                const index = rangeIndex++;\n                return this.getRangeString(this.compiledFormula.dependencies[index], this.sheetId, {\n                    useFixedReference: true,\n                });\n            }\n            return token.value;\n        }));\n    }\n}\nclass RangeReferenceToken {\n    ranges;\n    rangeIndex;\n    sheetId;\n    getRangeString;\n    type = \"REFERENCE\";\n    constructor(ranges, rangeIndex, sheetId, getRangeString) {\n        this.ranges = ranges;\n        this.rangeIndex = rangeIndex;\n        this.sheetId = sheetId;\n        this.getRangeString = getRangeString;\n    }\n    get value() {\n        const range = this.ranges[this.rangeIndex];\n        return this.getRangeString(range, this.sheetId);\n    }\n}\n\nclass ChartPlugin extends CorePlugin {\n    static getters = [\n        \"isChartDefined\",\n        \"getChartDefinition\",\n        \"getChartType\",\n        \"getChartIds\",\n        \"getChart\",\n        \"getContextCreationChart\",\n    ];\n    charts = {};\n    createChart = chartFactory(this.getters);\n    validateChartDefinition = (cmd) => validateChartDefinition(this, cmd.definition);\n    adaptRanges(applyChange) {\n        for (const [chartId, chart] of Object.entries(this.charts)) {\n            this.history.update(\"charts\", chartId, chart?.updateRanges(applyChange));\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_CHART\":\n                return this.checkValidations(cmd, this.chainValidations(this.validateChartDefinition, this.checkChartDuplicate));\n            case \"UPDATE_CHART\":\n                return this.checkValidations(cmd, this.chainValidations(this.validateChartDefinition, this.checkChartExists));\n            default:\n                return \"Success\" /* CommandResult.Success */;\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_CHART\":\n                this.addFigure(cmd.id, cmd.sheetId, cmd.position, cmd.size);\n                this.addChart(cmd.id, cmd.definition);\n                break;\n            case \"UPDATE_CHART\": {\n                this.addChart(cmd.id, cmd.definition);\n                break;\n            }\n            case \"DUPLICATE_SHEET\": {\n                const sheetFiguresFrom = this.getters.getFigures(cmd.sheetId);\n                for (const fig of sheetFiguresFrom) {\n                    if (fig.tag === \"chart\") {\n                        const figureIdBase = fig.id.split(FIGURE_ID_SPLITTER).pop();\n                        const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;\n                        const chart = this.charts[fig.id]?.copyForSheetId(cmd.sheetIdTo);\n                        if (chart) {\n                            this.dispatch(\"CREATE_CHART\", {\n                                id: duplicatedFigureId,\n                                position: { x: fig.x, y: fig.y },\n                                size: { width: fig.width, height: fig.height },\n                                definition: chart.getDefinition(),\n                                sheetId: cmd.sheetIdTo,\n                            });\n                        }\n                    }\n                }\n                break;\n            }\n            case \"DELETE_FIGURE\":\n                this.history.update(\"charts\", cmd.id, undefined);\n                break;\n            case \"DELETE_SHEET\":\n                for (let id of this.getChartIds(cmd.sheetId)) {\n                    this.history.update(\"charts\", id, undefined);\n                }\n                break;\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getContextCreationChart(figureId) {\n        return this.charts[figureId]?.getContextCreation();\n    }\n    getChart(figureId) {\n        return this.charts[figureId];\n    }\n    getChartType(figureId) {\n        const type = this.charts[figureId]?.type;\n        if (!type) {\n            throw new Error(\"Chart not defined.\");\n        }\n        return type;\n    }\n    isChartDefined(figureId) {\n        return figureId in this.charts && this.charts !== undefined;\n    }\n    getChartIds(sheetId) {\n        return Object.entries(this.charts)\n            .filter(([, chart]) => chart?.sheetId === sheetId)\n            .map(([id]) => id);\n    }\n    getChartDefinition(figureId) {\n        const definition = this.charts[figureId]?.getDefinition();\n        if (!definition) {\n            throw new Error(`There is no chart with the given figureId: ${figureId}`);\n        }\n        return definition;\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n    import(data) {\n        for (let sheet of data.sheets) {\n            if (sheet.figures) {\n                for (let figure of sheet.figures) {\n                    // TODO:\n                    // figure data should be external IMO => chart should be in sheet.chart\n                    // instead of in figure.data\n                    if (figure.tag === \"chart\") {\n                        this.charts[figure.id] = this.createChart(figure.id, figure.data, sheet.id);\n                    }\n                }\n            }\n        }\n    }\n    export(data) {\n        if (data.sheets) {\n            for (let sheet of data.sheets) {\n                // TODO This code is false, if two plugins want to insert figures on the sheet, it will crash !\n                const sheetFigures = this.getters.getFigures(sheet.id);\n                const figures = [];\n                for (let sheetFigure of sheetFigures) {\n                    const figure = sheetFigure;\n                    if (figure && figure.tag === \"chart\") {\n                        const data = this.charts[figure.id]?.getDefinition();\n                        if (data) {\n                            figure.data = data;\n                            figures.push(figure);\n                        }\n                    }\n                    else {\n                        figures.push(figure);\n                    }\n                }\n                sheet.figures = figures;\n            }\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    /**\n     * Add a figure with tag chart with the given id at the given position\n     */\n    addFigure(id, sheetId, position = { x: 0, y: 0 }, size = {\n        width: DEFAULT_FIGURE_WIDTH,\n        height: DEFAULT_FIGURE_HEIGHT,\n    }) {\n        if (this.getters.getFigure(sheetId, id)) {\n            return;\n        }\n        const figure = {\n            id,\n            x: position.x,\n            y: position.y,\n            width: size.width,\n            height: size.height,\n            tag: \"chart\",\n        };\n        this.dispatch(\"CREATE_FIGURE\", { sheetId, figure });\n    }\n    /**\n     * Add a chart in the local state. If a chart already exists, this chart is\n     * replaced\n     */\n    addChart(id, definition) {\n        const sheetId = this.getters.getFigureSheetId(id);\n        if (sheetId) {\n            this.history.update(\"charts\", id, this.createChart(id, definition, sheetId));\n        }\n    }\n    checkChartDuplicate(cmd) {\n        return this.getters.getFigureSheetId(cmd.id)\n            ? \"DuplicatedChartId\" /* CommandResult.DuplicatedChartId */\n            : \"Success\" /* CommandResult.Success */;\n    }\n    checkChartExists(cmd) {\n        return this.getters.getFigureSheetId(cmd.id)\n            ? \"Success\" /* CommandResult.Success */\n            : \"ChartDoesNotExist\" /* CommandResult.ChartDoesNotExist */;\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Constants\n// -----------------------------------------------------------------------------\nfunction stringToNumber(value) {\n    return value === \"\" ? NaN : Number(value);\n}\nclass ConditionalFormatPlugin extends CorePlugin {\n    static getters = [\n        \"getConditionalFormats\",\n        \"getRulesSelection\",\n        \"getRulesByCell\",\n        \"getAdaptedCfRanges\",\n    ];\n    cfRules = {};\n    loopThroughRangesOfSheet(sheetId, applyChange) {\n        for (const rule of this.cfRules[sheetId]) {\n            if (rule.rule.type === \"DataBarRule\" && rule.rule.rangeValues) {\n                const change = applyChange(rule.rule.rangeValues);\n                switch (change.changeType) {\n                    case \"REMOVE\":\n                        this.history.update(\"cfRules\", sheetId, this.cfRules[sheetId].indexOf(rule), \"rule\", \n                        //@ts-expect-error\n                        \"rangeValues\", undefined);\n                        break;\n                    case \"RESIZE\":\n                    case \"MOVE\":\n                    case \"CHANGE\":\n                        this.history.update(\"cfRules\", sheetId, this.cfRules[sheetId].indexOf(rule), \"rule\", \n                        //@ts-expect-error\n                        \"rangeValues\", change.range);\n                        break;\n                }\n            }\n            for (const range of rule.ranges) {\n                const change = applyChange(range);\n                switch (change.changeType) {\n                    case \"REMOVE\":\n                        let copy = rule.ranges.slice();\n                        copy.splice(rule.ranges.indexOf(range), 1);\n                        if (copy.length >= 1) {\n                            this.history.update(\"cfRules\", sheetId, this.cfRules[sheetId].indexOf(rule), \"ranges\", copy);\n                        }\n                        else {\n                            this.removeConditionalFormatting(rule.id, sheetId);\n                        }\n                        break;\n                    case \"RESIZE\":\n                    case \"MOVE\":\n                    case \"CHANGE\":\n                        this.history.update(\"cfRules\", sheetId, this.cfRules[sheetId].indexOf(rule), \"ranges\", rule.ranges.indexOf(range), change.range);\n                        break;\n                }\n            }\n        }\n    }\n    adaptRanges(applyChange, sheetId) {\n        if (sheetId) {\n            this.loopThroughRangesOfSheet(sheetId, applyChange);\n        }\n        else {\n            for (const sheetId of Object.keys(this.cfRules)) {\n                this.loopThroughRangesOfSheet(sheetId, applyChange);\n            }\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"ADD_CONDITIONAL_FORMAT\":\n                return this.checkValidations(cmd, this.checkCFRule, this.checkEmptyRange);\n            case \"CHANGE_CONDITIONAL_FORMAT_PRIORITY\":\n                return this.checkValidPriorityChange(cmd.cfId, cmd.delta, cmd.sheetId);\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_SHEET\":\n                this.cfRules[cmd.sheetId] = [];\n                break;\n            case \"DUPLICATE_SHEET\":\n                this.history.update(\"cfRules\", cmd.sheetIdTo, []);\n                for (const cf of this.getConditionalFormats(cmd.sheetId)) {\n                    this.addConditionalFormatting(cf, cmd.sheetIdTo);\n                }\n                break;\n            case \"DELETE_SHEET\":\n                const cfRules = Object.assign({}, this.cfRules);\n                delete cfRules[cmd.sheetId];\n                this.history.update(\"cfRules\", cfRules);\n                break;\n            case \"ADD_CONDITIONAL_FORMAT\":\n                const cf = {\n                    ...cmd.cf,\n                    ranges: cmd.ranges.map((rangeData) => this.getters.getRangeString(this.getters.getRangeFromRangeData(rangeData), cmd.sheetId)),\n                };\n                this.addConditionalFormatting(cf, cmd.sheetId);\n                break;\n            case \"REMOVE_CONDITIONAL_FORMAT\":\n                this.removeConditionalFormatting(cmd.id, cmd.sheetId);\n                break;\n            case \"CHANGE_CONDITIONAL_FORMAT_PRIORITY\":\n                this.changeCFPriority(cmd.cfId, cmd.delta, cmd.sheetId);\n                break;\n        }\n    }\n    import(data) {\n        for (let sheet of data.sheets) {\n            this.cfRules[sheet.id] = sheet.conditionalFormats.map((rule) => this.mapToConditionalFormatInternal(sheet.id, rule));\n        }\n    }\n    export(data) {\n        if (data.sheets) {\n            for (let sheet of data.sheets) {\n                if (this.cfRules[sheet.id]) {\n                    sheet.conditionalFormats = this.cfRules[sheet.id].map((rule) => this.mapToConditionalFormat(sheet.id, rule));\n                }\n            }\n        }\n    }\n    exportForExcel(data) {\n        if (data.sheets) {\n            for (let sheet of data.sheets) {\n                if (this.cfRules[sheet.id]) {\n                    sheet.conditionalFormats = this.cfRules[sheet.id].map((rule) => this.mapToConditionalFormat(sheet.id, rule, { useFixedReference: true }));\n                }\n            }\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    /**\n     * Returns all the conditional format rules defined for the current sheet to display the user\n     */\n    getConditionalFormats(sheetId) {\n        return this.cfRules[sheetId]?.map((cf) => this.mapToConditionalFormat(sheetId, cf)) || [];\n    }\n    getRulesSelection(sheetId, selection) {\n        const ruleIds = new Set();\n        selection.forEach((zone) => {\n            const zoneRuleId = this.getRulesByZone(sheetId, zone);\n            zoneRuleId.forEach((ruleId) => {\n                ruleIds.add(ruleId);\n            });\n        });\n        return Array.from(ruleIds);\n    }\n    getRulesByZone(sheetId, zone) {\n        const ruleIds = new Set();\n        for (let row = zone.top; row <= zone.bottom; row++) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                const cellRules = this.getRulesByCell(sheetId, col, row);\n                cellRules.forEach((rule) => {\n                    ruleIds.add(rule.id);\n                });\n            }\n        }\n        return ruleIds;\n    }\n    getRulesByCell(sheetId, cellCol, cellRow) {\n        const rules = [];\n        for (let cf of this.cfRules[sheetId]) {\n            for (let range of cf.ranges) {\n                if (isInside(cellCol, cellRow, range.zone)) {\n                    rules.push(cf);\n                }\n            }\n        }\n        return new Set(rules.map((rule) => {\n            return this.mapToConditionalFormat(sheetId, rule);\n        }));\n    }\n    /**\n     * Add or remove cells to a given conditional formatting rule and return the adapted CF's XCs.\n     */\n    getAdaptedCfRanges(sheetId, cf, toAdd, toRemove) {\n        if (toAdd.length === 0 && toRemove.length === 0) {\n            return;\n        }\n        const rules = this.getters.getConditionalFormats(sheetId);\n        const replaceIndex = rules.findIndex((c) => c.id === cf.id);\n        let currentRanges = [];\n        if (replaceIndex > -1) {\n            currentRanges = rules[replaceIndex].ranges.map(toUnboundedZone);\n        }\n        // Remove the zones first in case the same position is in toAdd and toRemove\n        const withRemovedZones = recomputeZones(currentRanges, toRemove);\n        return recomputeZones([...toAdd, ...withRemovedZones], []).map((zone) => this.getters.getRangeDataFromZone(sheetId, zone));\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    mapToConditionalFormat(sheetId, cf, { useFixedReference } = { useFixedReference: false }) {\n        const ranges = cf.ranges.map((range) => {\n            return this.getters.getRangeString(range, sheetId, { useFixedReference });\n        });\n        if (cf.rule.type !== \"DataBarRule\") {\n            return {\n                ...cf,\n                rule: { ...cf.rule },\n                ranges,\n            };\n        }\n        return {\n            ...cf,\n            rule: {\n                ...cf.rule,\n                rangeValues: cf.rule.rangeValues &&\n                    this.getters.getRangeString(cf.rule.rangeValues, sheetId, {\n                        useFixedReference,\n                    }),\n            },\n            ranges,\n        };\n    }\n    mapToConditionalFormatInternal(sheet, cf) {\n        const ranges = cf.ranges.map((range) => {\n            return this.getters.getRangeFromSheetXC(sheet, range);\n        });\n        if (cf.rule.type !== \"DataBarRule\") {\n            return {\n                ...cf,\n                rule: { ...cf.rule },\n                ranges,\n            };\n        }\n        return {\n            ...cf,\n            rule: {\n                ...cf.rule,\n                rangeValues: cf.rule.rangeValues\n                    ? this.getters.getRangeFromSheetXC(sheet, cf.rule.rangeValues)\n                    : undefined,\n            },\n            ranges,\n        };\n    }\n    /**\n     * Add or replace a conditional format rule\n     */\n    addConditionalFormatting(cf, sheet) {\n        const currentCF = this.cfRules[sheet].slice();\n        const replaceIndex = currentCF.findIndex((c) => c.id === cf.id);\n        const newCF = this.mapToConditionalFormatInternal(sheet, cf);\n        if (replaceIndex > -1) {\n            currentCF.splice(replaceIndex, 1, newCF);\n        }\n        else {\n            currentCF.push(newCF);\n        }\n        this.history.update(\"cfRules\", sheet, currentCF);\n    }\n    checkValidPriorityChange(cfId, delta, sheetId) {\n        if (!this.cfRules[sheetId])\n            return \"InvalidSheetId\" /* CommandResult.InvalidSheetId */;\n        const ruleIndex = this.cfRules[sheetId].findIndex((cf) => cf.id === cfId);\n        if (ruleIndex === -1)\n            return \"InvalidConditionalFormatId\" /* CommandResult.InvalidConditionalFormatId */;\n        const cfIndex2 = ruleIndex - delta;\n        if (cfIndex2 < 0 || cfIndex2 >= this.cfRules[sheetId].length) {\n            return \"InvalidConditionalFormatId\" /* CommandResult.InvalidConditionalFormatId */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkEmptyRange(cmd) {\n        return cmd.ranges.length ? \"Success\" /* CommandResult.Success */ : \"EmptyRange\" /* CommandResult.EmptyRange */;\n    }\n    checkCFRule(cmd) {\n        const rule = cmd.cf.rule;\n        switch (rule.type) {\n            case \"CellIsRule\":\n                return this.checkValidations(rule, this.checkOperatorArgsNumber(2, [\"Between\", \"NotBetween\"]), this.checkOperatorArgsNumber(1, [\n                    \"BeginsWith\",\n                    \"ContainsText\",\n                    \"EndsWith\",\n                    \"GreaterThan\",\n                    \"GreaterThanOrEqual\",\n                    \"LessThan\",\n                    \"LessThanOrEqual\",\n                    \"NotContains\",\n                    \"Equal\",\n                    \"NotEqual\",\n                ]), this.checkOperatorArgsNumber(0, [\"IsEmpty\", \"IsNotEmpty\"]), this.checkCFValues);\n            case \"ColorScaleRule\": {\n                return this.checkValidations(rule, this.chainValidations(this.checkThresholds(this.checkFormulaCompilation)), this.chainValidations(this.checkThresholds(this.checkNaN), this.batchValidations(this.checkMinBiggerThanMax, this.checkMinBiggerThanMid, this.checkMidBiggerThanMax\n                // Those three validations can be factorized further\n                )));\n            }\n            case \"IconSetRule\": {\n                return this.checkValidations(rule, this.chainValidations(this.checkInflectionPoints(this.checkNaN), this.checkLowerBiggerThanUpper), this.chainValidations(this.checkInflectionPoints(this.checkFormulaCompilation)));\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkOperatorArgsNumber(expectedNumber, operators) {\n        if (expectedNumber > 2) {\n            throw new Error(\"Checking more than 2 arguments is currently not supported. Add the appropriate CommandResult if you want to.\");\n        }\n        return (rule) => {\n            if (operators.includes(rule.operator)) {\n                const errors = [];\n                const isEmpty = (value) => value === undefined || value === \"\";\n                if (expectedNumber >= 1 && isEmpty(rule.values[0])) {\n                    errors.push(\"FirstArgMissing\" /* CommandResult.FirstArgMissing */);\n                }\n                if (expectedNumber >= 2 && isEmpty(rule.values[1])) {\n                    errors.push(\"SecondArgMissing\" /* CommandResult.SecondArgMissing */);\n                }\n                return errors.length ? errors : \"Success\" /* CommandResult.Success */;\n            }\n            return \"Success\" /* CommandResult.Success */;\n        };\n    }\n    checkNaN(threshold, thresholdName) {\n        if ([\"number\", \"percentage\", \"percentile\"].includes(threshold.type) &&\n            (threshold.value === \"\" || isNaN(threshold.value))) {\n            switch (thresholdName) {\n                case \"min\":\n                    return \"MinNaN\" /* CommandResult.MinNaN */;\n                case \"max\":\n                    return \"MaxNaN\" /* CommandResult.MaxNaN */;\n                case \"mid\":\n                    return \"MidNaN\" /* CommandResult.MidNaN */;\n                case \"upperInflectionPoint\":\n                    return \"ValueUpperInflectionNaN\" /* CommandResult.ValueUpperInflectionNaN */;\n                case \"lowerInflectionPoint\":\n                    return \"ValueLowerInflectionNaN\" /* CommandResult.ValueLowerInflectionNaN */;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkFormulaCompilation(threshold, thresholdName) {\n        if (threshold.type !== \"formula\")\n            return \"Success\" /* CommandResult.Success */;\n        const compiledFormula = compile(threshold.value || \"\");\n        if (compiledFormula.isBadExpression) {\n            switch (thresholdName) {\n                case \"min\":\n                    return \"MinInvalidFormula\" /* CommandResult.MinInvalidFormula */;\n                case \"max\":\n                    return \"MaxInvalidFormula\" /* CommandResult.MaxInvalidFormula */;\n                case \"mid\":\n                    return \"MidInvalidFormula\" /* CommandResult.MidInvalidFormula */;\n                case \"upperInflectionPoint\":\n                    return \"ValueUpperInvalidFormula\" /* CommandResult.ValueUpperInvalidFormula */;\n                case \"lowerInflectionPoint\":\n                    return \"ValueLowerInvalidFormula\" /* CommandResult.ValueLowerInvalidFormula */;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkThresholds(check) {\n        return this.batchValidations((rule) => check(rule.minimum, \"min\"), (rule) => check(rule.maximum, \"max\"), (rule) => (rule.midpoint ? check(rule.midpoint, \"mid\") : \"Success\" /* CommandResult.Success */));\n    }\n    checkInflectionPoints(check) {\n        return this.batchValidations((rule) => check(rule.lowerInflectionPoint, \"lowerInflectionPoint\"), (rule) => check(rule.upperInflectionPoint, \"upperInflectionPoint\"));\n    }\n    checkLowerBiggerThanUpper(rule) {\n        const minValue = rule.lowerInflectionPoint.value;\n        const maxValue = rule.upperInflectionPoint.value;\n        if ([\"number\", \"percentage\", \"percentile\"].includes(rule.lowerInflectionPoint.type) &&\n            rule.lowerInflectionPoint.type === rule.upperInflectionPoint.type &&\n            Number(minValue) > Number(maxValue)) {\n            return \"LowerBiggerThanUpper\" /* CommandResult.LowerBiggerThanUpper */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkMinBiggerThanMax(rule) {\n        const minValue = rule.minimum.value;\n        const maxValue = rule.maximum.value;\n        if ([\"number\", \"percentage\", \"percentile\"].includes(rule.minimum.type) &&\n            rule.minimum.type === rule.maximum.type &&\n            stringToNumber(minValue) >= stringToNumber(maxValue)) {\n            return \"MinBiggerThanMax\" /* CommandResult.MinBiggerThanMax */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkMidBiggerThanMax(rule) {\n        const midValue = rule.midpoint?.value;\n        const maxValue = rule.maximum.value;\n        if (rule.midpoint &&\n            [\"number\", \"percentage\", \"percentile\"].includes(rule.midpoint.type) &&\n            rule.midpoint.type === rule.maximum.type &&\n            stringToNumber(midValue) >= stringToNumber(maxValue)) {\n            return \"MidBiggerThanMax\" /* CommandResult.MidBiggerThanMax */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkMinBiggerThanMid(rule) {\n        const minValue = rule.minimum.value;\n        const midValue = rule.midpoint?.value;\n        if (rule.midpoint &&\n            [\"number\", \"percentage\", \"percentile\"].includes(rule.midpoint.type) &&\n            rule.minimum.type === rule.midpoint.type &&\n            stringToNumber(minValue) >= stringToNumber(midValue)) {\n            return \"MinBiggerThanMid\" /* CommandResult.MinBiggerThanMid */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkCFValues(rule) {\n        for (const value of rule.values) {\n            if (!value.startsWith(\"=\"))\n                continue;\n            const compiledFormula = compile(value || \"\");\n            if (compiledFormula.isBadExpression) {\n                return \"ValueCellIsInvalidFormula\" /* CommandResult.ValueCellIsInvalidFormula */;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    removeConditionalFormatting(id, sheet) {\n        const cfIndex = this.cfRules[sheet].findIndex((s) => s.id === id);\n        if (cfIndex !== -1) {\n            const currentCF = this.cfRules[sheet].slice();\n            currentCF.splice(cfIndex, 1);\n            this.history.update(\"cfRules\", sheet, currentCF);\n        }\n    }\n    changeCFPriority(cfId, delta, sheetId) {\n        const currentIndex = this.cfRules[sheetId].findIndex((s) => s.id === cfId);\n        const cf = this.cfRules[sheetId][currentIndex];\n        const targetIndex = currentIndex - delta; // priority goes up when index goes down\n        const cfRules = [...this.cfRules[sheetId]];\n        cfRules.splice(currentIndex, 1);\n        cfRules.splice(targetIndex, 0, cf);\n        this.history.update(\"cfRules\", sheetId, cfRules);\n    }\n}\n\nclass DataValidationPlugin extends CorePlugin {\n    static getters = [\n        \"cellHasListDataValidationIcon\",\n        \"getDataValidationRule\",\n        \"getDataValidationRules\",\n        \"getValidationRuleForCell\",\n    ];\n    rules = {};\n    adaptRanges(applyChange, sheetId) {\n        const sheetIds = sheetId ? [sheetId] : Object.keys(this.rules);\n        for (const sheetId of sheetIds) {\n            this.loopThroughRangesOfSheet(sheetId, applyChange);\n        }\n    }\n    loopThroughRangesOfSheet(sheetId, applyChange) {\n        const rules = this.rules[sheetId];\n        for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {\n            const rule = this.rules[sheetId][ruleIndex];\n            for (let rangeIndex = rule.ranges.length - 1; rangeIndex >= 0; rangeIndex--) {\n                const range = rule.ranges[rangeIndex];\n                const change = applyChange(range);\n                switch (change.changeType) {\n                    case \"REMOVE\":\n                        if (rule.ranges.length === 1) {\n                            this.removeDataValidationRule(sheetId, rule.id);\n                        }\n                        else {\n                            const copy = rule.ranges.slice();\n                            copy.splice(rangeIndex, 1);\n                            this.history.update(\"rules\", sheetId, ruleIndex, \"ranges\", copy);\n                        }\n                        break;\n                    case \"RESIZE\":\n                    case \"MOVE\":\n                    case \"CHANGE\":\n                        this.history.update(\"rules\", sheetId, ruleIndex, \"ranges\", rangeIndex, change.range);\n                        break;\n                }\n            }\n        }\n    }\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"ADD_DATA_VALIDATION_RULE\":\n                return this.checkValidations(cmd, this.chainValidations(this.checkEmptyRange, this.checkCriterionTypeIsValid, this.checkCriterionHasValidNumberOfValues, this.checkCriterionValuesAreValid));\n            case \"REMOVE_DATA_VALIDATION_RULE\":\n                if (!this.rules[cmd.sheetId].find((rule) => rule.id === cmd.id)) {\n                    return \"UnknownDataValidationRule\" /* CommandResult.UnknownDataValidationRule */;\n                }\n                break;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_SHEET\":\n                this.history.update(\"rules\", cmd.sheetId, []);\n                break;\n            case \"DUPLICATE_SHEET\": {\n                const rules = deepCopy(this.rules[cmd.sheetId]).map((rule) => ({\n                    ...rule,\n                    ranges: rule.ranges.map((range) => copyRangeWithNewSheetId(cmd.sheetId, cmd.sheetIdTo, range)),\n                }));\n                this.history.update(\"rules\", cmd.sheetIdTo, rules);\n                break;\n            }\n            case \"DELETE_SHEET\": {\n                const rules = { ...this.rules };\n                delete rules[cmd.sheetId];\n                this.history.update(\"rules\", rules);\n                break;\n            }\n            case \"REMOVE_DATA_VALIDATION_RULE\": {\n                this.removeDataValidationRule(cmd.sheetId, cmd.id);\n                break;\n            }\n            case \"ADD_DATA_VALIDATION_RULE\": {\n                const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));\n                this.addDataValidationRule(cmd.sheetId, { ...cmd.rule, ranges });\n                break;\n            }\n            case \"DELETE_CONTENT\": {\n                const zones = recomputeZones(cmd.target);\n                const sheetId = cmd.sheetId;\n                for (const zone of zones) {\n                    for (let row = zone.top; row <= zone.bottom; row++) {\n                        for (let col = zone.left; col <= zone.right; col++) {\n                            const dataValidation = this.getValidationRuleForCell({ sheetId, col, row });\n                            if (!dataValidation) {\n                                continue;\n                            }\n                            if (dataValidation.criterion.type === \"isBoolean\" ||\n                                (dataValidation.criterion.type === \"isValueInList\" &&\n                                    !this.getters.getCell({ sheetId, col, row })?.content)) {\n                                const rules = this.rules[sheetId];\n                                const ranges = [this.getters.getRangeFromSheetXC(sheetId, toXC(col, row))];\n                                const adaptedRules = this.removeRangesFromRules(sheetId, ranges, rules);\n                                this.history.update(\"rules\", sheetId, adaptedRules);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n    getDataValidationRules(sheetId) {\n        return this.rules[sheetId];\n    }\n    getDataValidationRule(sheetId, id) {\n        return this.rules[sheetId].find((rule) => rule.id === id);\n    }\n    getValidationRuleForCell({ sheetId, col, row }) {\n        if (!this.rules[sheetId]) {\n            return undefined;\n        }\n        for (const rule of this.rules[sheetId]) {\n            for (const range of rule.ranges) {\n                if (isInside(col, row, range.zone)) {\n                    return rule;\n                }\n            }\n        }\n        return undefined;\n    }\n    cellHasListDataValidationIcon(cellPosition) {\n        const rule = this.getValidationRuleForCell(cellPosition);\n        if (!rule)\n            return false;\n        return ((rule.criterion.type === \"isValueInList\" || rule.criterion.type === \"isValueInRange\") &&\n            rule.criterion.displayStyle === \"arrow\");\n    }\n    addDataValidationRule(sheetId, newRule) {\n        const rules = this.rules[sheetId];\n        if (newRule.criterion.type === \"isBoolean\") {\n            this.setCenterStyleToBooleanCells(newRule);\n        }\n        else if (newRule.criterion.type === \"isValueInList\") {\n            newRule.criterion.values = Array.from(new Set(newRule.criterion.values));\n        }\n        const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);\n        const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);\n        if (ruleIndex !== -1) {\n            adaptedRules[ruleIndex] = newRule;\n            this.history.update(\"rules\", sheetId, adaptedRules);\n        }\n        else {\n            this.history.update(\"rules\", sheetId, [...adaptedRules, newRule]);\n        }\n    }\n    removeRangesFromRules(sheetId, ranges, rules) {\n        rules = deepCopy(rules);\n        for (const rule of rules) {\n            rule.ranges = this.getters.recomputeRanges(rule.ranges, ranges);\n        }\n        return rules.filter((rule) => rule.ranges.length > 0);\n    }\n    removeDataValidationRule(sheetId, ruleId) {\n        const rules = this.rules[sheetId];\n        const newRules = rules.filter((rule) => rule.id !== ruleId);\n        this.history.update(\"rules\", sheetId, newRules);\n    }\n    setCenterStyleToBooleanCells(rule) {\n        for (const position of getCellPositionsInRanges(rule.ranges)) {\n            const cell = this.getters.getCell(position);\n            const style = {\n                ...cell?.style,\n                align: cell?.style?.align ?? \"center\",\n                verticalAlign: cell?.style?.verticalAlign ?? \"middle\",\n            };\n            this.dispatch(\"UPDATE_CELL\", { ...position, style });\n        }\n    }\n    checkEmptyRange(cmd) {\n        return cmd.ranges.length ? \"Success\" /* CommandResult.Success */ : \"EmptyRange\" /* CommandResult.EmptyRange */;\n    }\n    import(data) {\n        for (const sheet of data.sheets) {\n            this.rules[sheet.id] = [];\n            if (!sheet.dataValidationRules) {\n                continue;\n            }\n            for (const rule of sheet.dataValidationRules) {\n                this.rules[sheet.id].push({\n                    ...rule,\n                    ranges: rule.ranges.map((range) => this.getters.getRangeFromSheetXC(sheet.id, range)),\n                });\n            }\n        }\n    }\n    export(data) {\n        if (!data.sheets) {\n            return;\n        }\n        for (const sheet of data.sheets) {\n            sheet.dataValidationRules = [];\n            for (const rule of this.rules[sheet.id]) {\n                sheet.dataValidationRules.push({\n                    ...rule,\n                    ranges: rule.ranges.map((range) => this.getters.getRangeString(range, sheet.id)),\n                });\n            }\n        }\n    }\n    checkCriterionTypeIsValid(cmd) {\n        return dataValidationEvaluatorRegistry.contains(cmd.rule.criterion.type)\n            ? \"Success\" /* CommandResult.Success */\n            : \"UnknownDataValidationCriterionType\" /* CommandResult.UnknownDataValidationCriterionType */;\n    }\n    checkCriterionHasValidNumberOfValues(cmd) {\n        const criterion = cmd.rule.criterion;\n        const evaluator = dataValidationEvaluatorRegistry.get(criterion.type);\n        const expectedNumberOfValues = evaluator.numberOfValues(criterion);\n        if (expectedNumberOfValues !== undefined &&\n            criterion.values.length !== expectedNumberOfValues) {\n            return \"InvalidNumberOfCriterionValues\" /* CommandResult.InvalidNumberOfCriterionValues */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkCriterionValuesAreValid(cmd) {\n        const criterion = cmd.rule.criterion;\n        const evaluator = dataValidationEvaluatorRegistry.get(criterion.type);\n        if (criterion.values.some((value) => {\n            if (value.startsWith(\"=\")) {\n                return evaluator.allowedValues === \"onlyLiterals\";\n            }\n            else if (evaluator.allowedValues === \"onlyFormulas\") {\n                return true;\n            }\n            else {\n                return !evaluator.isCriterionValueValid(value);\n            }\n        })) {\n            return \"InvalidDataValidationCriterionValue\" /* CommandResult.InvalidDataValidationCriterionValue */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n}\n\nclass FigurePlugin extends CorePlugin {\n    static getters = [\"getFigures\", \"getFigure\", \"getFigureSheetId\"];\n    figures = {};\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_FIGURE\":\n                return this.checkFigureDuplicate(cmd.figure.id);\n            case \"UPDATE_FIGURE\":\n            case \"DELETE_FIGURE\":\n                return this.checkFigureExists(cmd.sheetId, cmd.id);\n            default:\n                return \"Success\" /* CommandResult.Success */;\n        }\n    }\n    beforeHandle(cmd) {\n        switch (cmd.type) {\n            case \"DELETE_SHEET\":\n                this.getters.getFigures(cmd.sheetId).forEach((figure) => {\n                    this.dispatch(\"DELETE_FIGURE\", { id: figure.id, sheetId: cmd.sheetId });\n                });\n                break;\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_SHEET\":\n                this.figures[cmd.sheetId] = {};\n                break;\n            case \"DELETE_SHEET\":\n                this.deleteSheet(cmd.sheetId);\n                break;\n            case \"CREATE_FIGURE\":\n                this.addFigure(cmd.figure, cmd.sheetId);\n                break;\n            case \"UPDATE_FIGURE\":\n                const { type, sheetId, ...update } = cmd;\n                const figure = update;\n                this.updateFigure(sheetId, figure);\n                break;\n            case \"DELETE_FIGURE\":\n                this.removeFigure(cmd.id, cmd.sheetId);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\":\n                this.onRowColDelete(cmd.sheetId, cmd.dimension);\n        }\n    }\n    onRowColDelete(sheetId, dimension) {\n        dimension === \"ROW\" ? this.onRowDeletion(sheetId) : this.onColDeletion(sheetId);\n    }\n    onRowDeletion(sheetId) {\n        const numHeader = this.getters.getNumberRows(sheetId);\n        let gridHeight = 0;\n        for (let i = 0; i < numHeader; i++) {\n            // TODO : since the row size is an UI value now, this doesn't work anymore. Using the default cell height is\n            // a temporary solution at best, but is broken.\n            gridHeight += this.getters.getUserRowSize(sheetId, i) || DEFAULT_CELL_HEIGHT;\n        }\n        const figures = this.getters.getFigures(sheetId);\n        for (const figure of figures) {\n            const newY = Math.min(figure.y, gridHeight - figure.height);\n            if (newY !== figure.y) {\n                this.dispatch(\"UPDATE_FIGURE\", { sheetId, id: figure.id, y: newY });\n            }\n        }\n    }\n    onColDeletion(sheetId) {\n        const numHeader = this.getters.getNumberCols(sheetId);\n        let gridWidth = 0;\n        for (let i = 0; i < numHeader; i++) {\n            gridWidth += this.getters.getColSize(sheetId, i);\n        }\n        const figures = this.getters.getFigures(sheetId);\n        for (const figure of figures) {\n            const newX = Math.min(figure.x, gridWidth - figure.width);\n            if (newX !== figure.x) {\n                this.dispatch(\"UPDATE_FIGURE\", { sheetId, id: figure.id, x: newX });\n            }\n        }\n    }\n    updateFigure(sheetId, figure) {\n        if (!(\"id\" in figure)) {\n            return;\n        }\n        for (const [key, value] of Object.entries(figure)) {\n            switch (key) {\n                case \"x\":\n                case \"y\":\n                    if (value !== undefined) {\n                        this.history.update(\"figures\", sheetId, figure.id, key, Math.max(value, 0));\n                    }\n                    break;\n                case \"width\":\n                case \"height\":\n                    if (value !== undefined) {\n                        this.history.update(\"figures\", sheetId, figure.id, key, value);\n                    }\n                    break;\n            }\n        }\n    }\n    addFigure(figure, sheetId) {\n        this.history.update(\"figures\", sheetId, figure.id, figure);\n    }\n    deleteSheet(sheetId) {\n        this.history.update(\"figures\", sheetId, undefined);\n    }\n    removeFigure(id, sheetId) {\n        this.history.update(\"figures\", sheetId, id, undefined);\n    }\n    checkFigureExists(sheetId, figureId) {\n        if (this.figures[sheetId]?.[figureId] === undefined) {\n            return \"FigureDoesNotExist\" /* CommandResult.FigureDoesNotExist */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkFigureDuplicate(figureId) {\n        if (Object.values(this.figures).find((sheet) => sheet?.[figureId])) {\n            return \"DuplicatedFigureId\" /* CommandResult.DuplicatedFigureId */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getFigures(sheetId) {\n        return Object.values(this.figures[sheetId] || {}).filter(isDefined);\n    }\n    getFigure(sheetId, figureId) {\n        return this.figures[sheetId]?.[figureId];\n    }\n    getFigureSheetId(figureId) {\n        return Object.keys(this.figures).find((sheetId) => this.figures[sheetId]?.[figureId] !== undefined);\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n    import(data) {\n        for (let sheet of data.sheets) {\n            const figures = {};\n            sheet.figures.forEach((figure) => {\n                figures[figure.id] = figure;\n            });\n            this.figures[sheet.id] = figures;\n        }\n    }\n    export(data) {\n        for (const sheet of data.sheets) {\n            for (const figure of this.getFigures(sheet.id)) {\n                const data = undefined;\n                sheet.figures.push({ ...figure, data });\n            }\n        }\n    }\n    exportForExcel(data) {\n        this.export(data);\n    }\n}\n\nclass HeaderSizePlugin extends CorePlugin {\n    static getters = [\"getUserRowSize\", \"getColSize\"];\n    sizes = {};\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_SHEET\": {\n                this.history.update(\"sizes\", cmd.sheetId, {\n                    COL: Array(this.getters.getNumberCols(cmd.sheetId)).fill(undefined),\n                    ROW: Array(this.getters.getNumberRows(cmd.sheetId)).fill(undefined),\n                });\n                break;\n            }\n            case \"DUPLICATE_SHEET\":\n                this.history.update(\"sizes\", cmd.sheetIdTo, deepCopy(this.sizes[cmd.sheetId]));\n                break;\n            case \"DELETE_SHEET\":\n                const sizes = { ...this.sizes };\n                delete sizes[cmd.sheetId];\n                this.history.update(\"sizes\", sizes);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\": {\n                const arr = this.sizes[cmd.sheetId][cmd.dimension];\n                const sizes = removeIndexesFromArray(arr, cmd.elements);\n                this.history.update(\"sizes\", cmd.sheetId, cmd.dimension, sizes);\n                break;\n            }\n            case \"ADD_COLUMNS_ROWS\": {\n                let sizes = [...this.sizes[cmd.sheetId][cmd.dimension]];\n                const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);\n                const baseSize = sizes[cmd.base];\n                sizes.splice(addIndex, 0, ...Array(cmd.quantity).fill(baseSize));\n                this.history.update(\"sizes\", cmd.sheetId, cmd.dimension, sizes);\n                break;\n            }\n            case \"RESIZE_COLUMNS_ROWS\":\n                if (cmd.dimension === \"ROW\") {\n                    for (const el of cmd.elements) {\n                        this.history.update(\"sizes\", cmd.sheetId, cmd.dimension, el, cmd.size || undefined);\n                    }\n                }\n                else {\n                    for (const el of cmd.elements) {\n                        this.history.update(\"sizes\", cmd.sheetId, cmd.dimension, el, cmd.size || undefined);\n                    }\n                }\n                break;\n        }\n        return;\n    }\n    getColSize(sheetId, index) {\n        return Math.round(this.sizes[sheetId]?.[\"COL\"][index] || DEFAULT_CELL_WIDTH);\n    }\n    getUserRowSize(sheetId, index) {\n        const rowSize = this.sizes[sheetId]?.[\"ROW\"][index];\n        return rowSize ? Math.round(rowSize) : undefined;\n    }\n    import(data) {\n        for (let sheet of data.sheets) {\n            const sizes = {\n                COL: Array(sheet.colNumber).fill(undefined),\n                ROW: Array(sheet.rowNumber).fill(undefined),\n            };\n            for (let [rowIndex, row] of Object.entries(sheet.rows)) {\n                if (row.size) {\n                    sizes[\"ROW\"][rowIndex] = row.size;\n                }\n            }\n            for (let [colIndex, col] of Object.entries(sheet.cols)) {\n                if (col.size) {\n                    sizes[\"COL\"][colIndex] = col.size;\n                }\n            }\n            this.sizes[sheet.id] = sizes;\n        }\n        return;\n    }\n    exportForExcel(data) {\n        this.exportData(data, true);\n    }\n    export(data) {\n        this.exportData(data);\n    }\n    /**\n     * Export the header sizes\n     *\n     * @param exportDefaults : if true, export column/row sizes even if they have the default size\n     */\n    exportData(data, exportDefaults = false) {\n        for (let sheet of data.sheets) {\n            // Export row sizes\n            if (sheet.rows === undefined) {\n                sheet.rows = {};\n            }\n            for (const row of range(0, this.getters.getNumberRows(sheet.id))) {\n                if (exportDefaults || this.sizes[sheet.id][\"ROW\"][row]) {\n                    sheet.rows[row] = {\n                        ...sheet.rows[row],\n                        size: this.getUserRowSize(sheet.id, row) ?? DEFAULT_CELL_HEIGHT,\n                    };\n                }\n            }\n            // Export col sizes\n            if (sheet.cols === undefined) {\n                sheet.cols = {};\n            }\n            for (let col of range(0, this.getters.getNumberCols(sheet.id))) {\n                if (exportDefaults || this.sizes[sheet.id][\"COL\"][col]) {\n                    sheet.cols[col] = { ...sheet.cols[col], size: this.getColSize(sheet.id, col) };\n                }\n            }\n        }\n    }\n}\n\nclass HeaderVisibilityPlugin extends CorePlugin {\n    static getters = [\n        \"checkElementsIncludeAllVisibleHeaders\",\n        \"getHiddenColsGroups\",\n        \"getHiddenRowsGroups\",\n        \"isHeaderHiddenByUser\",\n        \"isRowHiddenByUser\",\n        \"isColHiddenByUser\",\n    ];\n    hiddenHeaders = {};\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"HIDE_COLUMNS_ROWS\": {\n                if (!this.getters.tryGetSheet(cmd.sheetId)) {\n                    return \"InvalidSheetId\" /* CommandResult.InvalidSheetId */;\n                }\n                const hiddenGroup = cmd.dimension === \"COL\"\n                    ? this.getHiddenColsGroups(cmd.sheetId)\n                    : this.getHiddenRowsGroups(cmd.sheetId);\n                const elements = cmd.dimension === \"COL\"\n                    ? this.getters.getNumberCols(cmd.sheetId)\n                    : this.getters.getNumberRows(cmd.sheetId);\n                const hiddenElements = new Set((hiddenGroup || []).flat().concat(cmd.elements));\n                if (hiddenElements.size >= elements) {\n                    return \"TooManyHiddenElements\" /* CommandResult.TooManyHiddenElements */;\n                }\n                else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {\n                    return \"InvalidHeaderIndex\" /* CommandResult.InvalidHeaderIndex */;\n                }\n                else {\n                    return \"Success\" /* CommandResult.Success */;\n                }\n            }\n            case \"REMOVE_COLUMNS_ROWS\":\n                if (!this.getters.tryGetSheet(cmd.sheetId)) {\n                    return \"InvalidSheetId\" /* CommandResult.InvalidSheetId */;\n                }\n                if (this.checkElementsIncludeAllVisibleHeaders(cmd.sheetId, cmd.dimension, cmd.elements)) {\n                    return \"NotEnoughElements\" /* CommandResult.NotEnoughElements */;\n                }\n                return \"Success\" /* CommandResult.Success */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_SHEET\":\n                const hiddenHeaders = {\n                    COL: Array(this.getters.getNumberCols(cmd.sheetId)).fill(false),\n                    ROW: Array(this.getters.getNumberRows(cmd.sheetId)).fill(false),\n                };\n                this.history.update(\"hiddenHeaders\", cmd.sheetId, hiddenHeaders);\n                break;\n            case \"DUPLICATE_SHEET\":\n                this.history.update(\"hiddenHeaders\", cmd.sheetIdTo, deepCopy(this.hiddenHeaders[cmd.sheetId]));\n                break;\n            case \"DELETE_SHEET\":\n                this.history.update(\"hiddenHeaders\", cmd.sheetId, undefined);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\": {\n                const hiddenHeaders = [...this.hiddenHeaders[cmd.sheetId][cmd.dimension]];\n                for (let el of [...cmd.elements].sort((a, b) => b - a)) {\n                    hiddenHeaders.splice(el, 1);\n                }\n                this.history.update(\"hiddenHeaders\", cmd.sheetId, cmd.dimension, hiddenHeaders);\n                break;\n            }\n            case \"ADD_COLUMNS_ROWS\": {\n                const hiddenHeaders = [...this.hiddenHeaders[cmd.sheetId][cmd.dimension]];\n                const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);\n                hiddenHeaders.splice(addIndex, 0, ...Array(cmd.quantity).fill(false));\n                this.history.update(\"hiddenHeaders\", cmd.sheetId, cmd.dimension, hiddenHeaders);\n                break;\n            }\n            case \"HIDE_COLUMNS_ROWS\":\n                for (let el of cmd.elements) {\n                    this.history.update(\"hiddenHeaders\", cmd.sheetId, cmd.dimension, el, true);\n                }\n                break;\n            case \"UNHIDE_COLUMNS_ROWS\":\n                for (let el of cmd.elements) {\n                    this.history.update(\"hiddenHeaders\", cmd.sheetId, cmd.dimension, el, false);\n                }\n                break;\n        }\n        return;\n    }\n    checkElementsIncludeAllVisibleHeaders(sheetId, dimension, elements) {\n        const visibleHeaders = this.getAllVisibleHeaders(sheetId, dimension);\n        return includesAll(elements, visibleHeaders);\n    }\n    isHeaderHiddenByUser(sheetId, dimension, index) {\n        return dimension === \"COL\"\n            ? this.isColHiddenByUser(sheetId, index)\n            : this.isRowHiddenByUser(sheetId, index);\n    }\n    isRowHiddenByUser(sheetId, index) {\n        return this.hiddenHeaders[sheetId].ROW[index] || this.getters.isRowFolded(sheetId, index);\n    }\n    isColHiddenByUser(sheetId, index) {\n        return this.hiddenHeaders[sheetId].COL[index] || this.getters.isColFolded(sheetId, index);\n    }\n    getHiddenColsGroups(sheetId) {\n        const consecutiveIndexes = [[]];\n        const hiddenCols = this.hiddenHeaders[sheetId].COL;\n        for (let col = 0; col < hiddenCols.length; col++) {\n            const isColHidden = hiddenCols[col];\n            if (isColHidden) {\n                consecutiveIndexes[consecutiveIndexes.length - 1].push(col);\n            }\n            else {\n                if (consecutiveIndexes[consecutiveIndexes.length - 1].length !== 0) {\n                    consecutiveIndexes.push([]);\n                }\n            }\n        }\n        if (consecutiveIndexes[consecutiveIndexes.length - 1].length === 0) {\n            consecutiveIndexes.pop();\n        }\n        return consecutiveIndexes;\n    }\n    getHiddenRowsGroups(sheetId) {\n        const consecutiveIndexes = [[]];\n        const hiddenCols = this.hiddenHeaders[sheetId].ROW;\n        for (let row = 0; row < hiddenCols.length; row++) {\n            const isRowHidden = hiddenCols[row];\n            if (isRowHidden) {\n                consecutiveIndexes[consecutiveIndexes.length - 1].push(row);\n            }\n            else {\n                if (consecutiveIndexes[consecutiveIndexes.length - 1].length !== 0) {\n                    consecutiveIndexes.push([]);\n                }\n            }\n        }\n        if (consecutiveIndexes[consecutiveIndexes.length - 1].length === 0) {\n            consecutiveIndexes.pop();\n        }\n        return consecutiveIndexes;\n    }\n    getAllVisibleHeaders(sheetId, dimension) {\n        const headers = range(0, this.getters.getNumberHeaders(sheetId, dimension));\n        const foldedHeaders = [];\n        this.getters.getHeaderGroups(sheetId, dimension).forEach((group) => {\n            if (group.isFolded) {\n                foldedHeaders.push(...range(group.start, group.end + 1));\n            }\n        });\n        return headers.filter((i) => {\n            return !this.hiddenHeaders[sheetId][dimension][i] && !foldedHeaders.includes(i);\n        });\n    }\n    import(data) {\n        for (let sheet of data.sheets) {\n            this.hiddenHeaders[sheet.id] = { COL: [], ROW: [] };\n            for (let row = 0; row < sheet.rowNumber; row++) {\n                this.hiddenHeaders[sheet.id].ROW[row] = Boolean(sheet.rows[row]?.isHidden);\n            }\n            for (let col = 0; col < sheet.colNumber; col++) {\n                this.hiddenHeaders[sheet.id].COL[col] = Boolean(sheet.cols[col]?.isHidden);\n            }\n        }\n        return;\n    }\n    exportForExcel(data) {\n        this.exportData(data, true);\n    }\n    export(data) {\n        this.exportData(data);\n    }\n    exportData(data, exportDefaults = false) {\n        for (let sheet of data.sheets) {\n            if (sheet.rows === undefined) {\n                sheet.rows = {};\n            }\n            for (let row = 0; row < this.getters.getNumberRows(sheet.id); row++) {\n                if (exportDefaults || this.hiddenHeaders[sheet.id][\"ROW\"][row]) {\n                    if (sheet.rows[row] === undefined) {\n                        sheet.rows[row] = {};\n                    }\n                    sheet.rows[row].isHidden ||= this.hiddenHeaders[sheet.id][\"ROW\"][row];\n                }\n            }\n            if (sheet.cols === undefined) {\n                sheet.cols = {};\n            }\n            for (let col = 0; col < this.getters.getNumberCols(sheet.id); col++) {\n                if (exportDefaults || this.hiddenHeaders[sheet.id][\"COL\"][col]) {\n                    if (sheet.cols[col] === undefined) {\n                        sheet.cols[col] = {};\n                    }\n                    sheet.cols[col].isHidden ||= this.hiddenHeaders[sheet.id][\"COL\"][col];\n                }\n            }\n        }\n    }\n}\n\nclass ImagePlugin extends CorePlugin {\n    static getters = [\"getImage\", \"getImagePath\", \"getImageSize\"];\n    fileStore;\n    images = {};\n    /**\n     * paths of images synced with the file store server.\n     */\n    syncedImages = new Set();\n    constructor(config) {\n        super(config);\n        this.fileStore = config.external.fileStore;\n    }\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_IMAGE\":\n                if (this.getters.getFigure(cmd.sheetId, cmd.figureId)) {\n                    return \"InvalidFigureId\" /* CommandResult.InvalidFigureId */;\n                }\n                return \"Success\" /* CommandResult.Success */;\n            default:\n                return \"Success\" /* CommandResult.Success */;\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_IMAGE\":\n                this.addImage(cmd.figureId, cmd.sheetId, cmd.position, cmd.size);\n                this.history.update(\"images\", cmd.sheetId, cmd.figureId, cmd.definition);\n                this.syncedImages.add(cmd.definition.path);\n                break;\n            case \"DUPLICATE_SHEET\": {\n                const sheetFiguresFrom = this.getters.getFigures(cmd.sheetId);\n                for (const fig of sheetFiguresFrom) {\n                    if (fig.tag === \"image\") {\n                        const figureIdBase = fig.id.split(FIGURE_ID_SPLITTER).pop();\n                        const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;\n                        const image = this.getImage(fig.id);\n                        if (image) {\n                            const size = { width: fig.width, height: fig.height };\n                            this.dispatch(\"CREATE_IMAGE\", {\n                                sheetId: cmd.sheetIdTo,\n                                figureId: duplicatedFigureId,\n                                position: { x: fig.x, y: fig.y },\n                                size,\n                                definition: deepCopy(image),\n                            });\n                        }\n                    }\n                }\n                break;\n            }\n            case \"DELETE_FIGURE\":\n                this.history.update(\"images\", cmd.sheetId, cmd.id, undefined);\n                break;\n            case \"DELETE_SHEET\":\n                this.history.update(\"images\", cmd.sheetId, undefined);\n                break;\n        }\n    }\n    /**\n     * Delete unused images from the file store\n     */\n    garbageCollectExternalResources() {\n        const images = new Set(this.getAllImages().map((image) => image.path));\n        for (const path of this.syncedImages) {\n            if (!images.has(path)) {\n                this.fileStore?.delete(path);\n            }\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getImage(figureId) {\n        for (const sheet of Object.values(this.images)) {\n            if (sheet && sheet[figureId]) {\n                return sheet[figureId];\n            }\n        }\n        throw new Error(`There is no image with the given figureId: ${figureId}`);\n    }\n    getImagePath(figureId) {\n        return this.getImage(figureId).path;\n    }\n    getImageSize(figureId) {\n        return this.getImage(figureId).size;\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    addImage(id, sheetId, position, size) {\n        const figure = {\n            id,\n            x: position.x,\n            y: position.y,\n            width: size.width,\n            height: size.height,\n            tag: \"image\",\n        };\n        this.dispatch(\"CREATE_FIGURE\", { sheetId, figure });\n    }\n    import(data) {\n        for (const sheet of data.sheets) {\n            const images = (sheet.figures || []).filter((figure) => figure.tag === \"image\");\n            for (const image of images) {\n                this.history.update(\"images\", sheet.id, image.id, image.data);\n                this.syncedImages.add(image.data.path);\n            }\n        }\n    }\n    export(data) {\n        for (const sheet of data.sheets) {\n            const images = sheet.figures.filter((figure) => figure.tag === \"image\");\n            for (const image of images) {\n                image.data = this.images[sheet.id]?.[image.id];\n            }\n        }\n    }\n    exportForExcel(data) {\n        for (const sheet of data.sheets) {\n            if (!sheet.images) {\n                sheet.images = [];\n            }\n            const figures = this.getters.getFigures(sheet.id);\n            const images = [];\n            for (const figure of figures) {\n                if (figure?.tag === \"image\") {\n                    const image = this.getImage(figure.id);\n                    if (image) {\n                        images.push({\n                            ...figure,\n                            data: deepCopy(image),\n                        });\n                    }\n                }\n            }\n            sheet.images = [...sheet.images, ...images];\n        }\n    }\n    getAllImages() {\n        const images = [];\n        for (const sheetId in this.images) {\n            images.push(...Object.values(this.images[sheetId] || {}).filter(isDefined));\n        }\n        return images;\n    }\n}\n\nclass MergePlugin extends CorePlugin {\n    static getters = [\n        \"isInMerge\",\n        \"isInSameMerge\",\n        \"isMergeHidden\",\n        \"getMainCellPosition\",\n        \"expandZone\",\n        \"doesIntersectMerge\",\n        \"doesColumnsHaveCommonMerges\",\n        \"doesRowsHaveCommonMerges\",\n        \"getMerges\",\n        \"getMerge\",\n        \"getMergesInZone\",\n        \"isSingleCellOrMerge\",\n        \"getSelectionRangeString\",\n        \"isMainCellPosition\",\n    ];\n    nextId = 1;\n    merges = {};\n    mergeCellMap = {};\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        const force = \"force\" in cmd ? !!cmd.force : false;\n        switch (cmd.type) {\n            case \"ADD_MERGE\":\n                if (force) {\n                    return this.checkValidations(cmd, this.checkFrozenPanes);\n                }\n                return this.checkValidations(cmd, this.checkDestructiveMerge, this.checkOverlap, this.checkFrozenPanes);\n            case \"UPDATE_CELL\":\n                return this.checkMergedContentUpdate(cmd);\n            case \"REMOVE_MERGE\":\n                return this.checkMergeExists(cmd);\n            default:\n                return \"Success\" /* CommandResult.Success */;\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_SHEET\":\n                this.history.update(\"merges\", cmd.sheetId, {});\n                this.history.update(\"mergeCellMap\", cmd.sheetId, {});\n                break;\n            case \"DELETE_SHEET\":\n                this.history.update(\"merges\", cmd.sheetId, {});\n                this.history.update(\"mergeCellMap\", cmd.sheetId, {});\n                break;\n            case \"DUPLICATE_SHEET\":\n                const merges = this.merges[cmd.sheetId];\n                if (!merges)\n                    break;\n                for (const range of Object.values(merges).filter(isDefined)) {\n                    this.addMerge(cmd.sheetIdTo, range.zone);\n                }\n                break;\n            case \"ADD_MERGE\":\n                for (const zone of cmd.target) {\n                    this.addMerge(cmd.sheetId, zone);\n                }\n                break;\n            case \"REMOVE_MERGE\":\n                for (const zone of cmd.target) {\n                    this.removeMerge(cmd.sheetId, zone);\n                }\n                break;\n        }\n    }\n    adaptRanges(applyChange, sheetId) {\n        const sheetIds = sheetId ? [sheetId] : Object.keys(this.merges);\n        for (const sheetId of sheetIds) {\n            this.applyRangeChangeOnSheet(sheetId, applyChange);\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getMerges(sheetId) {\n        return Object.keys(this.merges[sheetId] || {})\n            .map((mergeId) => this.getMergeById(sheetId, parseInt(mergeId, 10)))\n            .filter(isDefined);\n    }\n    getMerge({ sheetId, col, row }) {\n        const sheetMap = this.mergeCellMap[sheetId];\n        const mergeId = sheetMap ? col in sheetMap && sheetMap[col]?.[row] : undefined;\n        return mergeId ? this.getMergeById(sheetId, mergeId) : undefined;\n    }\n    getMergesInZone(sheetId, zone) {\n        const sheetMap = this.mergeCellMap[sheetId];\n        if (!sheetMap)\n            return [];\n        const mergeIds = new Set();\n        for (let col = zone.left; col <= zone.right; col++) {\n            for (let row = zone.top; row <= zone.bottom; row++) {\n                const mergeId = sheetMap[col]?.[row];\n                if (mergeId) {\n                    mergeIds.add(mergeId);\n                }\n            }\n        }\n        return Array.from(mergeIds)\n            .map((mergeId) => this.getMergeById(sheetId, mergeId))\n            .filter(isDefined);\n    }\n    /**\n     * Same as `getRangeString` but add all necessary merge to the range to make it a valid selection\n     */\n    getSelectionRangeString(range, forSheetId) {\n        const rangeImpl = RangeImpl.fromRange(range, this.getters);\n        const expandedZone = this.getters.expandZone(rangeImpl.sheetId, rangeImpl.zone);\n        const expandedRange = rangeImpl.clone({\n            zone: {\n                ...expandedZone,\n                bottom: rangeImpl.isFullCol ? undefined : expandedZone.bottom,\n                right: rangeImpl.isFullRow ? undefined : expandedZone.right,\n            },\n        });\n        const rangeString = this.getters.getRangeString(expandedRange, forSheetId);\n        if (this.isSingleCellOrMerge(rangeImpl.sheetId, rangeImpl.zone)) {\n            const { sheetName, xc } = splitReference(rangeString);\n            return getFullReference(sheetName, xc.split(\":\")[0]);\n        }\n        return rangeString;\n    }\n    /**\n     * Return true if the zone intersects an existing merge:\n     * if they have at least a common cell\n     */\n    doesIntersectMerge(sheetId, zone) {\n        for (const merge of this.getMerges(sheetId)) {\n            if (overlap(zone, merge)) {\n                return true;\n            }\n        }\n        return false;\n    }\n    /**\n     * Returns true if two columns have at least one merge in common\n     */\n    doesColumnsHaveCommonMerges(sheetId, colA, colB) {\n        const sheet = this.getters.getSheet(sheetId);\n        for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {\n            if (this.isInSameMerge(sheet.id, colA, row, colB, row)) {\n                return true;\n            }\n        }\n        return false;\n    }\n    /**\n     * Returns true if two rows have at least one merge in common\n     */\n    doesRowsHaveCommonMerges(sheetId, rowA, rowB) {\n        const sheet = this.getters.getSheet(sheetId);\n        for (let col = 0; col <= this.getters.getNumberCols(sheetId); col++) {\n            if (this.isInSameMerge(sheet.id, col, rowA, col, rowB)) {\n                return true;\n            }\n        }\n        return false;\n    }\n    /**\n     * Add all necessary merge to the current selection to make it valid\n     */\n    expandZone(sheetId, zone) {\n        let { left, right, top, bottom } = zone;\n        let result = { left, right, top, bottom };\n        for (let id in this.merges[sheetId]) {\n            const merge = this.getMergeById(sheetId, parseInt(id));\n            if (merge && overlap(merge, result)) {\n                result = union(merge, result);\n            }\n        }\n        return isEqual(result, zone) ? result : this.expandZone(sheetId, result);\n    }\n    isInSameMerge(sheetId, colA, rowA, colB, rowB) {\n        const mergeA = this.getMerge({ sheetId, col: colA, row: rowA });\n        const mergeB = this.getMerge({ sheetId, col: colB, row: rowB });\n        if (!mergeA || !mergeB) {\n            return false;\n        }\n        return isEqual(mergeA, mergeB);\n    }\n    isInMerge({ sheetId, col, row }) {\n        const sheetMap = this.mergeCellMap[sheetId];\n        return sheetMap ? col in sheetMap && Boolean(sheetMap[col]?.[row]) : false;\n    }\n    getMainCellPosition(position) {\n        const mergeZone = this.getMerge(position);\n        return mergeZone\n            ? {\n                sheetId: position.sheetId,\n                col: mergeZone.left,\n                row: mergeZone.top,\n            }\n            : position;\n    }\n    isMergeHidden(sheetId, merge) {\n        const hiddenColsGroups = this.getters.getHiddenColsGroups(sheetId);\n        const hiddenRowsGroups = this.getters.getHiddenRowsGroups(sheetId);\n        for (let group of hiddenColsGroups) {\n            if (merge.left >= group[0] && merge.right <= group[group.length - 1]) {\n                return true;\n            }\n        }\n        for (let group of hiddenRowsGroups) {\n            if (merge.top >= group[0] && merge.bottom <= group[group.length - 1]) {\n                return true;\n            }\n        }\n        return false;\n    }\n    /**\n     * Check if the zone represents a single cell or a single merge.\n     */\n    isSingleCellOrMerge(sheetId, zone) {\n        const merge = this.getMerge({ sheetId, col: zone.left, row: zone.top });\n        if (merge) {\n            return isEqual(zone, merge);\n        }\n        const { numberOfCols, numberOfRows } = zoneToDimension(zone);\n        return numberOfCols === 1 && numberOfRows === 1;\n    }\n    isMainCellPosition(position) {\n        return deepEquals(this.getMainCellPosition(position), position);\n    }\n    // ---------------------------------------------------------------------------\n    // Merges\n    // ---------------------------------------------------------------------------\n    /**\n     * Return true if the current selection requires losing state if it is merged.\n     * This happens when there is some textual content in other cells than the\n     * top left.\n     */\n    isMergeDestructive(sheetId, zone) {\n        let { left, right, top, bottom } = zone;\n        right = clip(right, 0, this.getters.getNumberCols(sheetId) - 1);\n        bottom = clip(bottom, 0, this.getters.getNumberRows(sheetId) - 1);\n        for (let row = top; row <= bottom; row++) {\n            for (let col = left; col <= right; col++) {\n                if (col !== left || row !== top) {\n                    const cell = this.getters.getCell({ sheetId, col, row });\n                    if (cell && cell.content !== \"\") {\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n    getMergeById(sheetId, mergeId) {\n        const range = this.merges[sheetId]?.[mergeId];\n        return range !== undefined ? rangeToMerge(mergeId, range) : undefined;\n    }\n    checkDestructiveMerge({ sheetId, target }) {\n        const sheet = this.getters.tryGetSheet(sheetId);\n        if (!sheet)\n            return \"Success\" /* CommandResult.Success */;\n        const isDestructive = target.some((zone) => this.isMergeDestructive(sheetId, zone));\n        return isDestructive ? \"MergeIsDestructive\" /* CommandResult.MergeIsDestructive */ : \"Success\" /* CommandResult.Success */;\n    }\n    checkOverlap({ target }) {\n        for (const zone of target) {\n            for (const zone2 of target) {\n                if (zone !== zone2 && overlap(zone, zone2)) {\n                    return \"MergeOverlap\" /* CommandResult.MergeOverlap */;\n                }\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkFrozenPanes({ sheetId, target }) {\n        const sheet = this.getters.tryGetSheet(sheetId);\n        if (!sheet)\n            return \"Success\" /* CommandResult.Success */;\n        const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n        for (const zone of target) {\n            if ((zone.left < xSplit && zone.right >= xSplit) ||\n                (zone.top < ySplit && zone.bottom >= ySplit)) {\n                return \"FrozenPaneOverlap\" /* CommandResult.FrozenPaneOverlap */;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    /**\n     * The content of a merged cell should always be empty.\n     * Except for the top-left cell.\n     */\n    checkMergedContentUpdate(cmd) {\n        const { col, row, content } = cmd;\n        if (content === undefined) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        const { col: mainCol, row: mainRow } = this.getMainCellPosition(cmd);\n        if (mainCol === col && mainRow === row) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        return \"CellIsMerged\" /* CommandResult.CellIsMerged */;\n    }\n    checkMergeExists(cmd) {\n        const { sheetId, target } = cmd;\n        for (const zone of target) {\n            const { left, top } = zone;\n            const merge = this.getMerge({ sheetId, col: left, row: top });\n            if (merge === undefined || !isEqual(zone, merge)) {\n                return \"InvalidTarget\" /* CommandResult.InvalidTarget */;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    /**\n     * Merge the current selection. Note that:\n     * - it assumes that we have a valid selection (no intersection with other\n     *   merges)\n     * - it does nothing if the merge is trivial: A1:A1\n     */\n    addMerge(sheetId, zone) {\n        let { left, right, top, bottom } = zone;\n        right = clip(right, 0, this.getters.getNumberCols(sheetId) - 1);\n        bottom = clip(bottom, 0, this.getters.getNumberRows(sheetId) - 1);\n        const tl = toXC(left, top);\n        const br = toXC(right, bottom);\n        if (tl === br) {\n            return;\n        }\n        const topLeft = this.getters.getCell({ sheetId, col: left, row: top });\n        let id = this.nextId++;\n        this.history.update(\"merges\", sheetId, id, this.getters.getRangeFromSheetXC(sheetId, zoneToXc({ left, top, right, bottom })));\n        let previousMerges = new Set();\n        for (let row = top; row <= bottom; row++) {\n            for (let col = left; col <= right; col++) {\n                if (col !== left || row !== top) {\n                    this.dispatch(\"UPDATE_CELL\", {\n                        sheetId,\n                        col,\n                        row,\n                        style: topLeft ? topLeft.style : null,\n                        content: \"\",\n                    });\n                }\n                const merge = this.getMerge({ sheetId, col, row });\n                if (merge) {\n                    previousMerges.add(merge.id);\n                }\n                this.history.update(\"mergeCellMap\", sheetId, col, row, id);\n            }\n        }\n        for (let mergeId of previousMerges) {\n            const { top, bottom, left, right } = this.getMergeById(sheetId, mergeId);\n            for (let row = top; row <= bottom; row++) {\n                for (let col = left; col <= right; col++) {\n                    const position = { sheetId, col, row };\n                    const merge = this.getMerge(position);\n                    if (!merge || merge.id !== id) {\n                        this.history.update(\"mergeCellMap\", sheetId, col, row, undefined);\n                        this.dispatch(\"CLEAR_CELL\", position);\n                    }\n                }\n            }\n            this.history.update(\"merges\", sheetId, mergeId, undefined);\n        }\n    }\n    removeMerge(sheetId, zone) {\n        const { left, top, bottom, right } = zone;\n        const merge = this.getMerge({ sheetId, col: left, row: top });\n        if (merge === undefined || !isEqual(zone, merge)) {\n            return;\n        }\n        this.history.update(\"merges\", sheetId, merge.id, undefined);\n        for (let r = top; r <= bottom; r++) {\n            for (let c = left; c <= right; c++) {\n                this.history.update(\"mergeCellMap\", sheetId, c, r, undefined);\n            }\n        }\n    }\n    /**\n     * Apply a range change on merges of a particular sheet.\n     */\n    applyRangeChangeOnSheet(sheetId, applyChange) {\n        const merges = Object.entries(this.merges[sheetId] || {});\n        for (const [mergeId, range] of merges) {\n            if (range) {\n                const currentZone = range.zone;\n                const result = applyChange(range);\n                switch (result.changeType) {\n                    case \"NONE\":\n                        break;\n                    case \"REMOVE\":\n                        this.removeMerge(sheetId, currentZone);\n                        break;\n                    default:\n                        const { numberOfCols, numberOfRows } = zoneToDimension(result.range.zone);\n                        if (numberOfCols === 1 && numberOfRows === 1) {\n                            this.removeMerge(sheetId, currentZone);\n                        }\n                        else {\n                            this.history.update(\"merges\", sheetId, parseInt(mergeId, 10), result.range);\n                        }\n                        break;\n                }\n            }\n        }\n        this.history.update(\"mergeCellMap\", sheetId, {});\n        for (const merge of this.getMerges(sheetId)) {\n            for (const { col, row } of positions(merge)) {\n                this.history.update(\"mergeCellMap\", sheetId, col, row, merge.id);\n            }\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n    import(data) {\n        const sheets = data.sheets || [];\n        for (let sheetData of sheets) {\n            this.history.update(\"merges\", sheetData.id, {});\n            this.history.update(\"mergeCellMap\", sheetData.id, {});\n            if (sheetData.merges) {\n                this.importMerges(sheetData.id, sheetData.merges);\n            }\n        }\n    }\n    importMerges(sheetId, merges) {\n        for (let merge of merges) {\n            this.addMerge(sheetId, toZone(merge));\n        }\n    }\n    export(data) {\n        for (let sheetData of data.sheets) {\n            const merges = this.merges[sheetData.id];\n            if (merges) {\n                sheetData.merges.push(...exportMerges(merges));\n            }\n        }\n    }\n    exportForExcel(data) {\n        this.export(data);\n    }\n}\nfunction exportMerges(merges) {\n    return Object.entries(merges)\n        .map(([mergeId, range]) => (range ? rangeToMerge(parseInt(mergeId, 10), range) : undefined))\n        .filter(isDefined)\n        .map((merge) => toXC(merge.left, merge.top) + \":\" + toXC(merge.right, merge.bottom));\n}\nfunction rangeToMerge(mergeId, range) {\n    return {\n        ...range.zone,\n        id: mergeId,\n    };\n}\n\nclass RangeAdapter {\n    getters;\n    providers = [];\n    constructor(getters) {\n        this.getters = getters;\n    }\n    static getters = [\n        \"extendRange\",\n        \"getRangeString\",\n        \"getRangeFromSheetXC\",\n        \"createAdaptedRanges\",\n        \"getRangeDataFromXc\",\n        \"getRangeDataFromZone\",\n        \"getRangeFromRangeData\",\n        \"getRangeFromZone\",\n        \"getRangesUnion\",\n        \"recomputeRanges\",\n        \"isRangeValid\",\n        \"removeRangesSheetPrefix\",\n    ];\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        if (cmd.type === \"MOVE_RANGES\") {\n            return cmd.target.length === 1 ? \"Success\" /* CommandResult.Success */ : \"InvalidZones\" /* CommandResult.InvalidZones */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    beforeHandle(command) { }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"REMOVE_COLUMNS_ROWS\": {\n                let start = cmd.dimension === \"COL\" ? \"left\" : \"top\";\n                let end = cmd.dimension === \"COL\" ? \"right\" : \"bottom\";\n                let dimension = cmd.dimension === \"COL\" ? \"columns\" : \"rows\";\n                const elements = [...cmd.elements];\n                elements.sort((a, b) => b - a);\n                const groups = groupConsecutive(elements);\n                this.executeOnAllRanges((range) => {\n                    if (range.sheetId !== cmd.sheetId) {\n                        return { changeType: \"NONE\" };\n                    }\n                    let newRange = range;\n                    let changeType = \"NONE\";\n                    for (let group of groups) {\n                        const min = largeMin(group);\n                        const max = largeMax(group);\n                        if (range.zone[start] <= min && min <= range.zone[end]) {\n                            const toRemove = Math.min(range.zone[end], max) - min + 1;\n                            changeType = \"RESIZE\";\n                            newRange = this.createAdaptedRange(newRange, dimension, changeType, -toRemove);\n                        }\n                        else if (range.zone[start] >= min && range.zone[end] <= max) {\n                            changeType = \"REMOVE\";\n                            newRange = range.clone({ ...this.getInvalidRange() });\n                        }\n                        else if (range.zone[start] <= max && range.zone[end] >= max) {\n                            const toRemove = max - range.zone[start] + 1;\n                            changeType = \"RESIZE\";\n                            newRange = this.createAdaptedRange(newRange, dimension, changeType, -toRemove);\n                            newRange = this.createAdaptedRange(newRange, dimension, \"MOVE\", -(range.zone[start] - min));\n                        }\n                        else if (min < range.zone[start]) {\n                            changeType = \"MOVE\";\n                            newRange = this.createAdaptedRange(newRange, dimension, changeType, -(max - min + 1));\n                        }\n                    }\n                    if (changeType !== \"NONE\") {\n                        return { changeType, range: newRange };\n                    }\n                    return { changeType: \"NONE\" };\n                }, cmd.sheetId);\n                break;\n            }\n            case \"ADD_COLUMNS_ROWS\": {\n                let start = cmd.dimension === \"COL\" ? \"left\" : \"top\";\n                let end = cmd.dimension === \"COL\" ? \"right\" : \"bottom\";\n                let dimension = cmd.dimension === \"COL\" ? \"columns\" : \"rows\";\n                this.executeOnAllRanges((range) => {\n                    if (range.sheetId !== cmd.sheetId) {\n                        return { changeType: \"NONE\" };\n                    }\n                    if (cmd.position === \"after\") {\n                        if (range.zone[start] <= cmd.base && cmd.base < range.zone[end]) {\n                            return {\n                                changeType: \"RESIZE\",\n                                range: this.createAdaptedRange(range, dimension, \"RESIZE\", cmd.quantity),\n                            };\n                        }\n                        if (cmd.base < range.zone[start]) {\n                            return {\n                                changeType: \"MOVE\",\n                                range: this.createAdaptedRange(range, dimension, \"MOVE\", cmd.quantity),\n                            };\n                        }\n                    }\n                    else {\n                        if (range.zone[start] < cmd.base && cmd.base <= range.zone[end]) {\n                            return {\n                                changeType: \"RESIZE\",\n                                range: this.createAdaptedRange(range, dimension, \"RESIZE\", cmd.quantity),\n                            };\n                        }\n                        if (cmd.base <= range.zone[start]) {\n                            return {\n                                changeType: \"MOVE\",\n                                range: this.createAdaptedRange(range, dimension, \"MOVE\", cmd.quantity),\n                            };\n                        }\n                    }\n                    return { changeType: \"NONE\" };\n                }, cmd.sheetId);\n                break;\n            }\n            case \"DELETE_SHEET\": {\n                this.executeOnAllRanges((range) => {\n                    if (range.sheetId !== cmd.sheetId) {\n                        return { changeType: \"NONE\" };\n                    }\n                    const invalidSheetName = this.getters.getSheetName(cmd.sheetId);\n                    range = range.clone({\n                        ...this.getInvalidRange(),\n                        invalidSheetName,\n                    });\n                    return { changeType: \"REMOVE\", range };\n                }, cmd.sheetId);\n                break;\n            }\n            case \"RENAME_SHEET\": {\n                this.executeOnAllRanges((range) => {\n                    if (range.sheetId === cmd.sheetId) {\n                        return { changeType: \"CHANGE\", range };\n                    }\n                    if (cmd.name && range.invalidSheetName === cmd.name) {\n                        const invalidSheetName = undefined;\n                        const sheetId = cmd.sheetId;\n                        const newRange = range.clone({ sheetId, invalidSheetName });\n                        return { changeType: \"CHANGE\", range: newRange };\n                    }\n                    return { changeType: \"NONE\" };\n                });\n                break;\n            }\n            case \"MOVE_RANGES\": {\n                const originZone = cmd.target[0];\n                this.executeOnAllRanges((range) => {\n                    if (range.sheetId !== cmd.sheetId || !isZoneInside(range.zone, originZone)) {\n                        return { changeType: \"NONE\" };\n                    }\n                    const targetSheetId = cmd.targetSheetId;\n                    const offsetX = cmd.col - originZone.left;\n                    const offsetY = cmd.row - originZone.top;\n                    const adaptedRange = this.createAdaptedRange(range, \"both\", \"MOVE\", [offsetX, offsetY]);\n                    const prefixSheet = cmd.sheetId === targetSheetId ? adaptedRange.prefixSheet : true;\n                    return {\n                        changeType: \"MOVE\",\n                        range: adaptedRange.clone({ sheetId: targetSheetId, prefixSheet }),\n                    };\n                });\n                break;\n            }\n        }\n    }\n    finalize() { }\n    /**\n     * Return a modified adapting function that verifies that after adapting a range, the range is still valid.\n     * Any range that gets adapted by the function adaptRange in parameter does so\n     * without caring if the start and end of the range in both row and column\n     * direction can be incorrect. This function ensure that an incorrect range gets removed.\n     */\n    verifyRangeRemoved(adaptRange) {\n        return (range) => {\n            const result = adaptRange(range);\n            if (result.changeType !== \"NONE\" && !isZoneValid(result.range.zone)) {\n                return { range: result.range, changeType: \"REMOVE\" };\n            }\n            return result;\n        };\n    }\n    createAdaptedRange(range, dimension, operation, by) {\n        const zone = createAdaptedZone(range.unboundedZone, dimension, operation, by);\n        const adaptedRange = range.clone({ zone });\n        return adaptedRange;\n    }\n    executeOnAllRanges(adaptRange, sheetId) {\n        const func = this.verifyRangeRemoved(adaptRange);\n        for (const provider of this.providers) {\n            provider(func, sheetId);\n        }\n    }\n    /**\n     * Stores the functions bound to each plugin to be able to iterate over all ranges of the application,\n     * without knowing any details of the internal data structure of the plugins and without storing ranges\n     * in the range adapter.\n     *\n     * @param provider a function bound to a plugin that will loop over its internal data structure to find\n     * all ranges\n     */\n    addRangeProvider(provider) {\n        this.providers.push(provider);\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    createAdaptedRanges(ranges, offsetX, offsetY, sheetId) {\n        const rangesImpl = ranges.map((range) => RangeImpl.fromRange(range, this.getters));\n        return rangesImpl.map((range) => {\n            if (!isZoneValid(range.zone)) {\n                return range;\n            }\n            const copySheetId = range.prefixSheet ? range.sheetId : sheetId;\n            const unboundZone = {\n                ...range.unboundedZone,\n                // Don't shift left if the range is a full row without header\n                left: range.isFullRow && !range.unboundedZone.hasHeader\n                    ? range.unboundedZone.left\n                    : range.unboundedZone.left + (range.parts[0].colFixed ? 0 : offsetX),\n                // Don't shift right if the range is a full row\n                right: range.isFullRow\n                    ? range.unboundedZone.right\n                    : range.unboundedZone.right +\n                        ((range.parts[1] || range.parts[0]).colFixed ? 0 : offsetX),\n                // Don't shift up if the range is a column row without header\n                top: range.isFullCol && !range.unboundedZone.hasHeader\n                    ? range.unboundedZone.top\n                    : range.unboundedZone.top + (range.parts[0].rowFixed ? 0 : offsetY),\n                // Don't shift down if the range is a full column\n                bottom: range.isFullCol\n                    ? range.unboundedZone.bottom\n                    : range.unboundedZone.bottom +\n                        ((range.parts[1] || range.parts[0]).rowFixed ? 0 : offsetY),\n            };\n            return range.clone({ sheetId: copySheetId, zone: unboundZone }).orderZone();\n        });\n    }\n    /**\n     * Remove the sheet name prefix if a range is part of the given sheet.\n     */\n    removeRangesSheetPrefix(sheetId, ranges) {\n        return ranges.map((range) => {\n            const rangeImpl = RangeImpl.fromRange(range, this.getters);\n            if (rangeImpl.prefixSheet && rangeImpl.sheetId === sheetId) {\n                return rangeImpl.clone({ prefixSheet: false });\n            }\n            return rangeImpl;\n        });\n    }\n    extendRange(range, dimension, quantity) {\n        const rangeImpl = RangeImpl.fromRange(range, this.getters);\n        const right = dimension === \"COL\" ? rangeImpl.zone.right + quantity : rangeImpl.zone.right;\n        const bottom = dimension === \"ROW\" ? rangeImpl.zone.bottom + quantity : rangeImpl.zone.bottom;\n        const zone = {\n            left: rangeImpl.zone.left,\n            top: rangeImpl.zone.top,\n            right: rangeImpl.isFullRow ? undefined : right,\n            bottom: rangeImpl.isFullCol ? undefined : bottom,\n        };\n        return new RangeImpl({ ...rangeImpl, zone }, this.getters.getSheetSize).orderZone();\n    }\n    /**\n     * Creates a range from a XC reference that can contain a sheet reference\n     * @param defaultSheetId the sheet to default to if the sheetXC parameter does not contain a sheet reference (usually the active sheet Id)\n     * @param sheetXC the string description of a range, in the form SheetName!XC:XC\n     */\n    getRangeFromSheetXC(defaultSheetId, sheetXC) {\n        if (!rangeReference.test(sheetXC) || !this.getters.tryGetSheet(defaultSheetId)) {\n            return new RangeImpl({\n                sheetId: \"\",\n                zone: { left: -1, top: -1, right: -1, bottom: -1 },\n                parts: [],\n                invalidXc: sheetXC,\n                prefixSheet: false,\n            }, this.getters.getSheetSize);\n        }\n        let sheetName;\n        let xc = sheetXC;\n        let prefixSheet = false;\n        if (sheetXC.includes(\"!\")) {\n            ({ xc, sheetName } = splitReference(sheetXC));\n            if (sheetName) {\n                prefixSheet = true;\n            }\n        }\n        const zone = toUnboundedZone(xc);\n        const parts = RangeImpl.getRangeParts(xc, zone);\n        const invalidSheetName = sheetName && !this.getters.getSheetIdByName(sheetName) ? sheetName : undefined;\n        const sheetId = this.getters.getSheetIdByName(sheetName) || defaultSheetId;\n        const rangeInterface = { prefixSheet, zone, sheetId, invalidSheetName, parts };\n        return new RangeImpl(rangeInterface, this.getters.getSheetSize).orderZone();\n    }\n    /**\n     * Gets the string that represents the range as it is at the moment of the call.\n     * The string will be prefixed with the sheet name if the call specified a sheet id in `forSheetId`\n     * different than the sheet on which the range has been created.\n     *\n     * @param range the range (received from getRangeFromXC or getRangeFromZone)\n     * @param forSheetId the id of the sheet where the range string is supposed to be used.\n     * @param options\n     * @param options.useFixedReference if true, the range will be returned with fixed row and column\n     */\n    getRangeString(range, forSheetId, options = { useFixedReference: false }) {\n        if (!range) {\n            return CellErrorType.InvalidReference;\n        }\n        if (range.invalidXc) {\n            return range.invalidXc;\n        }\n        if (!this.getters.tryGetSheet(range.sheetId)) {\n            return CellErrorType.InvalidReference;\n        }\n        if (range.zone.bottom - range.zone.top < 0 || range.zone.right - range.zone.left < 0) {\n            return CellErrorType.InvalidReference;\n        }\n        if (range.zone.left < 0 || range.zone.top < 0) {\n            return CellErrorType.InvalidReference;\n        }\n        const rangeImpl = RangeImpl.fromRange(range, this.getters);\n        let prefixSheet = rangeImpl.sheetId !== forSheetId || rangeImpl.invalidSheetName || rangeImpl.prefixSheet;\n        let sheetName = \"\";\n        if (prefixSheet) {\n            if (rangeImpl.invalidSheetName) {\n                sheetName = rangeImpl.invalidSheetName;\n            }\n            else {\n                sheetName = getCanonicalSymbolName(this.getters.getSheetName(rangeImpl.sheetId));\n            }\n        }\n        if (prefixSheet && !sheetName) {\n            return CellErrorType.InvalidReference;\n        }\n        let rangeString = this.getRangePartString(rangeImpl, 0, options);\n        if (rangeImpl.parts && rangeImpl.parts.length === 2) {\n            // this if converts A2:A2 into A2 except if any part of the original range had fixed row or column (with $)\n            if (rangeImpl.zone.top !== rangeImpl.zone.bottom ||\n                rangeImpl.zone.left !== rangeImpl.zone.right ||\n                rangeImpl.parts[0].rowFixed ||\n                rangeImpl.parts[0].colFixed ||\n                rangeImpl.parts[1].rowFixed ||\n                rangeImpl.parts[1].colFixed) {\n                rangeString += \":\";\n                rangeString += this.getRangePartString(rangeImpl, 1, options);\n            }\n        }\n        return `${prefixSheet ? sheetName + \"!\" : \"\"}${rangeString}`;\n    }\n    getRangeDataFromXc(sheetId, xc) {\n        return this.getters.getRangeFromSheetXC(sheetId, xc).rangeData;\n    }\n    getRangeDataFromZone(sheetId, zone) {\n        zone = this.getters.getUnboundedZone(sheetId, zone);\n        return { _sheetId: sheetId, _zone: zone };\n    }\n    getRangeFromZone(sheetId, zone) {\n        return new RangeImpl({\n            sheetId,\n            zone,\n            parts: [\n                { colFixed: false, rowFixed: false },\n                { colFixed: false, rowFixed: false },\n            ],\n            prefixSheet: false,\n        }, this.getters.getSheetSize);\n    }\n    /**\n     * Allows you to recompute ranges from the same sheet\n     */\n    recomputeRanges(ranges, rangesToRemove) {\n        const zones = ranges.map((range) => RangeImpl.fromRange(range, this.getters).unboundedZone);\n        const zonesToRemove = rangesToRemove.map((range) => RangeImpl.fromRange(range, this.getters).unboundedZone);\n        return recomputeZones(zones, zonesToRemove).map((zone) => this.getRangeFromZone(ranges[0].sheetId, zone));\n    }\n    getRangeFromRangeData(data) {\n        const rangeInterface = {\n            prefixSheet: false,\n            zone: data._zone,\n            sheetId: data._sheetId,\n            invalidSheetName: undefined,\n            parts: [\n                { colFixed: false, rowFixed: false },\n                { colFixed: false, rowFixed: false },\n            ],\n        };\n        return new RangeImpl(rangeInterface, this.getters.getSheetSize);\n    }\n    isRangeValid(rangeStr) {\n        if (!rangeStr) {\n            return false;\n        }\n        const { xc, sheetName } = splitReference(rangeStr);\n        return (xc.match(rangeReference) !== null &&\n            (!sheetName || this.getters.getSheetIdByName(sheetName) !== undefined));\n    }\n    getRangesUnion(ranges) {\n        const zones = ranges.map((range) => RangeImpl.fromRange(range, this.getters).unboundedZone);\n        const unionOfZones = unionUnboundedZones(...zones);\n        return this.getRangeFromZone(ranges[0].sheetId, unionOfZones);\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    /**\n     * Get a Xc string that represent a part of a range\n     */\n    getRangePartString(range, part, options = { useFixedReference: false }) {\n        const colFixed = range.parts && range.parts[part]?.colFixed ? \"$\" : \"\";\n        const col = part === 0 ? numberToLetters(range.zone.left) : numberToLetters(range.zone.right);\n        const rowFixed = range.parts && range.parts[part]?.rowFixed ? \"$\" : \"\";\n        const row = part === 0 ? String(range.zone.top + 1) : String(range.zone.bottom + 1);\n        let str = \"\";\n        if (range.isFullCol && !options.useFixedReference) {\n            if (part === 0 && range.unboundedZone.hasHeader) {\n                str = colFixed + col + rowFixed + row;\n            }\n            else {\n                str = colFixed + col;\n            }\n        }\n        else if (range.isFullRow && !options.useFixedReference) {\n            if (part === 0 && range.unboundedZone.hasHeader) {\n                str = colFixed + col + rowFixed + row;\n            }\n            else {\n                str = rowFixed + row;\n            }\n        }\n        else {\n            str = colFixed + col + rowFixed + row;\n        }\n        return str;\n    }\n    getInvalidRange() {\n        return {\n            parts: [],\n            prefixSheet: false,\n            zone: { left: -1, top: -1, right: -1, bottom: -1 },\n            sheetId: \"\",\n            invalidXc: CellErrorType.InvalidReference,\n        };\n    }\n}\n\nclass SheetPlugin extends CorePlugin {\n    static getters = [\n        \"getSheetName\",\n        \"tryGetSheetName\",\n        \"getSheet\",\n        \"tryGetSheet\",\n        \"getSheetIdByName\",\n        \"getSheetIds\",\n        \"getVisibleSheetIds\",\n        \"isSheetVisible\",\n        \"doesHeaderExist\",\n        \"doesHeadersExist\",\n        \"getCell\",\n        \"getCellPosition\",\n        \"getColsZone\",\n        \"getRowCells\",\n        \"getRowsZone\",\n        \"getNumberCols\",\n        \"getNumberRows\",\n        \"getNumberHeaders\",\n        \"getGridLinesVisibility\",\n        \"getNextSheetName\",\n        \"getSheetSize\",\n        \"getSheetZone\",\n        \"getPaneDivisions\",\n        \"checkZonesExistInSheet\",\n        \"getCommandZones\",\n        \"getUnboundedZone\",\n        \"checkElementsIncludeAllNonFrozenHeaders\",\n    ];\n    sheetIdsMapName = {};\n    orderedSheetIds = [];\n    sheets = {};\n    cellPosition = {};\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        const genericChecks = this.chainValidations(this.checkSheetExists, this.checkZonesAreInSheet)(cmd);\n        if (genericChecks !== \"Success\" /* CommandResult.Success */) {\n            return genericChecks;\n        }\n        switch (cmd.type) {\n            case \"HIDE_SHEET\": {\n                if (this.getVisibleSheetIds().length === 1) {\n                    return \"NotEnoughSheets\" /* CommandResult.NotEnoughSheets */;\n                }\n                return \"Success\" /* CommandResult.Success */;\n            }\n            case \"CREATE_SHEET\": {\n                return this.checkValidations(cmd, this.checkSheetName, this.checkSheetPosition);\n            }\n            case \"MOVE_SHEET\":\n                try {\n                    const currentIndex = this.orderedSheetIds.findIndex((id) => id === cmd.sheetId);\n                    this.findIndexOfTargetSheet(currentIndex, cmd.delta);\n                    return \"Success\" /* CommandResult.Success */;\n                }\n                catch (e) {\n                    return \"WrongSheetMove\" /* CommandResult.WrongSheetMove */;\n                }\n            case \"RENAME_SHEET\":\n                return this.isRenameAllowed(cmd);\n            case \"COLOR_SHEET\":\n                return !cmd.color || isColorValid(cmd.color)\n                    ? \"Success\" /* CommandResult.Success */\n                    : \"InvalidColor\" /* CommandResult.InvalidColor */;\n            case \"DELETE_SHEET\":\n                return this.orderedSheetIds.length > 1\n                    ? \"Success\" /* CommandResult.Success */\n                    : \"NotEnoughSheets\" /* CommandResult.NotEnoughSheets */;\n            case \"ADD_COLUMNS_ROWS\":\n                if (!this.doesHeaderExist(cmd.sheetId, cmd.dimension, cmd.base)) {\n                    return \"InvalidHeaderIndex\" /* CommandResult.InvalidHeaderIndex */;\n                }\n                else if (cmd.quantity <= 0) {\n                    return \"InvalidQuantity\" /* CommandResult.InvalidQuantity */;\n                }\n                return \"Success\" /* CommandResult.Success */;\n            case \"REMOVE_COLUMNS_ROWS\": {\n                const min = largeMin(cmd.elements);\n                const max = largeMax(cmd.elements);\n                if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {\n                    return \"InvalidHeaderIndex\" /* CommandResult.InvalidHeaderIndex */;\n                }\n                else if (this.checkElementsIncludeAllNonFrozenHeaders(cmd.sheetId, cmd.dimension, cmd.elements)) {\n                    return \"NotEnoughElements\" /* CommandResult.NotEnoughElements */;\n                }\n                else {\n                    return \"Success\" /* CommandResult.Success */;\n                }\n            }\n            case \"FREEZE_ROWS\": {\n                return this.checkValidations(cmd, this.checkRowFreezeQuantity, this.checkRowFreezeOverlapMerge);\n            }\n            case \"FREEZE_COLUMNS\": {\n                return this.checkValidations(cmd, this.checkColFreezeQuantity, this.checkColFreezeOverlapMerge);\n            }\n            default:\n                return \"Success\" /* CommandResult.Success */;\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SET_GRID_LINES_VISIBILITY\":\n                this.setGridLinesVisibility(cmd.sheetId, cmd.areGridLinesVisible);\n                break;\n            case \"CREATE_SHEET\":\n                const sheet = this.createSheet(cmd.sheetId, cmd.name || this.getNextSheetName(), cmd.cols || 26, cmd.rows || 100, cmd.position);\n                this.history.update(\"sheetIdsMapName\", sheet.name, sheet.id);\n                break;\n            case \"MOVE_SHEET\":\n                this.moveSheet(cmd.sheetId, cmd.delta);\n                break;\n            case \"RENAME_SHEET\":\n                this.renameSheet(this.sheets[cmd.sheetId], cmd.name);\n                break;\n            case \"COLOR_SHEET\":\n                this.history.update(\"sheets\", cmd.sheetId, \"color\", cmd.color);\n                break;\n            case \"HIDE_SHEET\":\n                this.hideSheet(cmd.sheetId);\n                break;\n            case \"SHOW_SHEET\":\n                this.showSheet(cmd.sheetId);\n                break;\n            case \"DUPLICATE_SHEET\":\n                this.duplicateSheet(cmd.sheetId, cmd.sheetIdTo);\n                break;\n            case \"DELETE_SHEET\":\n                this.deleteSheet(this.sheets[cmd.sheetId]);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\":\n                if (cmd.dimension === \"COL\") {\n                    this.removeColumns(this.sheets[cmd.sheetId], [...cmd.elements]);\n                }\n                else {\n                    this.removeRows(this.sheets[cmd.sheetId], [...cmd.elements]);\n                }\n                break;\n            case \"ADD_COLUMNS_ROWS\":\n                if (cmd.dimension === \"COL\") {\n                    this.addColumns(this.sheets[cmd.sheetId], cmd.base, cmd.position, cmd.quantity);\n                }\n                else {\n                    this.addRows(this.sheets[cmd.sheetId], cmd.base, cmd.position, cmd.quantity);\n                }\n                break;\n            case \"UPDATE_CELL_POSITION\":\n                this.updateCellPosition(cmd);\n                break;\n            case \"FREEZE_COLUMNS\":\n                this.setPaneDivisions(cmd.sheetId, cmd.quantity, \"COL\");\n                break;\n            case \"FREEZE_ROWS\":\n                this.setPaneDivisions(cmd.sheetId, cmd.quantity, \"ROW\");\n                break;\n            case \"UNFREEZE_ROWS\":\n                this.setPaneDivisions(cmd.sheetId, 0, \"ROW\");\n                break;\n            case \"UNFREEZE_COLUMNS\":\n                this.setPaneDivisions(cmd.sheetId, 0, \"COL\");\n                break;\n            case \"UNFREEZE_COLUMNS_ROWS\":\n                this.setPaneDivisions(cmd.sheetId, 0, \"COL\");\n                this.setPaneDivisions(cmd.sheetId, 0, \"ROW\");\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n    import(data) {\n        // we need to fill the sheetIds mapping first, because otherwise formulas\n        // that depends on a sheet not already imported will not be able to be\n        // compiled\n        for (let sheet of data.sheets) {\n            this.sheetIdsMapName[sheet.name] = sheet.id;\n        }\n        for (let sheetData of data.sheets) {\n            const name = sheetData.name || _t(\"Sheet\") + (Object.keys(this.sheets).length + 1);\n            const { colNumber, rowNumber } = this.getImportedSheetSize(sheetData);\n            const sheet = {\n                id: sheetData.id,\n                name: name,\n                numberOfCols: colNumber,\n                rows: createDefaultRows(rowNumber),\n                areGridLinesVisible: sheetData.areGridLinesVisible === undefined ? true : sheetData.areGridLinesVisible,\n                isVisible: sheetData.isVisible,\n                panes: {\n                    xSplit: sheetData.panes?.xSplit || 0,\n                    ySplit: sheetData.panes?.ySplit || 0,\n                },\n                color: sheetData.color,\n            };\n            this.orderedSheetIds.push(sheet.id);\n            this.sheets[sheet.id] = sheet;\n        }\n    }\n    exportSheets(data) {\n        data.sheets = this.orderedSheetIds.filter(isDefined).map((id) => {\n            const sheet = this.sheets[id];\n            const sheetData = {\n                id: sheet.id,\n                name: sheet.name,\n                colNumber: sheet.numberOfCols,\n                rowNumber: this.getters.getNumberRows(sheet.id),\n                rows: {},\n                cols: {},\n                merges: [],\n                cells: {},\n                styles: {},\n                formats: {},\n                borders: {},\n                conditionalFormats: [],\n                figures: [],\n                tables: [],\n                areGridLinesVisible: sheet.areGridLinesVisible === undefined ? true : sheet.areGridLinesVisible,\n                isVisible: sheet.isVisible,\n                color: sheet.color,\n            };\n            if (sheet.panes.xSplit || sheet.panes.ySplit) {\n                sheetData.panes = sheet.panes;\n            }\n            return sheetData;\n        });\n    }\n    export(data) {\n        this.exportSheets(data);\n    }\n    exportForExcel(data) {\n        this.exportSheets(data);\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getGridLinesVisibility(sheetId) {\n        return this.getSheet(sheetId).areGridLinesVisible;\n    }\n    tryGetSheet(sheetId) {\n        return this.sheets[sheetId];\n    }\n    getSheet(sheetId) {\n        const sheet = this.sheets[sheetId];\n        if (!sheet) {\n            throw new Error(`Sheet ${sheetId} not found.`);\n        }\n        return sheet;\n    }\n    isSheetVisible(sheetId) {\n        return this.getSheet(sheetId).isVisible;\n    }\n    /**\n     * Return the sheet name. Throw if the sheet is not found.\n     */\n    getSheetName(sheetId) {\n        return this.getSheet(sheetId).name;\n    }\n    /**\n     * Return the sheet name or undefined if the sheet doesn't exist.\n     */\n    tryGetSheetName(sheetId) {\n        return this.tryGetSheet(sheetId)?.name;\n    }\n    getSheetIdByName(name) {\n        if (name) {\n            const unquotedName = getUnquotedSheetName(name);\n            for (const key in this.sheetIdsMapName) {\n                if (key.toUpperCase() === unquotedName.toUpperCase()) {\n                    return this.sheetIdsMapName[key];\n                }\n            }\n        }\n        return undefined;\n    }\n    getSheetIds() {\n        return this.orderedSheetIds;\n    }\n    getVisibleSheetIds() {\n        return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));\n    }\n    doesHeaderExist(sheetId, dimension, index) {\n        return dimension === \"COL\"\n            ? index >= 0 && index < this.getNumberCols(sheetId)\n            : index >= 0 && index < this.getNumberRows(sheetId);\n    }\n    doesHeadersExist(sheetId, dimension, headerIndexes) {\n        return headerIndexes.every((index) => this.doesHeaderExist(sheetId, dimension, index));\n    }\n    getCell({ sheetId, col, row }) {\n        const sheet = this.tryGetSheet(sheetId);\n        const cellId = sheet?.rows[row]?.cells[col];\n        if (cellId === undefined) {\n            return undefined;\n        }\n        return this.getters.getCellById(cellId);\n    }\n    getColsZone(sheetId, start, end) {\n        return {\n            top: 0,\n            bottom: this.getNumberRows(sheetId) - 1,\n            left: start,\n            right: end,\n        };\n    }\n    getRowCells(sheetId, row) {\n        return Object.values(this.getSheet(sheetId).rows[row]?.cells).filter(isDefined);\n    }\n    getRowsZone(sheetId, start, end) {\n        return {\n            top: start,\n            bottom: end,\n            left: 0,\n            right: this.getSheet(sheetId).numberOfCols - 1,\n        };\n    }\n    getCellPosition(cellId) {\n        const cell = this.cellPosition[cellId];\n        if (!cell) {\n            throw new Error(`asking for a cell position that doesn't exist, cell id: ${cellId}`);\n        }\n        return cell;\n    }\n    getNumberCols(sheetId) {\n        return this.getSheet(sheetId).numberOfCols;\n    }\n    getNumberRows(sheetId) {\n        return this.getSheet(sheetId).rows.length;\n    }\n    getNumberHeaders(sheetId, dimension) {\n        return dimension === \"COL\" ? this.getNumberCols(sheetId) : this.getNumberRows(sheetId);\n    }\n    getNextSheetName(baseName = \"Sheet\") {\n        let i = 1;\n        const names = this.orderedSheetIds.map(this.getSheetName.bind(this));\n        let name = `${baseName}${i}`;\n        while (names.includes(name)) {\n            name = `${baseName}${i}`;\n            i++;\n        }\n        return name;\n    }\n    getSheetSize(sheetId) {\n        return {\n            numberOfRows: this.getNumberRows(sheetId),\n            numberOfCols: this.getNumberCols(sheetId),\n        };\n    }\n    getSheetZone(sheetId) {\n        return {\n            top: 0,\n            left: 0,\n            bottom: this.getNumberRows(sheetId) - 1,\n            right: this.getNumberCols(sheetId) - 1,\n        };\n    }\n    getUnboundedZone(sheetId, zone) {\n        if (zone.bottom === undefined || zone.right === undefined) {\n            return zone;\n        }\n        const isFullRow = zone.left === 0 && zone.right === this.getNumberCols(sheetId) - 1;\n        const isFullCol = zone.top === 0 && zone.bottom === this.getNumberRows(sheetId) - 1;\n        return {\n            ...zone,\n            bottom: isFullCol ? undefined : zone.bottom,\n            // cannot be unbounded in the 2 dimensions at once\n            right: isFullRow && !isFullCol ? undefined : zone.right,\n        };\n    }\n    getPaneDivisions(sheetId) {\n        return this.getSheet(sheetId).panes;\n    }\n    setPaneDivisions(sheetId, base, dimension) {\n        const panes = { ...this.getPaneDivisions(sheetId) };\n        if (dimension === \"COL\") {\n            panes.xSplit = base;\n        }\n        else if (dimension === \"ROW\") {\n            panes.ySplit = base;\n        }\n        this.history.update(\"sheets\", sheetId, \"panes\", panes);\n    }\n    /**\n     * Checks if all non-frozen header indices are present in the provided elements of selected rows/columns.\n     * This validation ensures that all rows or columns cannot be deleted when frozen panes exist.\n     */\n    checkElementsIncludeAllNonFrozenHeaders(sheetId, dimension, elements) {\n        const paneDivisions = this.getters.getPaneDivisions(sheetId);\n        const startIndex = dimension === \"ROW\" ? paneDivisions.ySplit : paneDivisions.xSplit;\n        const endIndex = this.getters.getNumberHeaders(sheetId, dimension);\n        if (!startIndex) {\n            return false;\n        }\n        const indicesToCheck = range(startIndex, endIndex);\n        return includesAll(elements, indicesToCheck);\n    }\n    // ---------------------------------------------------------------------------\n    // Row/Col manipulation\n    // ---------------------------------------------------------------------------\n    getCommandZones(cmd) {\n        const zones = [];\n        if (\"zone\" in cmd) {\n            zones.push(cmd.zone);\n        }\n        if (\"target\" in cmd) {\n            zones.push(...cmd.target);\n        }\n        if (\"ranges\" in cmd) {\n            zones.push(...cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData).zone));\n        }\n        if (\"col\" in cmd && \"row\" in cmd) {\n            zones.push({ top: cmd.row, left: cmd.col, bottom: cmd.row, right: cmd.col });\n        }\n        return zones;\n    }\n    /**\n     * Check if zones in the command are well formed and\n     * not outside the sheet.\n     */\n    checkZonesExistInSheet(sheetId, zones) {\n        if (!zones.every(isZoneValid))\n            return \"InvalidRange\" /* CommandResult.InvalidRange */;\n        if (zones.length) {\n            const sheetZone = this.getSheetZone(sheetId);\n            return zones.every((zone) => isZoneInside(zone, sheetZone))\n                ? \"Success\" /* CommandResult.Success */\n                : \"TargetOutOfSheet\" /* CommandResult.TargetOutOfSheet */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    updateCellPosition(cmd) {\n        const { sheetId, cellId, col, row } = cmd;\n        if (cellId) {\n            this.setNewPosition(cellId, sheetId, col, row);\n        }\n        else {\n            this.clearPosition(sheetId, col, row);\n        }\n    }\n    /**\n     * Set the cell at a new position and clear its previous position.\n     */\n    setNewPosition(cellId, sheetId, col, row) {\n        const currentPosition = this.cellPosition[cellId];\n        if (currentPosition) {\n            this.clearPosition(sheetId, currentPosition.col, currentPosition.row);\n        }\n        this.history.update(\"cellPosition\", cellId, {\n            row: row,\n            col: col,\n            sheetId: sheetId,\n        });\n        this.history.update(\"sheets\", sheetId, \"rows\", row, \"cells\", col, cellId);\n    }\n    /**\n     * Remove the cell at the given position (if there's one)\n     */\n    clearPosition(sheetId, col, row) {\n        const cellId = this.sheets[sheetId]?.rows[row].cells[col];\n        if (cellId) {\n            this.history.update(\"cellPosition\", cellId, undefined);\n            this.history.update(\"sheets\", sheetId, \"rows\", row, \"cells\", col, undefined);\n        }\n    }\n    setGridLinesVisibility(sheetId, areGridLinesVisible) {\n        this.history.update(\"sheets\", sheetId, \"areGridLinesVisible\", areGridLinesVisible);\n    }\n    createSheet(id, name, colNumber, rowNumber, position) {\n        const sheet = {\n            id,\n            name,\n            numberOfCols: colNumber,\n            rows: createDefaultRows(rowNumber),\n            areGridLinesVisible: true,\n            isVisible: true,\n            panes: {\n                xSplit: 0,\n                ySplit: 0,\n            },\n        };\n        const orderedSheetIds = this.orderedSheetIds.slice();\n        orderedSheetIds.splice(position, 0, sheet.id);\n        const sheets = this.sheets;\n        this.history.update(\"orderedSheetIds\", orderedSheetIds);\n        this.history.update(\"sheets\", Object.assign({}, sheets, { [sheet.id]: sheet }));\n        return sheet;\n    }\n    moveSheet(sheetId, delta) {\n        const orderedSheetIds = this.orderedSheetIds.slice();\n        const currentIndex = orderedSheetIds.findIndex((id) => id === sheetId);\n        const sheet = orderedSheetIds.splice(currentIndex, 1);\n        let index = this.findIndexOfTargetSheet(currentIndex, delta);\n        orderedSheetIds.splice(index, 0, sheet[0]);\n        this.history.update(\"orderedSheetIds\", orderedSheetIds);\n    }\n    findIndexOfTargetSheet(currentIndex, deltaIndex) {\n        while (deltaIndex != 0 && 0 <= currentIndex && currentIndex <= this.orderedSheetIds.length) {\n            if (deltaIndex > 0) {\n                currentIndex++;\n                if (this.isSheetVisible(this.orderedSheetIds[currentIndex])) {\n                    deltaIndex--;\n                }\n            }\n            else if (deltaIndex < 0) {\n                currentIndex--;\n                if (this.isSheetVisible(this.orderedSheetIds[currentIndex])) {\n                    deltaIndex++;\n                }\n            }\n        }\n        if (deltaIndex === 0) {\n            return currentIndex;\n        }\n        throw new Error(_t(\"There is not enough visible sheets\"));\n    }\n    checkSheetName(cmd) {\n        const originalSheetName = this.getters.tryGetSheetName(cmd.sheetId);\n        if (originalSheetName !== undefined && cmd.name === originalSheetName) {\n            return \"UnchangedSheetName\" /* CommandResult.UnchangedSheetName */;\n        }\n        const { orderedSheetIds, sheets } = this;\n        const name = cmd.name && cmd.name.trim().toLowerCase();\n        if (orderedSheetIds.find((id) => sheets[id]?.name.toLowerCase() === name && id !== cmd.sheetId)) {\n            return \"DuplicatedSheetName\" /* CommandResult.DuplicatedSheetName */;\n        }\n        if (FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX.test(name)) {\n            return \"ForbiddenCharactersInSheetName\" /* CommandResult.ForbiddenCharactersInSheetName */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkSheetPosition(cmd) {\n        const { orderedSheetIds } = this;\n        if (cmd.position > orderedSheetIds.length || cmd.position < 0) {\n            return \"WrongSheetPosition\" /* CommandResult.WrongSheetPosition */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkRowFreezeQuantity(cmd) {\n        return cmd.quantity >= 1 && cmd.quantity < this.getNumberRows(cmd.sheetId)\n            ? \"Success\" /* CommandResult.Success */\n            : \"InvalidFreezeQuantity\" /* CommandResult.InvalidFreezeQuantity */;\n    }\n    checkColFreezeQuantity(cmd) {\n        return cmd.quantity >= 1 && cmd.quantity < this.getNumberCols(cmd.sheetId)\n            ? \"Success\" /* CommandResult.Success */\n            : \"InvalidFreezeQuantity\" /* CommandResult.InvalidFreezeQuantity */;\n    }\n    checkRowFreezeOverlapMerge(cmd) {\n        const merges = this.getters.getMerges(cmd.sheetId);\n        for (let merge of merges) {\n            if (merge.top < cmd.quantity && cmd.quantity <= merge.bottom) {\n                return \"MergeOverlap\" /* CommandResult.MergeOverlap */;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkColFreezeOverlapMerge(cmd) {\n        const merges = this.getters.getMerges(cmd.sheetId);\n        for (let merge of merges) {\n            if (merge.left < cmd.quantity && cmd.quantity <= merge.right) {\n                return \"MergeOverlap\" /* CommandResult.MergeOverlap */;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    isRenameAllowed(cmd) {\n        const name = cmd.name && cmd.name.trim().toLowerCase();\n        if (!name) {\n            return \"MissingSheetName\" /* CommandResult.MissingSheetName */;\n        }\n        return this.checkSheetName(cmd);\n    }\n    renameSheet(sheet, name) {\n        const oldName = sheet.name;\n        this.history.update(\"sheets\", sheet.id, \"name\", name.trim());\n        const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);\n        delete sheetIdsMapName[oldName];\n        sheetIdsMapName[name] = sheet.id;\n        this.history.update(\"sheetIdsMapName\", sheetIdsMapName);\n    }\n    hideSheet(sheetId) {\n        this.history.update(\"sheets\", sheetId, \"isVisible\", false);\n    }\n    showSheet(sheetId) {\n        this.history.update(\"sheets\", sheetId, \"isVisible\", true);\n    }\n    duplicateSheet(fromId, toId) {\n        const sheet = this.getSheet(fromId);\n        const toName = this.getDuplicateSheetName(sheet.name);\n        const newSheet = deepCopy(sheet);\n        newSheet.id = toId;\n        newSheet.name = toName;\n        for (let col = 0; col <= newSheet.numberOfCols; col++) {\n            for (let row = 0; row <= newSheet.rows.length; row++) {\n                if (newSheet.rows[row]) {\n                    newSheet.rows[row].cells[col] = undefined;\n                }\n            }\n        }\n        const orderedSheetIds = this.orderedSheetIds.slice();\n        const currentIndex = orderedSheetIds.indexOf(fromId);\n        orderedSheetIds.splice(currentIndex + 1, 0, newSheet.id);\n        this.history.update(\"orderedSheetIds\", orderedSheetIds);\n        this.history.update(\"sheets\", Object.assign({}, this.sheets, { [newSheet.id]: newSheet }));\n        for (const cell of Object.values(this.getters.getCells(fromId))) {\n            const { col, row } = this.getCellPosition(cell.id);\n            this.dispatch(\"UPDATE_CELL\", {\n                sheetId: newSheet.id,\n                col,\n                row,\n                content: cell.content,\n                format: cell.format,\n                style: cell.style,\n            });\n        }\n        const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);\n        sheetIdsMapName[newSheet.name] = newSheet.id;\n        this.history.update(\"sheetIdsMapName\", sheetIdsMapName);\n    }\n    getDuplicateSheetName(sheetName) {\n        let i = 1;\n        const names = this.orderedSheetIds.map(this.getSheetName.bind(this));\n        const baseName = _t(\"Copy of %s\", sheetName);\n        let name = baseName.toString();\n        while (names.includes(name)) {\n            name = `${baseName} (${i})`;\n            i++;\n        }\n        return name;\n    }\n    deleteSheet(sheet) {\n        const name = sheet.name;\n        const sheets = Object.assign({}, this.sheets);\n        delete sheets[sheet.id];\n        this.history.update(\"sheets\", sheets);\n        const orderedSheetIds = this.orderedSheetIds.slice();\n        const currentIndex = orderedSheetIds.indexOf(sheet.id);\n        orderedSheetIds.splice(currentIndex, 1);\n        this.history.update(\"orderedSheetIds\", orderedSheetIds);\n        const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);\n        delete sheetIdsMapName[name];\n        this.history.update(\"sheetIdsMapName\", sheetIdsMapName);\n    }\n    /**\n     * Delete column. This requires a lot of handling:\n     * - Update all the formulas in all sheets\n     * - Move the cells\n     * - Update the cols/rows (size, number, (cells), ...)\n     * - Reevaluate the cells\n     *\n     * @param sheet ID of the sheet on which deletion should be applied\n     * @param columns Columns to delete\n     */\n    removeColumns(sheet, columns) {\n        // This is necessary because we have to delete elements in correct order:\n        // begin with the end.\n        columns.sort((a, b) => b - a);\n        for (let column of columns) {\n            // Move the cells.\n            this.moveCellOnColumnsDeletion(sheet, column);\n        }\n        const numberOfCols = this.sheets[sheet.id].numberOfCols;\n        this.history.update(\"sheets\", sheet.id, \"numberOfCols\", numberOfCols - columns.length);\n        const count = columns.filter((col) => col < sheet.panes.xSplit).length;\n        if (count) {\n            this.setPaneDivisions(sheet.id, sheet.panes.xSplit - count, \"COL\");\n        }\n    }\n    /**\n     * Delete row. This requires a lot of handling:\n     * - Update the merges\n     * - Update all the formulas in all sheets\n     * - Move the cells\n     * - Update the cols/rows (size, number, (cells), ...)\n     * - Reevaluate the cells\n     *\n     * @param sheet ID of the sheet on which deletion should be applied\n     * @param rows Rows to delete\n     */\n    removeRows(sheet, rows) {\n        // This is necessary because we have to delete elements in correct order:\n        // begin with the end.\n        rows.sort((a, b) => b - a);\n        for (let group of groupConsecutive(rows)) {\n            // indexes are sorted in the descending order\n            const from = group[group.length - 1];\n            const to = group[0];\n            // Move the cells.\n            this.moveCellOnRowsDeletion(sheet, from, to);\n            // Effectively delete the rows\n            this.updateRowsStructureOnDeletion(sheet, from, to);\n        }\n        const count = rows.filter((row) => row < sheet.panes.ySplit).length;\n        if (count) {\n            this.setPaneDivisions(sheet.id, sheet.panes.ySplit - count, \"ROW\");\n        }\n    }\n    addColumns(sheet, column, position, quantity) {\n        const index = position === \"before\" ? column : column + 1;\n        // Move the cells.\n        this.moveCellsOnAddition(sheet, index, quantity, \"columns\");\n        const numberOfCols = this.sheets[sheet.id].numberOfCols;\n        this.history.update(\"sheets\", sheet.id, \"numberOfCols\", numberOfCols + quantity);\n        if (index < sheet.panes.xSplit) {\n            this.setPaneDivisions(sheet.id, sheet.panes.xSplit + quantity, \"COL\");\n        }\n    }\n    addRows(sheet, row, position, quantity) {\n        const index = position === \"before\" ? row : row + 1;\n        this.addEmptyRows(sheet, quantity);\n        // Move the cells.\n        this.moveCellsOnAddition(sheet, index, quantity, \"rows\");\n        if (index < sheet.panes.ySplit) {\n            this.setPaneDivisions(sheet.id, sheet.panes.ySplit + quantity, \"ROW\");\n        }\n    }\n    moveCellOnColumnsDeletion(sheet, deletedColumn) {\n        this.dispatch(\"CLEAR_CELLS\", {\n            sheetId: sheet.id,\n            target: [\n                {\n                    left: deletedColumn,\n                    top: 0,\n                    right: deletedColumn,\n                    bottom: sheet.rows.length - 1,\n                },\n            ],\n        });\n        for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {\n            const row = sheet.rows[rowIndex];\n            for (let i in row.cells) {\n                const colIndex = Number(i);\n                const cellId = row.cells[i];\n                if (cellId) {\n                    if (colIndex > deletedColumn) {\n                        this.setNewPosition(cellId, sheet.id, colIndex - 1, rowIndex);\n                    }\n                }\n            }\n        }\n    }\n    /**\n     * Move the cells after a column or rows insertion\n     */\n    moveCellsOnAddition(sheet, addedElement, quantity, dimension) {\n        const updates = [];\n        for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {\n            const row = sheet.rows[rowIndex];\n            if (dimension !== \"rows\" || rowIndex >= addedElement) {\n                for (let i in row.cells) {\n                    const colIndex = Number(i);\n                    const cellId = row.cells[i];\n                    if (cellId) {\n                        if (dimension === \"rows\" || colIndex >= addedElement) {\n                            updates.push({\n                                sheetId: sheet.id,\n                                cellId: cellId,\n                                col: colIndex + (dimension === \"columns\" ? quantity : 0),\n                                row: rowIndex + (dimension === \"rows\" ? quantity : 0),\n                                type: \"UPDATE_CELL_POSITION\",\n                            });\n                        }\n                    }\n                }\n            }\n        }\n        for (let update of updates.reverse()) {\n            this.updateCellPosition(update);\n        }\n    }\n    /**\n     * Move all the cells that are from the row under `deleteToRow` up to `deleteFromRow`\n     *\n     * b.e.\n     * move vertically with delete from 3 and delete to 5 will first clear all the cells from lines 3 to 5,\n     * then take all the row starting at index 6 and add them back at index 3\n     *\n     */\n    moveCellOnRowsDeletion(sheet, deleteFromRow, deleteToRow) {\n        this.dispatch(\"CLEAR_CELLS\", {\n            sheetId: sheet.id,\n            target: [\n                {\n                    left: 0,\n                    top: deleteFromRow,\n                    right: this.getters.getNumberCols(sheet.id),\n                    bottom: deleteToRow,\n                },\n            ],\n        });\n        const numberRows = deleteToRow - deleteFromRow + 1;\n        for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {\n            const row = sheet.rows[rowIndex];\n            if (rowIndex > deleteToRow) {\n                for (let i in row.cells) {\n                    const colIndex = Number(i);\n                    const cellId = row.cells[i];\n                    if (cellId) {\n                        this.setNewPosition(cellId, sheet.id, colIndex, rowIndex - numberRows);\n                    }\n                }\n            }\n        }\n    }\n    updateRowsStructureOnDeletion(sheet, deleteFromRow, deleteToRow) {\n        const rows = [];\n        const cellsQueue = sheet.rows.map((row) => row.cells).reverse();\n        for (let i in sheet.rows) {\n            const row = Number(i);\n            if (row >= deleteFromRow && row <= deleteToRow) {\n                continue;\n            }\n            rows.push({\n                cells: cellsQueue.pop(),\n            });\n        }\n        this.history.update(\"sheets\", sheet.id, \"rows\", rows);\n    }\n    /**\n     * Add empty rows at the end of the rows\n     *\n     * @param sheet Sheet\n     * @param quantity Number of rows to add\n     */\n    addEmptyRows(sheet, quantity) {\n        const rows = sheet.rows.slice();\n        for (let i = 0; i < quantity; i++) {\n            rows.push({\n                cells: {},\n            });\n        }\n        this.history.update(\"sheets\", sheet.id, \"rows\", rows);\n    }\n    getImportedSheetSize(data) {\n        const positions = Object.keys(data.cells).map(toCartesian);\n        let rowNumber = data.rowNumber;\n        let colNumber = data.colNumber;\n        for (let { col, row } of positions) {\n            rowNumber = Math.max(rowNumber, row + 1);\n            colNumber = Math.max(colNumber, col + 1);\n        }\n        return { rowNumber, colNumber };\n    }\n    /**\n     * Check that any \"sheetId\" in the command matches an existing\n     * sheet.\n     */\n    checkSheetExists(cmd) {\n        if (cmd.type !== \"CREATE_SHEET\" && \"sheetId\" in cmd && this.sheets[cmd.sheetId] === undefined) {\n            return \"InvalidSheetId\" /* CommandResult.InvalidSheetId */;\n        }\n        else if (cmd.type === \"CREATE_SHEET\" && this.sheets[cmd.sheetId] !== undefined) {\n            return \"DuplicatedSheetId\" /* CommandResult.DuplicatedSheetId */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    /**\n     * Check if zones in the command are well formed and\n     * not outside the sheet.\n     */\n    checkZonesAreInSheet(cmd) {\n        if (!(\"sheetId\" in cmd))\n            return \"Success\" /* CommandResult.Success */;\n        return this.checkZonesExistInSheet(cmd.sheetId, this.getCommandZones(cmd));\n    }\n}\n\nclass TablePlugin extends CorePlugin {\n    static getters = [\"getCoreTable\", \"getCoreTables\", \"getCoreTableMatchingTopLeft\"];\n    tables = {};\n    adaptRanges(applyChange, sheetId) {\n        const sheetIds = sheetId ? [sheetId] : this.getters.getSheetIds();\n        for (const sheetId of sheetIds) {\n            for (const table of this.getCoreTables(sheetId)) {\n                this.applyRangeChangeOnTable(sheetId, table, applyChange);\n            }\n        }\n    }\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_TABLE\":\n                const zones = cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData).zone);\n                if (!areZonesContinuous(zones)) {\n                    return \"NonContinuousTargets\" /* CommandResult.NonContinuousTargets */;\n                }\n                return this.checkValidations(cmd, (cmd) => this.getTablesOverlappingZones(cmd.sheetId, zones).length\n                    ? \"TableOverlap\" /* CommandResult.TableOverlap */\n                    : \"Success\" /* CommandResult.Success */, (cmd) => this.checkTableConfigUpdateIsValid(cmd.config));\n            case \"UPDATE_TABLE\":\n                const updatedTable = this.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);\n                if (!updatedTable) {\n                    return \"TableNotFound\" /* CommandResult.TableNotFound */;\n                }\n                return this.checkValidations(cmd, this.checkUpdatedTableZoneIsValid, (cmd) => this.checkTableConfigUpdateIsValid(cmd.config));\n            case \"ADD_MERGE\":\n                for (const table of this.getCoreTables(cmd.sheetId)) {\n                    const tableZone = table.range.zone;\n                    for (const merge of cmd.target) {\n                        if (overlap(tableZone, merge)) {\n                            return \"MergeInTable\" /* CommandResult.MergeInTable */;\n                        }\n                    }\n                }\n                break;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_SHEET\":\n                this.history.update(\"tables\", cmd.sheetId, {});\n                break;\n            case \"DELETE_SHEET\": {\n                const tables = { ...this.tables };\n                delete tables[cmd.sheetId];\n                this.history.update(\"tables\", tables);\n                break;\n            }\n            case \"DUPLICATE_SHEET\": {\n                const newTables = {};\n                for (const table of this.getCoreTables(cmd.sheetId)) {\n                    newTables[table.id] =\n                        table.type === \"dynamic\"\n                            ? this.copyDynamicTableForSheet(cmd.sheetIdTo, table)\n                            : this.copyStaticTableForSheet(cmd.sheetIdTo, table);\n                }\n                this.history.update(\"tables\", cmd.sheetIdTo, newTables);\n                break;\n            }\n            case \"CREATE_TABLE\": {\n                const ranges = cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData));\n                const union = this.getters.getRangesUnion(ranges);\n                const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);\n                this.dispatch(\"REMOVE_MERGE\", { sheetId: cmd.sheetId, target: mergesInTarget });\n                const id = this.uuidGenerator.uuidv4();\n                const config = cmd.config || DEFAULT_TABLE_CONFIG;\n                const newTable = cmd.tableType === \"dynamic\"\n                    ? this.createDynamicTable(id, union, config)\n                    : this.createStaticTable(id, cmd.tableType, union, config);\n                this.history.update(\"tables\", cmd.sheetId, newTable.id, newTable);\n                break;\n            }\n            case \"REMOVE_TABLE\": {\n                const tables = {};\n                for (const table of this.getCoreTables(cmd.sheetId)) {\n                    if (cmd.target.every((zone) => !intersection(table.range.zone, zone))) {\n                        tables[table.id] = table;\n                    }\n                }\n                this.history.update(\"tables\", cmd.sheetId, tables);\n                break;\n            }\n            case \"UPDATE_TABLE\": {\n                this.updateTable(cmd);\n                break;\n            }\n            case \"UPDATE_CELL\": {\n                const sheetId = cmd.sheetId;\n                for (const table of this.getCoreTables(sheetId)) {\n                    if (table.type === \"dynamic\") {\n                        continue;\n                    }\n                    const direction = this.canUpdateCellCmdExtendTable(cmd, table);\n                    if (direction === \"down\") {\n                        this.extendTableDown(sheetId, table);\n                    }\n                    else if (direction === \"right\") {\n                        this.extendTableRight(sheetId, table);\n                    }\n                }\n                break;\n            }\n            case \"DELETE_CONTENT\": {\n                const tables = { ...this.tables[cmd.sheetId] };\n                for (const tableId in tables) {\n                    const table = tables[tableId];\n                    if (table && cmd.target.some((zone) => isZoneInside(table.range.zone, zone))) {\n                        this.dispatch(\"REMOVE_TABLE\", { sheetId: cmd.sheetId, target: [table.range.zone] });\n                    }\n                }\n                break;\n            }\n        }\n    }\n    getCoreTables(sheetId) {\n        return this.tables[sheetId] ? Object.values(this.tables[sheetId]).filter(isDefined) : [];\n    }\n    getCoreTable({ sheetId, col, row }) {\n        return this.getCoreTables(sheetId).find((table) => isInside(col, row, table.range.zone));\n    }\n    getTablesOverlappingZones(sheetId, zones) {\n        return this.getCoreTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));\n    }\n    /** Extend a table down one row */\n    extendTableDown(sheetId, table) {\n        const newRange = this.getters.extendRange(table.range, \"ROW\", 1);\n        this.history.update(\"tables\", sheetId, table.id, this.updateStaticTable(table, newRange));\n    }\n    /** Extend a table right one col */\n    extendTableRight(sheetId, table) {\n        const newRange = this.getters.extendRange(table.range, \"COL\", 1);\n        this.history.update(\"tables\", sheetId, table.id, this.updateStaticTable(table, newRange));\n    }\n    /**\n     * Check if an UpdateCell command should cause the given table to be extended by one row or col.\n     *\n     * The table should be extended if all of these conditions are true:\n     * 1) The updated cell is right below/right of the table\n     * 2) The command adds a content to the cell\n     * 3) No cell right below/right next to the table had any content before the command\n     * 4) Extending the table down/right would not overlap with another table\n     * 5) Extending the table down/right would not overlap with a merge\n     *\n     */\n    canUpdateCellCmdExtendTable({ content: newCellContent, sheetId, col, row }, table) {\n        if (!newCellContent) {\n            return \"none\";\n        }\n        const zone = table.range.zone;\n        let direction = \"none\";\n        if (zone.bottom + 1 === row && col >= zone.left && col <= zone.right) {\n            direction = \"down\";\n        }\n        else if (zone.right + 1 === col && row >= zone.top && row <= zone.bottom) {\n            direction = \"right\";\n        }\n        if (direction === \"none\") {\n            return \"none\";\n        }\n        const zoneToCheckIfEmpty = direction === \"down\"\n            ? { ...zone, bottom: zone.bottom + 1, top: zone.bottom + 1 }\n            : { ...zone, right: zone.right + 1, left: zone.right + 1 };\n        for (const position of positions(zoneToCheckIfEmpty)) {\n            const cellPosition = { sheetId, ...position };\n            // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content\n            const cellContent = this.getters.getCell(cellPosition)?.content;\n            if (cellContent ||\n                this.getters.isInMerge(cellPosition) ||\n                this.getTablesOverlappingZones(sheetId, [positionToZone(position)]).length) {\n                return \"none\";\n            }\n        }\n        return direction;\n    }\n    getCoreTableMatchingTopLeft(sheetId, zone) {\n        for (const table of this.getCoreTables(sheetId)) {\n            const tableZone = table.range.zone;\n            if (tableZone.left === zone.left && tableZone.top === zone.top) {\n                return table;\n            }\n        }\n        return undefined;\n    }\n    checkUpdatedTableZoneIsValid(cmd) {\n        if (!cmd.newTableRange) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;\n        const zoneIsInSheet = this.getters.checkZonesExistInSheet(cmd.sheetId, [newTableZone]);\n        if (zoneIsInSheet !== \"Success\" /* CommandResult.Success */) {\n            return zoneIsInSheet;\n        }\n        const updatedTable = this.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);\n        if (!updatedTable) {\n            return \"TableNotFound\" /* CommandResult.TableNotFound */;\n        }\n        const overlappingTables = this.getTablesOverlappingZones(cmd.sheetId, [newTableZone]).filter((table) => table.id !== updatedTable.id);\n        return overlappingTables.length ? \"TableOverlap\" /* CommandResult.TableOverlap */ : \"Success\" /* CommandResult.Success */;\n    }\n    checkTableConfigUpdateIsValid(config) {\n        if (!config) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        if (config.numberOfHeaders !== undefined && config.numberOfHeaders < 0) {\n            return \"InvalidTableConfig\" /* CommandResult.InvalidTableConfig */;\n        }\n        if (config.hasFilters && config.numberOfHeaders === 0) {\n            return \"InvalidTableConfig\" /* CommandResult.InvalidTableConfig */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    createStaticTable(id, type, tableRange, config, filters) {\n        const zone = tableRange.zone;\n        if (!filters) {\n            filters = [];\n            for (const i of range(zone.left, zone.right + 1)) {\n                const filterZone = { ...zone, left: i, right: i };\n                const uid = this.uuidGenerator.uuidv4();\n                filters.push(this.createFilterFromZone(uid, tableRange.sheetId, filterZone, config));\n            }\n        }\n        return {\n            id,\n            range: tableRange,\n            filters,\n            config,\n            type,\n        };\n    }\n    createDynamicTable(id, tableRange, config) {\n        const zone = zoneToTopLeft(tableRange.zone);\n        return {\n            id,\n            range: this.getters.getRangeFromZone(tableRange.sheetId, zone),\n            config,\n            type: \"dynamic\",\n        };\n    }\n    updateTable(cmd) {\n        const table = this.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);\n        if (!table) {\n            return;\n        }\n        const newTableRange = cmd.newTableRange\n            ? this.getters.getRangeFromRangeData(cmd.newTableRange)\n            : undefined;\n        if (newTableRange) {\n            const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, newTableRange.zone);\n            this.dispatch(\"REMOVE_MERGE\", { sheetId: cmd.sheetId, target: mergesInTarget });\n        }\n        const range = newTableRange || table.range;\n        const newConfig = this.updateTableConfig(cmd.config, table.config);\n        const newTableType = cmd.tableType ?? table.type;\n        if ((newTableType === \"dynamic\" && table.type !== \"dynamic\") ||\n            (newTableType !== \"dynamic\" && table.type === \"dynamic\")) {\n            const newTable = newTableType === \"dynamic\"\n                ? this.createDynamicTable(table.id, range, newConfig)\n                : this.createStaticTable(table.id, newTableType, range, newConfig);\n            this.history.update(\"tables\", cmd.sheetId, table.id, newTable);\n        }\n        else {\n            const updatedTable = table.type === \"dynamic\"\n                ? this.updateDynamicTable(table, range, newConfig)\n                : this.updateStaticTable(table, range, newConfig, newTableType);\n            this.history.update(\"tables\", cmd.sheetId, table.id, updatedTable);\n        }\n    }\n    updateStaticTable(table, newRange, configUpdate, newTableType = table.type) {\n        if (newTableType === \"dynamic\") {\n            throw new Error(\"Cannot use updateStaticTable to update a dynamic table\");\n        }\n        const tableRange = newRange ? newRange : table.range;\n        const tableZone = tableRange.zone;\n        const newConfig = this.updateTableConfig(configUpdate, table.config);\n        const config = newConfig ? newConfig : table.config;\n        const filters = [];\n        if (newRange || (newConfig && \"numberOfHeaders\" in newConfig)) {\n            for (const i of range(tableZone.left, tableZone.right + 1)) {\n                const oldFilter = tableZone.top === table.range.zone.top\n                    ? table.filters.find((f) => f.col === i)\n                    : undefined;\n                const filterZone = { ...tableZone, left: i, right: i };\n                const filterId = oldFilter?.id || this.uuidGenerator.uuidv4();\n                filters.push(this.createFilterFromZone(filterId, tableRange.sheetId, filterZone, config));\n            }\n        }\n        return {\n            ...table,\n            range: tableRange,\n            config,\n            filters: filters.length ? filters : table.filters,\n            type: newTableType,\n        };\n    }\n    updateDynamicTable(table, newRange, newConfig) {\n        const range = newRange\n            ? this.getters.getRangeFromZone(newRange.sheetId, zoneToTopLeft(newRange.zone))\n            : table.range;\n        const config = newConfig ? newConfig : table.config;\n        return { ...table, range, config };\n    }\n    /**\n     * Update the old config of a table with the new partial config from an UpdateTable command.\n     *\n     * Make sure the new config make sense (e.g. if the table has no header, it should not have\n     * filters and number of headers should be 0)\n     */\n    updateTableConfig(update, oldConfig) {\n        if (!update) {\n            return oldConfig;\n        }\n        const saneConfig = { ...oldConfig, ...update };\n        if (update.numberOfHeaders === 0) {\n            saneConfig.hasFilters = false;\n        }\n        else if (update.hasFilters === true) {\n            saneConfig.numberOfHeaders ||= 1;\n        }\n        return saneConfig;\n    }\n    createFilterFromZone(id, sheetId, zone, config) {\n        const range = this.getters.getRangeFromZone(sheetId, zone);\n        return createFilter(id, range, config, this.getters.getRangeFromZone);\n    }\n    copyStaticTableForSheet(sheetId, table) {\n        const newRange = this.getters.getRangeFromZone(sheetId, table.range.zone);\n        const newFilters = table.filters.map((filter) => {\n            const newFilterRange = this.getters.getRangeFromZone(sheetId, filter.rangeWithHeaders.zone);\n            return createFilter(filter.id, newFilterRange, table.config, this.getters.getRangeFromZone);\n        });\n        return {\n            id: table.id,\n            range: newRange,\n            filters: newFilters,\n            config: deepCopy(table.config),\n            type: table.type,\n        };\n    }\n    copyDynamicTableForSheet(sheetId, table) {\n        const newRange = this.getters.getRangeFromZone(sheetId, table.range.zone);\n        return {\n            id: table.id,\n            range: newRange,\n            config: deepCopy(table.config),\n            type: \"dynamic\",\n        };\n    }\n    applyRangeChangeOnTable(sheetId, table, applyChange) {\n        const tableRangeChange = applyChange(table.range);\n        let newTableRange;\n        switch (tableRangeChange.changeType) {\n            case \"REMOVE\":\n                this.history.update(\"tables\", sheetId, table.id, undefined);\n                return;\n            case \"NONE\":\n                return;\n            default:\n                newTableRange = tableRangeChange.range;\n        }\n        if (table.type === \"dynamic\") {\n            const newTable = this.updateDynamicTable(table, newTableRange);\n            this.history.update(\"tables\", sheetId, table.id, newTable);\n            return;\n        }\n        const filters = [];\n        for (const filter of table.filters) {\n            const filterRangeChange = applyChange(filter.rangeWithHeaders);\n            switch (filterRangeChange.changeType) {\n                case \"REMOVE\":\n                    continue;\n                case \"NONE\":\n                    filters.push(filter);\n                    break;\n                default:\n                    const newFilterRange = filterRangeChange.range;\n                    const newFilter = createFilter(filter.id, newFilterRange, table.config, this.getters.getRangeFromZone);\n                    filters.push(newFilter);\n            }\n        }\n        const tableZone = newTableRange.zone;\n        if (filters.length < zoneToDimension(tableZone).numberOfCols) {\n            for (let col = tableZone.left; col <= tableZone.right; col++) {\n                if (!filters.find((filter) => filter.col === col)) {\n                    const uid = this.uuidGenerator.uuidv4();\n                    const filterZone = { ...tableZone, left: col, right: col };\n                    filters.push(this.createFilterFromZone(uid, sheetId, filterZone, table.config));\n                }\n            }\n            filters.sort((f1, f2) => f1.col - f2.col);\n        }\n        const newTable = this.createStaticTable(table.id, table.type, newTableRange, table.config, filters);\n        this.history.update(\"tables\", sheetId, table.id, newTable);\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n    import(data) {\n        for (const sheet of data.sheets) {\n            for (const tableData of sheet.tables || []) {\n                const uuid = this.uuidGenerator.uuidv4();\n                const tableConfig = tableData.config || DEFAULT_TABLE_CONFIG;\n                const range = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);\n                const tableType = tableData.type || \"static\";\n                const table = tableType === \"dynamic\"\n                    ? this.createDynamicTable(uuid, range, tableConfig)\n                    : this.createStaticTable(uuid, tableType, range, tableConfig);\n                this.history.update(\"tables\", sheet.id, table.id, table);\n            }\n        }\n    }\n    export(data) {\n        for (const sheet of data.sheets) {\n            for (const table of this.getCoreTables(sheet.id)) {\n                const range = zoneToXc(table.range.zone);\n                const tableData = { range, type: table.type };\n                if (!deepEquals(table.config, DEFAULT_TABLE_CONFIG)) {\n                    tableData.config = table.config;\n                }\n                sheet.tables.push(tableData);\n            }\n        }\n    }\n    exportForExcel(data) {\n        for (const sheet of data.sheets) {\n            for (const table of this.getCoreTables(sheet.id)) {\n                const range = zoneToXc(table.range.zone);\n                sheet.tables.push({ range, filters: [], config: table.config });\n            }\n        }\n    }\n}\n\nclass HeaderGroupingPlugin extends CorePlugin {\n    static getters = [\n        \"getHeaderGroups\",\n        \"getGroupsLayers\",\n        \"getVisibleGroupLayers\",\n        \"getHeaderGroup\",\n        \"getHeaderGroupsInZone\",\n        \"isGroupFolded\",\n        \"isRowFolded\",\n        \"isColFolded\",\n    ];\n    groups = {};\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"GROUP_HEADERS\": {\n                const { start, end } = cmd;\n                if (!this.getters.doesHeadersExist(cmd.sheetId, cmd.dimension, [start, end])) {\n                    return \"InvalidHeaderGroupStartEnd\" /* CommandResult.InvalidHeaderGroupStartEnd */;\n                }\n                if (start > end) {\n                    return \"InvalidHeaderGroupStartEnd\" /* CommandResult.InvalidHeaderGroupStartEnd */;\n                }\n                if (this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, start, end)) {\n                    return \"HeaderGroupAlreadyExists\" /* CommandResult.HeaderGroupAlreadyExists */;\n                }\n                break;\n            }\n            case \"UNGROUP_HEADERS\": {\n                const { start, end } = cmd;\n                if (!this.getters.doesHeadersExist(cmd.sheetId, cmd.dimension, [start, end])) {\n                    return \"InvalidHeaderGroupStartEnd\" /* CommandResult.InvalidHeaderGroupStartEnd */;\n                }\n                if (start > end) {\n                    return \"InvalidHeaderGroupStartEnd\" /* CommandResult.InvalidHeaderGroupStartEnd */;\n                }\n                break;\n            }\n            case \"UNFOLD_HEADER_GROUP\":\n            case \"FOLD_HEADER_GROUP\":\n                const group = this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);\n                if (!group) {\n                    return \"UnknownHeaderGroup\" /* CommandResult.UnknownHeaderGroup */;\n                }\n                const numberOfHeaders = this.getters.getNumberHeaders(cmd.sheetId, cmd.dimension);\n                const willHideAllHeaders = range(0, numberOfHeaders).every((i) => (i >= group.start && i <= group.end) ||\n                    this.getters.isHeaderHiddenByUser(cmd.sheetId, cmd.dimension, i));\n                if (willHideAllHeaders) {\n                    return \"NotEnoughElements\" /* CommandResult.NotEnoughElements */;\n                }\n                break;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_SHEET\":\n                this.history.update(\"groups\", cmd.sheetId, { ROW: [], COL: [] });\n                break;\n            case \"GROUP_HEADERS\":\n                this.groupHeaders(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);\n                break;\n            case \"UNGROUP_HEADERS\": {\n                this.unGroupHeaders(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);\n                break;\n            }\n            case \"DUPLICATE_SHEET\": {\n                const groups = deepCopy(this.groups[cmd.sheetId]);\n                this.history.update(\"groups\", cmd.sheetIdTo, groups);\n                break;\n            }\n            case \"DELETE_SHEET\": {\n                const groups = { ...this.groups };\n                delete groups[cmd.sheetId];\n                this.history.update(\"groups\", groups);\n                break;\n            }\n            case \"ADD_COLUMNS_ROWS\":\n                const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);\n                this.moveGroupsOnHeaderInsertion(cmd.sheetId, cmd.dimension, addIndex, cmd.quantity);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\":\n                this.moveGroupsOnHeaderDeletion(cmd.sheetId, cmd.dimension, cmd.elements);\n                break;\n            case \"UNFOLD_HEADER_GROUP\": {\n                const group = this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);\n                if (group) {\n                    this.unfoldHeaderGroup(cmd.sheetId, cmd.dimension, group);\n                }\n                break;\n            }\n            case \"FOLD_HEADER_GROUP\": {\n                const group = this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);\n                if (group) {\n                    this.foldHeaderGroup(cmd.sheetId, cmd.dimension, group);\n                }\n                break;\n            }\n            case \"UNFOLD_ALL_HEADER_GROUPS\": {\n                const groups = this.getters.getHeaderGroups(cmd.sheetId, cmd.dimension);\n                for (const group of groups) {\n                    this.unfoldHeaderGroup(cmd.sheetId, cmd.dimension, group);\n                }\n                break;\n            }\n            case \"FOLD_ALL_HEADER_GROUPS\": {\n                const groups = this.getters.getHeaderGroups(cmd.sheetId, cmd.dimension);\n                for (const group of groups) {\n                    this.foldHeaderGroup(cmd.sheetId, cmd.dimension, group);\n                }\n                break;\n            }\n            case \"FOLD_HEADER_GROUPS_IN_ZONE\":\n            case \"UNFOLD_HEADER_GROUPS_IN_ZONE\": {\n                const action = cmd.type === \"UNFOLD_HEADER_GROUPS_IN_ZONE\" ? \"unfold\" : \"fold\";\n                const layers = this.getGroupsLayers(cmd.sheetId, cmd.dimension);\n                if (action === \"fold\") {\n                    layers.reverse();\n                }\n                const groups = layers.flat();\n                const start = cmd.dimension === \"ROW\" ? cmd.zone.top : cmd.zone.left;\n                const end = cmd.dimension === \"ROW\" ? cmd.zone.bottom : cmd.zone.right;\n                const groupsToToggle = new Set();\n                for (let header = start; header <= end; header++) {\n                    const matchedGroups = groups.filter((g) => g.start - 1 <= header && header <= g.end); // -1 to include the group header\n                    for (const group of matchedGroups) {\n                        if ((action === \"fold\" && group.isFolded) || (action === \"unfold\" && !group.isFolded)) {\n                            continue;\n                        }\n                        groupsToToggle.add(group);\n                        break;\n                    }\n                }\n                for (const group of groupsToToggle) {\n                    if (action === \"unfold\") {\n                        this.unfoldHeaderGroup(cmd.sheetId, cmd.dimension, group);\n                    }\n                    else {\n                        this.foldHeaderGroup(cmd.sheetId, cmd.dimension, group);\n                    }\n                }\n                break;\n            }\n        }\n    }\n    getHeaderGroups(sheetId, dim) {\n        return this.groups[sheetId][dim];\n    }\n    getHeaderGroup(sheetId, dim, start, end) {\n        return this.getHeaderGroups(sheetId, dim).find((group) => group.start === start && group.end === end);\n    }\n    getHeaderGroupsInZone(sheetId, dim, zone) {\n        return this.getHeaderGroups(sheetId, dim).filter((group) => {\n            const start = dim === \"ROW\" ? zone.top : zone.left;\n            const end = dim === \"ROW\" ? zone.bottom : zone.right;\n            return this.doGroupOverlap(group, start, end);\n        });\n    }\n    /**\n     * Get all the groups of a sheet in a dimension, and return an array of layers of those groups.\n     *\n     * The layering rules are:\n     * 1) A group containing another group should be on a layer above the group it contains\n     * 2) The widest/highest groups should be on the left/top layer compared to the groups it contains\n     * 3) The group should be on the left/top-most layer possible, barring intersections with other groups (see rules 1 and 2)\n     */\n    getGroupsLayers(sheetId, dimension) {\n        const groups = this.getHeaderGroups(sheetId, dimension);\n        return this.bricksFallingAlgorithm(groups, 0, 0);\n    }\n    /**\n     * Get all the groups of a sheet in a dimension, and return an array of layers of those groups,\n     * excluding the groups that are totally hidden.\n     */\n    getVisibleGroupLayers(sheetId, dimension) {\n        const layers = this.getGroupsLayers(sheetId, dimension);\n        for (const layer of layers) {\n            for (let k = layer.length - 1; k >= 0; k--) {\n                const group = layer[k];\n                if (group.start === 0) {\n                    continue;\n                }\n                const headersInGroup = range(group.start - 1, group.end + 1);\n                if (headersInGroup.every((i) => this.getters.isHeaderHiddenByUser(sheetId, dimension, i))) {\n                    layer.splice(k, 1);\n                }\n            }\n        }\n        return layers.filter((layer) => layer.length > 0);\n    }\n    isGroupFolded(sheetId, dimension, start, end) {\n        return this.getHeaderGroup(sheetId, dimension, start, end)?.isFolded || false;\n    }\n    isRowFolded(sheetId, row) {\n        const groups = this.getters.getHeaderGroups(sheetId, \"ROW\");\n        return groups.some((group) => group.start <= row && row <= group.end && group.isFolded);\n    }\n    isColFolded(sheetId, col) {\n        const groups = this.getters.getHeaderGroups(sheetId, \"COL\");\n        // might become a performance issue if there are a lot of groups (this is called by isColHidden).\n        return groups.some((group) => group.start <= col && col <= group.end && group.isFolded);\n    }\n    getGroupId(group) {\n        return `${group.start}-${group.end}}`;\n    }\n    /**\n     * To get layers of groups, and to add/remove headers from groups, we can see each header of a group as a brick. Each\n     * brick falls down in the pile corresponding to its header, until it hits another brick, or the ground.\n     *\n     * With this abstraction, we can very simply group/ungroup headers from groups, and get the layers of groups.\n     * - grouping headers is done by adding a brick to each header pile\n     * - un-grouping headers is done by removing a brick from each header pile\n     * - getting the layers of groups is done by simply letting the brick fall and checking the result\n     *\n     * Example:\n     * We have 2 groups ([A=>E] and [C=>D]), and we want to group headers [C=>F]\n     *\n     * Headers :                 A B C D E F G          A B C D E F G            A B C D E F G\n     * Headers to group: [C=>D]:     _ _         [C=>F]:    _ _ _ _\n     *                               | |           ==>      | | | |       ==>                    ==> Result: 3 groups\n     *                               | |                    \u02c5 \u02c5 | |                  _ _                - [C=>D]\n     * Groups:                       \u02c5 \u02c5                    _ _ \u02c5 |                  _ _ _              - [C=>E]\n     * Groups:                   _ _ _ _ _              _ _ _ _ _ \u02c5              _ _ _ _ _ _            - [A=>F]\n  \n     * @param groups\n     * @param start start of the range where to add/remove headers\n     * @param end end of the range where to add/remove headers\n     * @param delta -1: remove headers, 1: add headers, 0: get layers (don't add/remove anything)\n     */\n    bricksFallingAlgorithm(groups, start, end, delta = 0) {\n        const isGroupFolded = {};\n        for (const group of groups) {\n            isGroupFolded[this.getGroupId(group)] = group.isFolded;\n        }\n        /** Number of bricks in each header pile */\n        const brickPileSize = {};\n        for (const group of groups) {\n            for (let i = group.start; i <= group.end; i++) {\n                brickPileSize[i] = brickPileSize[i] ? brickPileSize[i] + 1 : 1;\n            }\n        }\n        for (let i = start; i <= end; i++) {\n            brickPileSize[i] = brickPileSize[i] ? brickPileSize[i] + delta : delta;\n        }\n        const numberOfLayers = Math.max(...Object.values(brickPileSize), 0);\n        const groupLayers = Array.from({ length: numberOfLayers }, () => []);\n        const maxHeader = Math.max(end, ...groups.map((group) => group.end));\n        const minHeader = Math.min(start, ...groups.map((group) => group.start));\n        for (let header = minHeader; header <= maxHeader; header++) {\n            const pileSize = brickPileSize[header] || 0;\n            for (let layer = 0; layer < pileSize; layer++) {\n                const currentGroup = groupLayers[layer].at(-1);\n                if (currentGroup && isConsecutive([currentGroup.end, header])) {\n                    currentGroup.end++;\n                }\n                else {\n                    const newGroup = { start: header, end: header };\n                    groupLayers[layer].push(newGroup);\n                }\n            }\n        }\n        for (const layer of groupLayers) {\n            for (const group of layer) {\n                group.isFolded = isGroupFolded[this.getGroupId(group)];\n            }\n        }\n        return groupLayers;\n    }\n    groupHeaders(sheetId, dimension, start, end) {\n        const groups = this.getHeaderGroups(sheetId, dimension);\n        const newGroups = this.bricksFallingAlgorithm(groups, start, end, +1).flat();\n        this.history.update(\"groups\", sheetId, dimension, this.removeDuplicateGroups(newGroups));\n    }\n    /**\n     * Ungroup the given headers. The headers will be taken out of the group they are in. This might split a group into two\n     * if the headers were in the middle of a group. If multiple groups contains a header, it will only be taken out of the\n     * lowest group in the layering of the groups.\n     */\n    unGroupHeaders(sheetId, dimension, start, end) {\n        const groups = this.getHeaderGroups(sheetId, dimension);\n        const newGroups = this.bricksFallingAlgorithm(groups, start, end, -1).flat();\n        this.history.update(\"groups\", sheetId, dimension, this.removeDuplicateGroups(newGroups));\n    }\n    moveGroupsOnHeaderInsertion(sheetId, dim, index, count) {\n        const groups = this.groups[sheetId][dim];\n        for (let i = 0; i < groups.length; i++) {\n            const group = groups[i];\n            const [start, end] = moveHeaderIndexesOnHeaderAddition(index, count, [\n                group.start,\n                group.end,\n            ]);\n            if (start !== group.start || end !== group.end) {\n                this.history.update(\"groups\", sheetId, dim, i, { ...group, start, end });\n            }\n        }\n    }\n    moveGroupsOnHeaderDeletion(sheetId, dimension, deletedElements) {\n        const groups = this.getHeaderGroups(sheetId, dimension);\n        const newGroups = [];\n        for (const group of groups) {\n            const headersInGroup = range(group.start, group.end + 1);\n            const headersAfterDeletion = moveHeaderIndexesOnHeaderDeletion(deletedElements, headersInGroup);\n            if (headersAfterDeletion.length === 0) {\n                continue;\n            }\n            newGroups.push({\n                ...group,\n                start: Math.min(...headersAfterDeletion),\n                end: Math.max(...headersAfterDeletion),\n            });\n        }\n        this.history.update(\"groups\", sheetId, dimension, this.bricksFallingAlgorithm(newGroups, 0, 0).flat());\n    }\n    doGroupOverlap(group, start, end) {\n        return group.start <= end && group.end >= start;\n    }\n    removeDuplicateGroups(groups) {\n        const newGroups = {};\n        for (const group of groups) {\n            newGroups[this.getGroupId(group)] = group;\n        }\n        return Object.values(newGroups);\n    }\n    findGroupWithStartEnd(sheetId, dimension, start, end) {\n        return this.getHeaderGroups(sheetId, dimension).find((group) => group.start === start && group.end === end);\n    }\n    /**\n     * Fold the given group, and all the groups starting at the same index that are contained inside the given group.\n     */\n    foldHeaderGroup(sheetId, dim, groupToFold) {\n        const index = this.getGroupIndex(sheetId, dim, groupToFold.start, groupToFold.end);\n        if (index === undefined) {\n            return;\n        }\n        this.history.update(\"groups\", sheetId, dim, index, \"isFolded\", true);\n        const groups = this.getters.getHeaderGroups(sheetId, dim);\n        for (let i = 0; i < groups.length; i++) {\n            const group = groups[i];\n            if (group.start === groupToFold.start && group.end <= groupToFold.end) {\n                this.history.update(\"groups\", sheetId, dim, i, \"isFolded\", true);\n            }\n        }\n    }\n    /**\n     * Unfold the given group, and all the groups starting at the same index that contain the given group.\n     */\n    unfoldHeaderGroup(sheetId, dim, groupToUnfold) {\n        const index = this.getGroupIndex(sheetId, dim, groupToUnfold.start, groupToUnfold.end);\n        if (index === undefined) {\n            return;\n        }\n        this.history.update(\"groups\", sheetId, dim, index, \"isFolded\", false);\n        const groups = this.getters.getHeaderGroups(sheetId, dim);\n        for (let i = 0; i < groups.length; i++) {\n            const group = groups[i];\n            if (group.start === groupToUnfold.start && group.end >= groupToUnfold.end) {\n                this.history.update(\"groups\", sheetId, dim, i, \"isFolded\", false);\n            }\n        }\n    }\n    getGroupIndex(sheetId, dimension, start, end) {\n        const index = this.groups[sheetId][dimension].findIndex((group) => group.start === start && group.end === end);\n        return index === -1 ? undefined : index;\n    }\n    import(data) {\n        for (const sheet of data.sheets) {\n            this.groups[sheet.id] = { ROW: [], COL: [] };\n            if (!sheet.headerGroups) {\n                continue;\n            }\n            for (const dim of [\"ROW\", \"COL\"]) {\n                for (const groupData of sheet.headerGroups[dim] || []) {\n                    this.groups[sheet.id][dim].push({ ...groupData });\n                }\n            }\n        }\n    }\n    export(data) {\n        for (const sheet of data.sheets) {\n            sheet.headerGroups = this.groups[sheet.id];\n        }\n    }\n    exportForExcel(data) {\n        /**\n         * Example of header groups in the XLSX file:\n         *\n         * 0. |        <row index=\"1\" outlineLevel=\"1\">\n         * 1. | |      <row index=\"2\" outlineLevel=\"2\">\n         * 2. | |      <row index=\"3\" outlineLevel=\"2\">\n         * 3. | |_     <row index=\"4\" outlineLevel=\"2\">\n         * 4. |_       <row index=\"5\" outlineLevel=\"1\" collapsed=\"0\">\n         * 5.          <row index=\"6\" collapsed=\"0\">\n         *\n         * The collapsed flag can be on the header before or after the group (or can be missing). Default is after.\n         */\n        for (const sheet of data.sheets) {\n            for (const dim of [\"ROW\", \"COL\"]) {\n                const layers = this.getGroupsLayers(sheet.id, dim);\n                for (let layerIndex = 0; layerIndex < layers.length; layerIndex++) {\n                    const layer = layers[layerIndex];\n                    for (const group of layer) {\n                        for (let headerIndex = group.start; headerIndex <= group.end; headerIndex++) {\n                            const header = getSheetDataHeader(sheet, dim, headerIndex);\n                            header.outlineLevel = layerIndex + 1;\n                            if (group.isFolded) {\n                                header.isHidden = true;\n                            }\n                        }\n                        if (group.isFolded) {\n                            const header = getSheetDataHeader(sheet, dim, group.end + 1);\n                            header.collapsed = true;\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nclass PivotCorePlugin extends CorePlugin {\n    static getters = [\n        \"getPivotCoreDefinition\",\n        \"getPivotDisplayName\",\n        \"getPivotId\",\n        \"getPivotFormulaId\",\n        \"getPivotIds\",\n        \"getMeasureCompiledFormula\",\n        \"getPivotName\",\n        \"isExistingPivot\",\n    ];\n    nextFormulaId = 1;\n    pivots = {};\n    formulaIds = {};\n    compiledMeasureFormulas = {};\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"ADD_PIVOT\": {\n                return this.checkDuplicatedMeasureIds(cmd.pivot);\n            }\n            case \"UPDATE_PIVOT\": {\n                if (deepEquals(cmd.pivot, this.pivots[cmd.pivotId]?.definition)) {\n                    return \"NoChanges\" /* CommandResult.NoChanges */;\n                }\n                if (cmd.pivot.name === \"\") {\n                    return \"EmptyName\" /* CommandResult.EmptyName */;\n                }\n                return this.checkDuplicatedMeasureIds(cmd.pivot);\n            }\n            case \"RENAME_PIVOT\":\n                if (!(cmd.pivotId in this.pivots)) {\n                    return \"PivotIdNotFound\" /* CommandResult.PivotIdNotFound */;\n                }\n                if (cmd.name === \"\") {\n                    return \"EmptyName\" /* CommandResult.EmptyName */;\n                }\n                break;\n            case \"INSERT_PIVOT\": {\n                if (!(cmd.pivotId in this.pivots)) {\n                    return \"PivotIdNotFound\" /* CommandResult.PivotIdNotFound */;\n                }\n                break;\n            }\n            case \"DUPLICATE_PIVOT\":\n                if (!(cmd.pivotId in this.pivots)) {\n                    return \"PivotIdNotFound\" /* CommandResult.PivotIdNotFound */;\n                }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_PIVOT\": {\n                const { pivotId, pivot } = cmd;\n                this.addPivot(pivotId, pivot);\n                break;\n            }\n            case \"INSERT_PIVOT\": {\n                const { sheetId, col, row, pivotId, table } = cmd;\n                const position = { sheetId, col, row };\n                const { cols, rows, measures, fieldsType } = table;\n                const spTable = new SpreadsheetPivotTable(cols, rows, measures, fieldsType || {});\n                const formulaId = this.getPivotFormulaId(pivotId);\n                this.insertPivot(position, formulaId, spTable);\n                break;\n            }\n            case \"RENAME_PIVOT\": {\n                this.history.update(\"pivots\", cmd.pivotId, \"definition\", \"name\", cmd.name);\n                break;\n            }\n            case \"REMOVE_PIVOT\": {\n                const pivots = { ...this.pivots };\n                delete pivots[cmd.pivotId];\n                const formulaId = this.getPivotFormulaId(cmd.pivotId);\n                this.history.update(\"formulaIds\", formulaId, undefined);\n                this.history.update(\"pivots\", pivots);\n                break;\n            }\n            case \"DUPLICATE_PIVOT\": {\n                const { pivotId, newPivotId } = cmd;\n                const pivot = deepCopy(this.getPivotCore(pivotId).definition);\n                pivot.name = cmd.duplicatedPivotName ?? pivot.name + \" (copy)\";\n                this.addPivot(newPivotId, pivot);\n                break;\n            }\n            case \"UPDATE_PIVOT\": {\n                this.history.update(\"pivots\", cmd.pivotId, \"definition\", cmd.pivot);\n                this.compileCalculatedMeasures(cmd.pivot.measures);\n                break;\n            }\n        }\n    }\n    adaptRanges(applyChange) {\n        for (const sheetId in this.compiledMeasureFormulas) {\n            for (const formulaString in this.compiledMeasureFormulas[sheetId]) {\n                const compiledFormula = this.compiledMeasureFormulas[sheetId][formulaString];\n                const newDependencies = [];\n                for (const range of compiledFormula.dependencies) {\n                    const change = applyChange(range);\n                    if (change.changeType === \"NONE\") {\n                        newDependencies.push(range);\n                    }\n                    else {\n                        newDependencies.push(change.range);\n                    }\n                }\n                const newFormulaString = this.getters.getFormulaString(sheetId, compiledFormula.tokens, newDependencies);\n                if (newFormulaString !== formulaString) {\n                    this.replaceMeasureFormula(sheetId, formulaString, newFormulaString);\n                }\n            }\n        }\n    }\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n    getPivotDisplayName(pivotId) {\n        const formulaId = this.getPivotFormulaId(pivotId);\n        return `(#${formulaId}) ${this.getPivotName(pivotId)}`;\n    }\n    getPivotName(pivotId) {\n        return this.getPivotCore(pivotId).definition.name;\n    }\n    /**\n     * Returns the pivot core definition of the pivot with the given id.\n     * Be careful, this is the core definition, this should be used only in a\n     * context where the pivot is not loaded yet.\n     */\n    getPivotCoreDefinition(pivotId) {\n        return this.getPivotCore(pivotId).definition;\n    }\n    /**\n     * Get the pivot ID (UID) from the formula ID (the one used in the formula)\n     */\n    getPivotId(formulaId) {\n        return this.formulaIds[formulaId];\n    }\n    getPivotFormulaId(pivotId) {\n        return this.getPivotCore(pivotId).formulaId;\n    }\n    getPivotIds() {\n        return Object.keys(this.pivots);\n    }\n    isExistingPivot(pivotId) {\n        return pivotId in this.pivots;\n    }\n    getMeasureCompiledFormula(measure) {\n        if (!measure.computedBy) {\n            throw new Error(`Measure ${measure.fieldName} is not computed by formula`);\n        }\n        const sheetId = measure.computedBy.sheetId;\n        return this.compiledMeasureFormulas[sheetId][measure.computedBy.formula];\n    }\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n    addPivot(pivotId, pivot, formulaId = this.nextFormulaId.toString()) {\n        this.history.update(\"pivots\", pivotId, { definition: pivot, formulaId });\n        this.compileCalculatedMeasures(pivot.measures);\n        this.history.update(\"formulaIds\", formulaId, pivotId);\n        this.history.update(\"nextFormulaId\", this.nextFormulaId + 1);\n    }\n    compileCalculatedMeasures(measures) {\n        for (const measure of measures) {\n            if (measure.computedBy) {\n                const sheetId = measure.computedBy.sheetId;\n                const compiledFormula = this.compileMeasureFormula(measure.computedBy.sheetId, measure.computedBy.formula);\n                this.history.update(\"compiledMeasureFormulas\", sheetId, measure.computedBy.formula, compiledFormula);\n            }\n        }\n    }\n    insertPivot(position, formulaId, table) {\n        this.resizeSheet(position.sheetId, position, table);\n        const pivotCells = table.getPivotCells();\n        for (let col = 0; col < pivotCells.length; col++) {\n            for (let row = 0; row < pivotCells[col].length; row++) {\n                const pivotCell = pivotCells[col][row];\n                this.dispatch(\"UPDATE_CELL\", {\n                    sheetId: position.sheetId,\n                    col: position.col + col,\n                    row: position.row + row,\n                    content: createPivotFormula(formulaId, pivotCell),\n                });\n            }\n        }\n    }\n    resizeSheet(sheetId, { col, row }, table) {\n        const colLimit = table.getNumberOfDataColumns() + 1; // +1 for the Top-Left\n        const numberCols = this.getters.getNumberCols(sheetId);\n        const deltaCol = numberCols - col;\n        if (deltaCol < colLimit) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"COL\",\n                base: numberCols - 1,\n                sheetId: sheetId,\n                quantity: colLimit - deltaCol,\n                position: \"after\",\n            });\n        }\n        const rowLimit = table.columns.length + table.rows.length;\n        const numberRows = this.getters.getNumberRows(sheetId);\n        const deltaRow = numberRows - row;\n        if (deltaRow < rowLimit) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"ROW\",\n                base: numberRows - 1,\n                sheetId: sheetId,\n                quantity: rowLimit - deltaRow,\n                position: \"after\",\n            });\n        }\n    }\n    getPivotCore(pivotId) {\n        const pivot = this.pivots[pivotId];\n        if (!pivot) {\n            throw new Error(`Pivot with id ${pivotId} not found`);\n        }\n        return pivot;\n    }\n    compileMeasureFormula(sheetId, formulaString) {\n        const compiledFormula = compile(formulaString);\n        const rangeDependencies = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));\n        return {\n            ...compiledFormula,\n            dependencies: rangeDependencies,\n        };\n    }\n    replaceMeasureFormula(sheetId, formulaString, newFormulaString) {\n        this.history.update(\"compiledMeasureFormulas\", sheetId, formulaString, undefined);\n        this.history.update(\"compiledMeasureFormulas\", sheetId, newFormulaString, this.compileMeasureFormula(sheetId, newFormulaString));\n        for (const pivotId in this.pivots) {\n            const pivot = this.pivots[pivotId];\n            if (!pivot) {\n                continue;\n            }\n            for (const measure of pivot.definition.measures) {\n                if (measure.computedBy?.formula === formulaString) {\n                    const measureIndex = pivot.definition.measures.indexOf(measure);\n                    this.history.update(\"pivots\", pivotId, \"definition\", \"measures\", measureIndex, \"computedBy\", { formula: newFormulaString, sheetId });\n                }\n            }\n        }\n    }\n    checkDuplicatedMeasureIds(definition) {\n        const uniqueIds = new Set(definition.measures.map((m) => m.id));\n        if (definition.measures.length !== uniqueIds.size) {\n            return \"InvalidDefinition\" /* CommandResult.InvalidDefinition */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    // ---------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------\n    /**\n     * Import the pivots\n     */\n    import(data) {\n        if (data.pivots) {\n            for (const [id, pivot] of Object.entries(data.pivots)) {\n                this.addPivot(id, pivot, pivot.formulaId);\n            }\n        }\n        this.history.update(\"nextFormulaId\", data.pivotNextId || getMaxObjectId(this.pivots) + 1);\n    }\n    /**\n     * Export the pivots\n     */\n    export(data) {\n        data.pivots = {};\n        for (const pivotId in this.pivots) {\n            data.pivots[pivotId] = {\n                ...this.getPivotCoreDefinition(pivotId),\n                formulaId: this.getPivotFormulaId(pivotId),\n            };\n        }\n        data.pivotNextId = this.nextFormulaId;\n    }\n}\n\nclass SettingsPlugin extends CorePlugin {\n    static getters = [\"getLocale\"];\n    locale = DEFAULT_LOCALE;\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"UPDATE_LOCALE\":\n                return isValidLocale(cmd.locale) ? \"Success\" /* CommandResult.Success */ : \"InvalidLocale\" /* CommandResult.InvalidLocale */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"UPDATE_LOCALE\":\n                const oldLocale = this.locale;\n                const newLocale = cmd.locale;\n                this.history.update(\"locale\", newLocale);\n                this.changeCellsDateFormatWithLocale(oldLocale, newLocale);\n                break;\n        }\n    }\n    getLocale() {\n        return this.locale;\n    }\n    changeCellsDateFormatWithLocale(oldLocale, newLocale) {\n        for (const sheetId of this.getters.getSheetIds()) {\n            for (const [cellId, cell] of Object.entries(this.getters.getCells(sheetId))) {\n                let formatToApply;\n                if (cell.format === oldLocale.dateFormat) {\n                    formatToApply = newLocale.dateFormat;\n                }\n                if (cell.format === oldLocale.timeFormat) {\n                    formatToApply = newLocale.timeFormat;\n                }\n                if (cell.format === getDateTimeFormat(oldLocale)) {\n                    formatToApply = getDateTimeFormat(newLocale);\n                }\n                if (formatToApply) {\n                    const { col, row, sheetId } = this.getters.getCellPosition(cellId);\n                    this.dispatch(\"UPDATE_CELL\", {\n                        col,\n                        row,\n                        sheetId,\n                        format: formatToApply,\n                    });\n                }\n            }\n        }\n    }\n    import(data) {\n        this.locale = data.settings?.locale ?? DEFAULT_LOCALE;\n    }\n    export(data) {\n        data.settings = {\n            locale: this.locale,\n        };\n    }\n}\n\nfunction adaptPivotRange(range, applyChange) {\n    if (!range) {\n        return undefined;\n    }\n    const change = applyChange(range);\n    switch (change.changeType) {\n        case \"NONE\":\n            return range;\n        case \"REMOVE\":\n            return undefined;\n        default:\n            return change.range;\n    }\n}\nclass SpreadsheetPivotCorePlugin extends CorePlugin {\n    adaptRanges(applyChange) {\n        for (const pivotId of this.getters.getPivotIds()) {\n            const definition = this.getters.getPivotCoreDefinition(pivotId);\n            if (definition.type !== \"SPREADSHEET\") {\n                continue;\n            }\n            if (definition.dataSet) {\n                const { sheetId, zone } = definition.dataSet;\n                const range = this.getters.getRangeFromZone(sheetId, zone);\n                const adaptedRange = adaptPivotRange(range, applyChange);\n                const dataSet = adaptedRange && {\n                    sheetId: adaptedRange.sheetId,\n                    zone: adaptedRange.zone,\n                };\n                this.dispatch(\"UPDATE_PIVOT\", { pivotId, pivot: { ...definition, dataSet } });\n            }\n        }\n    }\n}\n\nclass TableStylePlugin extends CorePlugin {\n    static getters = [\n        \"getNewCustomTableStyleName\",\n        \"getTableStyle\",\n        \"getTableStyles\",\n        \"isTableStyleEditable\",\n    ];\n    styles = {};\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_TABLE\":\n            case \"UPDATE_TABLE\":\n                if (cmd.config?.styleId && !this.styles[cmd.config.styleId]) {\n                    return \"InvalidTableConfig\" /* CommandResult.InvalidTableConfig */;\n                }\n                break;\n            case \"CREATE_TABLE_STYLE\":\n                if (!TABLE_STYLES_TEMPLATES[cmd.templateName]) {\n                    return \"InvalidTableStyle\" /* CommandResult.InvalidTableStyle */;\n                }\n                try {\n                    toHex(cmd.primaryColor);\n                }\n                catch (e) {\n                    return \"InvalidTableStyle\" /* CommandResult.InvalidTableStyle */;\n                }\n                break;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_TABLE_STYLE\":\n                const style = buildTableStyle(cmd.tableStyleName, cmd.templateName, cmd.primaryColor);\n                this.history.update(\"styles\", cmd.tableStyleId, style);\n                break;\n            case \"REMOVE_TABLE_STYLE\":\n                const styles = { ...this.styles };\n                delete styles[cmd.tableStyleId];\n                this.history.update(\"styles\", styles);\n                for (const sheetId of this.getters.getSheetIds()) {\n                    for (const table of this.getters.getCoreTables(sheetId)) {\n                        if (table.config.styleId === cmd.tableStyleId) {\n                            this.dispatch(\"UPDATE_TABLE\", {\n                                sheetId,\n                                zone: table.range.zone,\n                                config: { styleId: DEFAULT_TABLE_CONFIG.styleId },\n                            });\n                        }\n                    }\n                }\n                break;\n        }\n    }\n    getTableStyle(styleId) {\n        if (!this.styles[styleId]) {\n            throw new Error(`Table style ${styleId} does not exist`);\n        }\n        return this.styles[styleId];\n    }\n    getTableStyles() {\n        return this.styles;\n    }\n    getNewCustomTableStyleName() {\n        let name = _t(\"Custom Table Style\");\n        const styleNames = new Set(Object.values(this.styles).map((style) => style.displayName));\n        if (!styleNames.has(name)) {\n            return name;\n        }\n        let i = 2;\n        while (styleNames.has(`${name} ${i}`)) {\n            i++;\n        }\n        return `${name} ${i}`;\n    }\n    isTableStyleEditable(styleId) {\n        return !TABLE_PRESETS[styleId];\n    }\n    import(data) {\n        for (const presetStyleId in TABLE_PRESETS) {\n            this.styles[presetStyleId] = TABLE_PRESETS[presetStyleId];\n        }\n        for (const styleId in data.customTableStyles) {\n            const styleData = data.customTableStyles[styleId];\n            this.styles[styleId] = buildTableStyle(styleData.displayName, styleData.templateName, styleData.primaryColor);\n        }\n    }\n    export(data) {\n        const exportedStyles = {};\n        for (const styleId in this.styles) {\n            if (!TABLE_PRESETS[styleId]) {\n                exportedStyles[styleId] = {\n                    displayName: this.styles[styleId].displayName,\n                    templateName: this.styles[styleId].templateName,\n                    primaryColor: this.styles[styleId].primaryColor,\n                };\n            }\n        }\n        data.customTableStyles = exportedStyles;\n    }\n}\n\n/**\n * UI plugins handle any transient data required to display a spreadsheet.\n * They can draw on the grid canvas.\n */\nclass UIPlugin extends BasePlugin {\n    static layers = [];\n    getters;\n    ui;\n    selection;\n    constructor({ getters, stateObserver, dispatch, canDispatch, uiActions, selection, }) {\n        super(stateObserver, dispatch, canDispatch);\n        this.getters = getters;\n        this.ui = uiActions;\n        this.selection = selection;\n    }\n    // ---------------------------------------------------------------------------\n    // Grid rendering\n    // ---------------------------------------------------------------------------\n    drawLayer(ctx, layer) { }\n}\n\n/**\n * This registry is used to register functions that should be called after each iteration of the evaluation.\n * This is use currently to mark the each pivot to be re-evaluated. We have to do this after each iteration\n * to avoid to reload the data of the pivot at each function call (PIVOT.VALUE and PIVOT.HEADER). After each\n * evaluation iteration, the pivot has to be re-evaluated during the next iteration.\n */\nconst onIterationEndEvaluationRegistry = new Registry();\nonIterationEndEvaluationRegistry.add(\"pivots\", (getters) => {\n    for (const pivotId of getters.getPivotIds()) {\n        const pivot = getters.getPivot(pivotId);\n        pivotRegistry.get(pivot.type).onIterationEndEvaluation(pivot);\n    }\n});\n\nconst functionMap = functionRegistry.mapping;\n/**\n * Return all functions necessary to properly evaluate a formula:\n * - a refFn function to read any reference, cell or range of a normalized formula\n * - a range function to convert any reference to a proper value array\n * - an evaluation context\n */\nfunction buildCompilationParameters(context, getters, computeCell) {\n    const builder = new CompilationParametersBuilder(context, getters, computeCell);\n    return builder.getParameters();\n}\nclass CompilationParametersBuilder {\n    getters;\n    computeCell;\n    evalContext;\n    rangeCache = {};\n    constructor(context, getters, computeCell) {\n        this.getters = getters;\n        this.computeCell = computeCell;\n        this.evalContext = Object.assign(Object.create(functionMap), context, {\n            getters: this.getters,\n            locale: this.getters.getLocale(),\n        });\n    }\n    getParameters() {\n        return {\n            referenceDenormalizer: this.refFn.bind(this),\n            ensureRange: this.range.bind(this),\n            evalContext: this.evalContext,\n        };\n    }\n    /**\n     * Returns the value of the cell(s) used in reference\n     *\n     * @param range the references used\n     * @param isMeta if a reference is supposed to be used in a `meta` parameter as described in the\n     *        function for which this parameter is used, we just return the string of the parameter.\n     *        The `compute` of the formula's function must process it completely\n     */\n    refFn(range, isMeta) {\n        const rangeError = this.getRangeError(range);\n        if (rangeError) {\n            return rangeError;\n        }\n        if (isMeta) {\n            // Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges\n            const sheetName = this.getters.getSheetName(range.sheetId);\n            return { value: getFullReference(sheetName, zoneToXc(range.zone)) };\n        }\n        // the compiler guarantees only single cell ranges reach this part of the code\n        const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };\n        return this.computeCell(position);\n    }\n    /**\n     * Return the values of the cell(s) used in reference, but always in the format of a range even\n     * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as\n     * range<number> etc.\n     *\n     * Note that each col is possibly sparse: it only contain the values of cells\n     * that are actually present in the grid.\n     */\n    range(range) {\n        const rangeError = this.getRangeError(range);\n        if (rangeError) {\n            return [[rangeError]];\n        }\n        const sheetId = range.sheetId;\n        const zone = range.zone;\n        // Performance issue: Avoid fetching data on positions that are out of the spreadsheet\n        // e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array\n        const sheetZone = this.getters.getSheetZone(sheetId);\n        const _zone = intersection(zone, sheetZone);\n        if (!_zone) {\n            return [[]];\n        }\n        const { top, left, bottom, right } = zone;\n        const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}`;\n        if (cacheKey in this.rangeCache) {\n            return this.rangeCache[cacheKey];\n        }\n        const height = _zone.bottom - _zone.top + 1;\n        const width = _zone.right - _zone.left + 1;\n        const matrix = new Array(width);\n        // Performance issue: nested loop is faster than a map here\n        for (let col = _zone.left; col <= _zone.right; col++) {\n            const colIndex = col - _zone.left;\n            matrix[colIndex] = new Array(height);\n            for (let row = _zone.top; row <= _zone.bottom; row++) {\n                const rowIndex = row - _zone.top;\n                matrix[colIndex][rowIndex] = this.computeCell({ sheetId, col, row });\n            }\n        }\n        this.rangeCache[cacheKey] = matrix;\n        return matrix;\n    }\n    getRangeError(range) {\n        if (!isZoneValid(range.zone)) {\n            return new InvalidReferenceError();\n        }\n        if (range.invalidSheetName) {\n            return new EvaluationError(_t(\"Invalid sheet name: %s\", range.invalidSheetName));\n        }\n        return undefined;\n    }\n}\n\nfunction quickselect(arr, k, left, right, compare) {\n    quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);\n}\n\nfunction quickselectStep(arr, k, left, right, compare) {\n\n    while (right > left) {\n        if (right - left > 600) {\n            var n = right - left + 1;\n            var m = k - left + 1;\n            var z = Math.log(n);\n            var s = 0.5 * Math.exp(2 * z / 3);\n            var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);\n            var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));\n            var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));\n            quickselectStep(arr, k, newLeft, newRight, compare);\n        }\n\n        var t = arr[k];\n        var i = left;\n        var j = right;\n\n        swap(arr, left, k);\n        if (compare(arr[right], t) > 0) swap(arr, left, right);\n\n        while (i < j) {\n            swap(arr, i, j);\n            i++;\n            j--;\n            while (compare(arr[i], t) < 0) i++;\n            while (compare(arr[j], t) > 0) j--;\n        }\n\n        if (compare(arr[left], t) === 0) swap(arr, left, j);\n        else {\n            j++;\n            swap(arr, j, right);\n        }\n\n        if (j <= k) left = j + 1;\n        if (k <= j) right = j - 1;\n    }\n}\n\nfunction swap(arr, i, j) {\n    var tmp = arr[i];\n    arr[i] = arr[j];\n    arr[j] = tmp;\n}\n\nfunction defaultCompare(a, b) {\n    return a < b ? -1 : a > b ? 1 : 0;\n}\n\nclass RBush {\n    constructor(maxEntries = 9) {\n        // max entries in a node is 9 by default; min node fill is 40% for best performance\n        this._maxEntries = Math.max(4, maxEntries);\n        this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));\n        this.clear();\n    }\n\n    all() {\n        return this._all(this.data, []);\n    }\n\n    search(bbox) {\n        let node = this.data;\n        const result = [];\n\n        if (!intersects(bbox, node)) return result;\n\n        const toBBox = this.toBBox;\n        const nodesToSearch = [];\n\n        while (node) {\n            for (let i = 0; i < node.children.length; i++) {\n                const child = node.children[i];\n                const childBBox = node.leaf ? toBBox(child) : child;\n\n                if (intersects(bbox, childBBox)) {\n                    if (node.leaf) result.push(child);\n                    else if (contains(bbox, childBBox)) this._all(child, result);\n                    else nodesToSearch.push(child);\n                }\n            }\n            node = nodesToSearch.pop();\n        }\n\n        return result;\n    }\n\n    collides(bbox) {\n        let node = this.data;\n\n        if (!intersects(bbox, node)) return false;\n\n        const nodesToSearch = [];\n        while (node) {\n            for (let i = 0; i < node.children.length; i++) {\n                const child = node.children[i];\n                const childBBox = node.leaf ? this.toBBox(child) : child;\n\n                if (intersects(bbox, childBBox)) {\n                    if (node.leaf || contains(bbox, childBBox)) return true;\n                    nodesToSearch.push(child);\n                }\n            }\n            node = nodesToSearch.pop();\n        }\n\n        return false;\n    }\n\n    load(data) {\n        if (!(data && data.length)) return this;\n\n        if (data.length < this._minEntries) {\n            for (let i = 0; i < data.length; i++) {\n                this.insert(data[i]);\n            }\n            return this;\n        }\n\n        // recursively build the tree with the given data from scratch using OMT algorithm\n        let node = this._build(data.slice(), 0, data.length - 1, 0);\n\n        if (!this.data.children.length) {\n            // save as is if tree is empty\n            this.data = node;\n\n        } else if (this.data.height === node.height) {\n            // split root if trees have the same height\n            this._splitRoot(this.data, node);\n\n        } else {\n            if (this.data.height < node.height) {\n                // swap trees if inserted one is bigger\n                const tmpNode = this.data;\n                this.data = node;\n                node = tmpNode;\n            }\n\n            // insert the small tree into the large tree at appropriate level\n            this._insert(node, this.data.height - node.height - 1, true);\n        }\n\n        return this;\n    }\n\n    insert(item) {\n        if (item) this._insert(item, this.data.height - 1);\n        return this;\n    }\n\n    clear() {\n        this.data = createNode([]);\n        return this;\n    }\n\n    remove(item, equalsFn) {\n        if (!item) return this;\n\n        let node = this.data;\n        const bbox = this.toBBox(item);\n        const path = [];\n        const indexes = [];\n        let i, parent, goingUp;\n\n        // depth-first iterative tree traversal\n        while (node || path.length) {\n\n            if (!node) { // go up\n                node = path.pop();\n                parent = path[path.length - 1];\n                i = indexes.pop();\n                goingUp = true;\n            }\n\n            if (node.leaf) { // check current node\n                const index = findItem(item, node.children, equalsFn);\n\n                if (index !== -1) {\n                    // item found, remove the item and condense tree upwards\n                    node.children.splice(index, 1);\n                    path.push(node);\n                    this._condense(path);\n                    return this;\n                }\n            }\n\n            if (!goingUp && !node.leaf && contains(node, bbox)) { // go down\n                path.push(node);\n                indexes.push(i);\n                i = 0;\n                parent = node;\n                node = node.children[0];\n\n            } else if (parent) { // go right\n                i++;\n                node = parent.children[i];\n                goingUp = false;\n\n            } else node = null; // nothing found\n        }\n\n        return this;\n    }\n\n    toBBox(item) { return item; }\n\n    compareMinX(a, b) { return a.minX - b.minX; }\n    compareMinY(a, b) { return a.minY - b.minY; }\n\n    toJSON() { return this.data; }\n\n    fromJSON(data) {\n        this.data = data;\n        return this;\n    }\n\n    _all(node, result) {\n        const nodesToSearch = [];\n        while (node) {\n            if (node.leaf) result.push(...node.children);\n            else nodesToSearch.push(...node.children);\n\n            node = nodesToSearch.pop();\n        }\n        return result;\n    }\n\n    _build(items, left, right, height) {\n\n        const N = right - left + 1;\n        let M = this._maxEntries;\n        let node;\n\n        if (N <= M) {\n            // reached leaf level; return leaf\n            node = createNode(items.slice(left, right + 1));\n            calcBBox(node, this.toBBox);\n            return node;\n        }\n\n        if (!height) {\n            // target height of the bulk-loaded tree\n            height = Math.ceil(Math.log(N) / Math.log(M));\n\n            // target number of root entries to maximize storage utilization\n            M = Math.ceil(N / Math.pow(M, height - 1));\n        }\n\n        node = createNode([]);\n        node.leaf = false;\n        node.height = height;\n\n        // split the items into M mostly square tiles\n\n        const N2 = Math.ceil(N / M);\n        const N1 = N2 * Math.ceil(Math.sqrt(M));\n\n        multiSelect(items, left, right, N1, this.compareMinX);\n\n        for (let i = left; i <= right; i += N1) {\n\n            const right2 = Math.min(i + N1 - 1, right);\n\n            multiSelect(items, i, right2, N2, this.compareMinY);\n\n            for (let j = i; j <= right2; j += N2) {\n\n                const right3 = Math.min(j + N2 - 1, right2);\n\n                // pack each entry recursively\n                node.children.push(this._build(items, j, right3, height - 1));\n            }\n        }\n\n        calcBBox(node, this.toBBox);\n\n        return node;\n    }\n\n    _chooseSubtree(bbox, node, level, path) {\n        while (true) {\n            path.push(node);\n\n            if (node.leaf || path.length - 1 === level) break;\n\n            let minArea = Infinity;\n            let minEnlargement = Infinity;\n            let targetNode;\n\n            for (let i = 0; i < node.children.length; i++) {\n                const child = node.children[i];\n                const area = bboxArea(child);\n                const enlargement = enlargedArea(bbox, child) - area;\n\n                // choose entry with the least area enlargement\n                if (enlargement < minEnlargement) {\n                    minEnlargement = enlargement;\n                    minArea = area < minArea ? area : minArea;\n                    targetNode = child;\n\n                } else if (enlargement === minEnlargement) {\n                    // otherwise choose one with the smallest area\n                    if (area < minArea) {\n                        minArea = area;\n                        targetNode = child;\n                    }\n                }\n            }\n\n            node = targetNode || node.children[0];\n        }\n\n        return node;\n    }\n\n    _insert(item, level, isNode) {\n        const bbox = isNode ? item : this.toBBox(item);\n        const insertPath = [];\n\n        // find the best node for accommodating the item, saving all nodes along the path too\n        const node = this._chooseSubtree(bbox, this.data, level, insertPath);\n\n        // put the item into the node\n        node.children.push(item);\n        extend(node, bbox);\n\n        // split on node overflow; propagate upwards if necessary\n        while (level >= 0) {\n            if (insertPath[level].children.length > this._maxEntries) {\n                this._split(insertPath, level);\n                level--;\n            } else break;\n        }\n\n        // adjust bboxes along the insertion path\n        this._adjustParentBBoxes(bbox, insertPath, level);\n    }\n\n    // split overflowed node into two\n    _split(insertPath, level) {\n        const node = insertPath[level];\n        const M = node.children.length;\n        const m = this._minEntries;\n\n        this._chooseSplitAxis(node, m, M);\n\n        const splitIndex = this._chooseSplitIndex(node, m, M);\n\n        const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));\n        newNode.height = node.height;\n        newNode.leaf = node.leaf;\n\n        calcBBox(node, this.toBBox);\n        calcBBox(newNode, this.toBBox);\n\n        if (level) insertPath[level - 1].children.push(newNode);\n        else this._splitRoot(node, newNode);\n    }\n\n    _splitRoot(node, newNode) {\n        // split root node\n        this.data = createNode([node, newNode]);\n        this.data.height = node.height + 1;\n        this.data.leaf = false;\n        calcBBox(this.data, this.toBBox);\n    }\n\n    _chooseSplitIndex(node, m, M) {\n        let index;\n        let minOverlap = Infinity;\n        let minArea = Infinity;\n\n        for (let i = m; i <= M - m; i++) {\n            const bbox1 = distBBox(node, 0, i, this.toBBox);\n            const bbox2 = distBBox(node, i, M, this.toBBox);\n\n            const overlap = intersectionArea(bbox1, bbox2);\n            const area = bboxArea(bbox1) + bboxArea(bbox2);\n\n            // choose distribution with minimum overlap\n            if (overlap < minOverlap) {\n                minOverlap = overlap;\n                index = i;\n\n                minArea = area < minArea ? area : minArea;\n\n            } else if (overlap === minOverlap) {\n                // otherwise choose distribution with minimum area\n                if (area < minArea) {\n                    minArea = area;\n                    index = i;\n                }\n            }\n        }\n\n        return index || M - m;\n    }\n\n    // sorts node children by the best axis for split\n    _chooseSplitAxis(node, m, M) {\n        const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;\n        const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;\n        const xMargin = this._allDistMargin(node, m, M, compareMinX);\n        const yMargin = this._allDistMargin(node, m, M, compareMinY);\n\n        // if total distributions margin value is minimal for x, sort by minX,\n        // otherwise it's already sorted by minY\n        if (xMargin < yMargin) node.children.sort(compareMinX);\n    }\n\n    // total margin of all possible split distributions where each node is at least m full\n    _allDistMargin(node, m, M, compare) {\n        node.children.sort(compare);\n\n        const toBBox = this.toBBox;\n        const leftBBox = distBBox(node, 0, m, toBBox);\n        const rightBBox = distBBox(node, M - m, M, toBBox);\n        let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);\n\n        for (let i = m; i < M - m; i++) {\n            const child = node.children[i];\n            extend(leftBBox, node.leaf ? toBBox(child) : child);\n            margin += bboxMargin(leftBBox);\n        }\n\n        for (let i = M - m - 1; i >= m; i--) {\n            const child = node.children[i];\n            extend(rightBBox, node.leaf ? toBBox(child) : child);\n            margin += bboxMargin(rightBBox);\n        }\n\n        return margin;\n    }\n\n    _adjustParentBBoxes(bbox, path, level) {\n        // adjust bboxes along the given tree path\n        for (let i = level; i >= 0; i--) {\n            extend(path[i], bbox);\n        }\n    }\n\n    _condense(path) {\n        // go through the path, removing empty nodes and updating bboxes\n        for (let i = path.length - 1, siblings; i >= 0; i--) {\n            if (path[i].children.length === 0) {\n                if (i > 0) {\n                    siblings = path[i - 1].children;\n                    siblings.splice(siblings.indexOf(path[i]), 1);\n\n                } else this.clear();\n\n            } else calcBBox(path[i], this.toBBox);\n        }\n    }\n}\n\nfunction findItem(item, items, equalsFn) {\n    if (!equalsFn) return items.indexOf(item);\n\n    for (let i = 0; i < items.length; i++) {\n        if (equalsFn(item, items[i])) return i;\n    }\n    return -1;\n}\n\n// calculate node's bbox from bboxes of its children\nfunction calcBBox(node, toBBox) {\n    distBBox(node, 0, node.children.length, toBBox, node);\n}\n\n// min bounding rectangle of node children from k to p-1\nfunction distBBox(node, k, p, toBBox, destNode) {\n    if (!destNode) destNode = createNode(null);\n    destNode.minX = Infinity;\n    destNode.minY = Infinity;\n    destNode.maxX = -Infinity;\n    destNode.maxY = -Infinity;\n\n    for (let i = k; i < p; i++) {\n        const child = node.children[i];\n        extend(destNode, node.leaf ? toBBox(child) : child);\n    }\n\n    return destNode;\n}\n\nfunction extend(a, b) {\n    a.minX = Math.min(a.minX, b.minX);\n    a.minY = Math.min(a.minY, b.minY);\n    a.maxX = Math.max(a.maxX, b.maxX);\n    a.maxY = Math.max(a.maxY, b.maxY);\n    return a;\n}\n\nfunction compareNodeMinX(a, b) { return a.minX - b.minX; }\nfunction compareNodeMinY(a, b) { return a.minY - b.minY; }\n\nfunction bboxArea(a)   { return (a.maxX - a.minX) * (a.maxY - a.minY); }\nfunction bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }\n\nfunction enlargedArea(a, b) {\n    return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *\n           (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));\n}\n\nfunction intersectionArea(a, b) {\n    const minX = Math.max(a.minX, b.minX);\n    const minY = Math.max(a.minY, b.minY);\n    const maxX = Math.min(a.maxX, b.maxX);\n    const maxY = Math.min(a.maxY, b.maxY);\n\n    return Math.max(0, maxX - minX) *\n           Math.max(0, maxY - minY);\n}\n\nfunction contains(a, b) {\n    return a.minX <= b.minX &&\n           a.minY <= b.minY &&\n           b.maxX <= a.maxX &&\n           b.maxY <= a.maxY;\n}\n\nfunction intersects(a, b) {\n    return b.minX <= a.maxX &&\n           b.minY <= a.maxY &&\n           b.maxX >= a.minX &&\n           b.maxY >= a.minY;\n}\n\nfunction createNode(children) {\n    return {\n        children,\n        height: 1,\n        leaf: true,\n        minX: Infinity,\n        minY: Infinity,\n        maxX: -Infinity,\n        maxY: -Infinity\n    };\n}\n\n// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;\n// combines selection algorithm with binary divide & conquer approach\n\nfunction multiSelect(arr, left, right, n, compare) {\n    const stack = [left, right];\n\n    while (stack.length) {\n        right = stack.pop();\n        left = stack.pop();\n\n        if (right - left <= n) continue;\n\n        const mid = left + Math.ceil((right - left) / n / 2) * n;\n        quickselect(arr, mid, left, right, compare);\n\n        stack.push(left, mid, mid, right);\n    }\n}\n\n/**\n * R-Tree Data Structure\n *\n * R-Tree is a spatial data structure used for efficient indexing and querying\n * of multi-dimensional objects, particularly in geometric and spatial applications.\n *\n * It organizes objects into a tree hierarchy, grouping nearby objects together\n * in bounding boxes. Each node in the tree represents a bounding box that\n * contains its child nodes or leaf objects. This hierarchical structure allows\n * for faster spatial queries.\n *\n * @see https://en.wikipedia.org/wiki/R-tree\n *\n * Consider a 2D Space with four zones: A, B, C, D\n * +--------------------------+\n * |                          |\n * |   +---+     +-------+    |\n * |   | A |     |   B   |    |\n * |   +---+     +-------+    |\n * |                          |\n * |                          |\n * |          +---+           |\n * |          | C |           |\n * |          +---+           |\n * |      +-----------+       |\n * |      |     D     |       |\n * |      +-----------+       |\n * |                          |\n * +--------------------------+\n *\n * It groups together zones that are spatially close into a minimum bounding box.\n * For example, A and B are grouped together in rectangle R1, and C and D are grouped\n * in R2.\n *\n * R0\n * +--------------------------+\n * |   R1                     |\n * |   +-----------------+    |\n * |   | A |     |   B   |    |\n * |   +-----------------+    |\n * |                          |\n * |      R2                  |\n * |      +---+---+---+       |\n * |      |   | C |   |       |\n * |      |   +---+   |       |\n * |      +-----------+       |\n * |      |     D     |       |\n * |      +-----------+       |\n * |                          |\n * +--------------------------+\n *\n * The tree would look like this:\n *          R0\n *         /  \\\n *        /    \\\n *       R1     R2\n *       |      |\n *      A,B    C,D\n\n * Choosing how to group the zones is crucial for the performance of the tree.\n * Key considerations include avoiding excessive empty space coverage and minimizing overlap\n * to reduce the number of subtrees processed during searches.\n *\n * Various heuristics exist for determining the optimal grouping strategy, such as \"least enlargement\"\n * which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where\n * the choice cannot be made based on this criterion due to the same enlargement for different groupings,\n * we then evaluate \"least area,\" aiming to minimize the overall area of bounding boxes.\n *\n * This implementation is tailored for spreadsheet use, indexing objects associated\n * with a zone and a sheet.\n *\n * It uses the RBush library under the hood. One 2D RBush R-tree per sheet.\n * @see https://github.com/mourner/rbush\n */\nclass SpreadsheetRTree {\n    /**\n     * One 2D R-tree per sheet\n     */\n    rTrees = {};\n    /**\n     * Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times\n     * faster than inserting items one by one. After bulk loading (bulk insertion into\n     * an empty tree), subsequent query performance is also ~20-30% better.\n     */\n    constructor(items = []) {\n        const rangesPerSheet = {};\n        for (const item of items) {\n            const sheetId = item.boundingBox.sheetId;\n            if (!rangesPerSheet[sheetId]) {\n                rangesPerSheet[sheetId] = [];\n            }\n            rangesPerSheet[sheetId].push(item);\n        }\n        for (const sheetId in rangesPerSheet) {\n            this.rTrees[sheetId] = new ZoneRBush();\n            this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert\n        }\n    }\n    insert(item) {\n        const sheetId = item.boundingBox.sheetId;\n        if (!this.rTrees[sheetId]) {\n            this.rTrees[sheetId] = new ZoneRBush();\n        }\n        this.rTrees[sheetId].insert(item);\n    }\n    search({ zone, sheetId }) {\n        if (!this.rTrees[sheetId]) {\n            return [];\n        }\n        return this.rTrees[sheetId].search({\n            minX: zone.left,\n            minY: zone.top,\n            maxX: zone.right,\n            maxY: zone.bottom,\n        });\n    }\n    remove(item) {\n        const sheetId = item.boundingBox.sheetId;\n        if (!this.rTrees[sheetId]) {\n            return;\n        }\n        this.rTrees[sheetId].remove(item, this.rtreeItemComparer);\n    }\n    rtreeItemComparer(left, right) {\n        return (left.data == right.data &&\n            left.boundingBox.sheetId === right.boundingBox.sheetId &&\n            left.boundingBox?.zone.left === right.boundingBox.zone.left &&\n            left.boundingBox?.zone.top === right.boundingBox.zone.top &&\n            left.boundingBox?.zone.right === right.boundingBox.zone.right &&\n            left.boundingBox?.zone.bottom === right.boundingBox.zone.bottom);\n    }\n}\n/**\n * RBush extension to use zones as bounding boxes\n */\nclass ZoneRBush extends RBush {\n    toBBox({ boundingBox }) {\n        const zone = boundingBox.zone;\n        return {\n            minX: zone.left,\n            minY: zone.top,\n            maxX: zone.right,\n            maxY: zone.bottom,\n        };\n    }\n    compareMinX(a, b) {\n        return a.boundingBox.zone.left - b.boundingBox.zone.left;\n    }\n    compareMinY(a, b) {\n        return a.boundingBox.zone.top - b.boundingBox.zone.top;\n    }\n}\n\n/**\n * Implementation of a dependency Graph.\n * The graph is used to evaluate the cells in the correct\n * order, and should be updated each time a cell's content is modified\n *\n * It uses an R-Tree data structure to efficiently find dependent cells.\n */\nclass FormulaDependencyGraph {\n    createEmptyPositionSet;\n    dependencies = new PositionMap();\n    rTree;\n    constructor(createEmptyPositionSet, data = []) {\n        this.createEmptyPositionSet = createEmptyPositionSet;\n        this.rTree = new SpreadsheetRTree(data);\n    }\n    removeAllDependencies(formulaPosition) {\n        const ranges = this.dependencies.get(formulaPosition);\n        if (!ranges) {\n            return;\n        }\n        for (const range of ranges) {\n            this.rTree.remove(range);\n        }\n        this.dependencies.delete(formulaPosition);\n    }\n    addDependencies(formulaPosition, dependencies) {\n        const rTreeItems = dependencies.map(({ sheetId, zone }) => ({\n            data: formulaPosition,\n            boundingBox: {\n                zone,\n                sheetId,\n            },\n        }));\n        for (const item of rTreeItems) {\n            this.rTree.insert(item);\n        }\n        const existingDependencies = this.dependencies.get(formulaPosition);\n        if (existingDependencies) {\n            existingDependencies.push(...rTreeItems);\n        }\n        else {\n            this.dependencies.set(formulaPosition, rTreeItems);\n        }\n    }\n    /**\n     * Return all the cells that depend on the provided ranges,\n     * in the correct order they should be evaluated.\n     * This is called a topological ordering (excluding cycles)\n     */\n    getCellsDependingOn(ranges) {\n        const visited = this.createEmptyPositionSet();\n        const queue = Array.from(ranges).reverse();\n        while (queue.length > 0) {\n            const range = queue.pop();\n            const zone = range.zone;\n            const sheetId = range.sheetId;\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    visited.add({ sheetId, col, row });\n                }\n            }\n            const impactedPositions = this.rTree.search(range).map((dep) => dep.data);\n            const nextInQueue = {};\n            for (const position of impactedPositions) {\n                if (!visited.has(position)) {\n                    if (!nextInQueue[position.sheetId]) {\n                        nextInQueue[position.sheetId] = [];\n                    }\n                    nextInQueue[position.sheetId].push(positionToZone(position));\n                }\n            }\n            for (const sheetId in nextInQueue) {\n                const zones = recomputeZones(nextInQueue[sheetId], []);\n                queue.push(...zones.map((zone) => ({ sheetId, zone })));\n            }\n        }\n        // remove initial ranges\n        for (const range of ranges) {\n            const zone = range.zone;\n            const sheetId = range.sheetId;\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    visited.delete({ sheetId, col, row });\n                }\n            }\n        }\n        return visited;\n    }\n}\n\n/**\n * Implements a fixed-sized grid or 2D matrix of bits.\n * based on https://github.com/zandaqo/structurae\n *\n * The grid is implemented as a 1D array of 32-bit integers, where each bit represents a cell in the grid.\n * It follows row-major order, with each row stored consecutively in 32-bit blocks.\n * Pads the number of columns to the next power of 2 to allow quick lookups with bitwise operations.\n *\n * Key terminology:\n * - bucket: Index of an item in the Uint32Array, a 32-bit integer.\n * - bitPosition: The position of a bit within the bucket 32-bit integer.\n */\nclass BinaryGrid extends Uint32Array {\n    columnOffset = 0;\n    cols = 0;\n    rows = 0;\n    /**\n     * Creates a binary grid of specified dimensions.\n     */\n    static create(rows, columns) {\n        const columnOffset = log2Ceil(columns);\n        const length = (rows << columnOffset) >> 5;\n        const grid = new this(length + 1);\n        grid.columnOffset = columnOffset;\n        grid.cols = columns;\n        grid.rows = rows;\n        return grid;\n    }\n    /**\n     * Returns the bit at given coordinates.\n     */\n    getValue(position) {\n        const [bucket, bitPosition] = this.getCoordinates(position);\n        return ((this[bucket] >> bitPosition) & 1);\n    }\n    /**\n     * Sets the bit at given coordinates.\n     */\n    setValue(position, value) {\n        const [bucket, bitPosition] = this.getCoordinates(position);\n        const currentValue = (this[bucket] >> bitPosition) & 1;\n        const hasBeenInserted = currentValue === 0 && value === 1;\n        this[bucket] = (this[bucket] & ~(1 << bitPosition)) | (value << bitPosition);\n        return hasBeenInserted;\n        // Let's breakdown of the above line:\n        // with an example with a 4-bit integer (instead of 32-bit).\n        //\n        // Let's say we want to set the bit at position 2 to 1 and the existing\n        // bit sequence this[bucket] is 1001. The final bit sequence should be 1101.\n        //\n        // First, we clear the bit at position 2 by AND-ing this[bucket] with a\n        // mask having all 1s except a 0 at the bit position (~ (1 << bitPosition)).\n        // 1 << bitPosition is 0100 (shifting 0001 to the left by 2)\n        // Inverting the bits with ~ gives the final mask ~(1 << bitPosition): 1011\n        //\n        // Then, we shift the value by the bit position (value << bitPosition: 0100)\n        // and OR the result with the previous step's result:\n        // (1001 & 1011) | 0100 = 1101\n    }\n    isEmpty() {\n        return !this.some((bucket) => bucket !== 0);\n    }\n    fillAllPositions() {\n        const thirtyTwoOnes = -1 >>> 0; // same as 2 ** 32 - 1, a 32-bit number with all bits set to 1\n        this.fill(thirtyTwoOnes);\n    }\n    clear() {\n        this.fill(0);\n    }\n    getCoordinates(position) {\n        const { row, col } = position;\n        const index = (row << this.columnOffset) + col;\n        const bucket = index >> 5;\n        return [bucket, index - (bucket << 5)];\n    }\n}\nfunction log2Ceil(value) {\n    // A faster version of Math.ceil(Math.log2(value)).\n    if (value === 0) {\n        return -Infinity;\n    }\n    else if (value < 0) {\n        return NaN;\n    }\n    // --value handles the case where value is a power of 2\n    return 32 - Math.clz32(--value);\n}\n\nclass PositionSet {\n    sheets = {};\n    /**\n     * List of positions in the order they were inserted.\n     */\n    insertions = [];\n    maxSize = 0;\n    constructor(sheetSizes) {\n        for (const sheetId in sheetSizes) {\n            const cols = sheetSizes[sheetId].cols;\n            const rows = sheetSizes[sheetId].rows;\n            this.maxSize += cols * rows;\n            this.sheets[sheetId] = BinaryGrid.create(rows, cols);\n        }\n    }\n    add(position) {\n        const hasBeenInserted = this.sheets[position.sheetId].setValue(position, 1);\n        if (hasBeenInserted) {\n            this.insertions.push(position);\n        }\n    }\n    addMany(positions) {\n        for (const position of positions) {\n            this.add(position);\n        }\n    }\n    delete(position) {\n        this.sheets[position.sheetId].setValue(position, 0);\n    }\n    deleteMany(positions) {\n        for (const position of positions) {\n            this.delete(position);\n        }\n    }\n    has(position) {\n        return this.sheets[position.sheetId].getValue(position) === 1;\n    }\n    clear() {\n        const insertions = [...this];\n        this.insertions = [];\n        for (const sheetId in this.sheets) {\n            this.sheets[sheetId].clear();\n        }\n        return insertions;\n    }\n    isEmpty() {\n        if (this.insertions.length === 0) {\n            return true;\n        }\n        for (const sheetId in this.sheets) {\n            if (!this.sheets[sheetId].isEmpty()) {\n                return false;\n            }\n        }\n        return true;\n    }\n    fillAllPositions() {\n        this.insertions = new Array(this.maxSize);\n        let index = 0;\n        for (const sheetId in this.sheets) {\n            const grid = this.sheets[sheetId];\n            grid.fillAllPositions();\n            for (let i = 0; i < grid.rows; i++) {\n                for (let j = 0; j < grid.cols; j++) {\n                    this.insertions[index++] = { sheetId, row: i, col: j };\n                }\n            }\n        }\n    }\n    /**\n     * Iterate over the positions in the order of insertion.\n     * Note that the same position may be yielded multiple times if the value was added\n     * to the set then removed and then added again.\n     */\n    *[Symbol.iterator]() {\n        for (const position of this.insertions) {\n            if (this.sheets[position.sheetId].getValue(position) === 1) {\n                yield position;\n            }\n        }\n    }\n}\n\n/**\n * Contains, for each cell, the array\n * formulas that could potentially spread on it\n * This is essentially a two way mapping between array formulas\n * and their results.\n *\n * As we don't allow two array formulas to spread on the same cell, this structure\n * is used to force the reevaluation of the potential spreaders of a cell when the\n * content of this cell is modified. This structure should be updated each time\n * an array formula is evaluated and try to spread on another cell.\n *\n */\nclass SpreadingRelation {\n    /**\n     * Internal structure:\n     * For something like\n     * - A2:'=SPLIT(\"KAYAK\", \"A\")'\n     * - B1:'=TRANSPOSE(SPLIT(\"COYOTE\", \"O\"))'\n     *\n     * Resulting in:\n     * ```\n     * -----------------\n     * |   | A | B | C |\n     * |---+---+---+---|\n     * | 1 |   | C |   |\n     * | 2 | K | Y | K |\n     * | 3 |   | T |   |\n     * | 4 |   | E |   |\n     * -----------------\n     * ```\n     * We have `resultsToArrayFormulas` is an R-tree looking like:\n     * - (A2:C2) --> A2     meaning values in A2:C2 are the result of A2\n     * - (B1:B4) --> B1     meaning values in B1:B4 are the result of B1\n     *\n     * Note that B2 is part of both zones because it can be the result of\n     * A2 or B1.\n     * Using an R-tree allows for fast insertions while still having\n     * a relatively fast lookup.\n     *\n     * We have `arrayFormulasToResults` looking like:\n     * - (A2) --> A2:C2     meaning A2 spreads on the zone A2:C2\n     * - (B1) --> B1:B4     meaning B1 spreads on the zone B1:B4\n     *\n     */\n    resultsToArrayFormulas = new SpreadsheetRTree();\n    arrayFormulasToResults = new PositionMap();\n    searchFormulaPositionsSpreadingOn(sheetId, zone) {\n        return (this.resultsToArrayFormulas.search({ sheetId, zone }).map((node) => node.data) || EMPTY_ARRAY);\n    }\n    getArrayResultZone(formulasPosition) {\n        return this.arrayFormulasToResults.get(formulasPosition);\n    }\n    /**\n     * Remove a node, also remove it from other nodes adjacency list\n     */\n    removeNode(position) {\n        this.resultsToArrayFormulas.remove({\n            boundingBox: { sheetId: position.sheetId, zone: positionToZone(position) },\n            data: position,\n        });\n        this.arrayFormulasToResults.delete(position);\n    }\n    /**\n     * Create a spreading relation between two cells\n     */\n    addRelation({ arrayFormulaPosition, resultZone: resultPosition, }) {\n        this.resultsToArrayFormulas.insert({\n            boundingBox: { sheetId: arrayFormulaPosition.sheetId, zone: resultPosition },\n            data: arrayFormulaPosition,\n        });\n        this.arrayFormulasToResults.set(arrayFormulaPosition, resultPosition);\n    }\n    isArrayFormula(position) {\n        return this.arrayFormulasToResults.has(position);\n    }\n}\nconst EMPTY_ARRAY = [];\n\nconst MAX_ITERATION = 30;\nconst ERROR_CYCLE_CELL = createEvaluatedCell(new CircularDependencyError());\nconst EMPTY_CELL = createEvaluatedCell({ value: null });\nclass Evaluator {\n    context;\n    getters;\n    compilationParams;\n    evaluatedCells = new PositionMap();\n    formulaDependencies = lazy(new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this)));\n    blockedArrayFormulas = new PositionSet({});\n    spreadingRelations = new SpreadingRelation();\n    constructor(context, getters) {\n        this.context = context;\n        this.getters = getters;\n        this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));\n    }\n    getEvaluatedCell(position) {\n        return this.evaluatedCells.get(position) || EMPTY_CELL;\n    }\n    getSpreadZone(position, options = { ignoreSpillError: false }) {\n        const spreadZone = this.spreadingRelations.getArrayResultZone(position);\n        if (!spreadZone) {\n            return undefined;\n        }\n        const evaluatedCell = this.evaluatedCells.get(position);\n        if (evaluatedCell?.type === CellValueType.error &&\n            !(options.ignoreSpillError && evaluatedCell?.value === CellErrorType.SpilledBlocked)) {\n            return positionToZone(position);\n        }\n        return union(positionToZone(position), spreadZone);\n    }\n    getEvaluatedPositions() {\n        return this.evaluatedCells.keys();\n    }\n    getEvaluatedPositionsInSheet(sheetId) {\n        return this.evaluatedCells.keysForSheet(sheetId);\n    }\n    getArrayFormulaSpreadingOn(position) {\n        const hasArrayFormulaResult = this.getEvaluatedCell(position).type !== CellValueType.empty &&\n            !this.getters.getCell(position)?.isFormula;\n        if (!hasArrayFormulaResult) {\n            return this.spreadingRelations.isArrayFormula(position) ? position : undefined;\n        }\n        const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));\n        return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));\n    }\n    updateDependencies(position) {\n        // removing dependencies is slow because it requires\n        // to traverse the entire r-tree.\n        // The data structure is optimized for searches the other way around\n        this.formulaDependencies().removeAllDependencies(position);\n        const dependencies = this.getDirectDependencies(position);\n        this.formulaDependencies().addDependencies(position, dependencies);\n    }\n    addDependencies(position, dependencies) {\n        this.formulaDependencies().addDependencies(position, dependencies);\n        for (const range of dependencies) {\n            const sheetId = range.sheetId;\n            const { left, bottom, right, top } = range.zone;\n            for (let col = left; col <= right; col++) {\n                for (let row = top; row <= bottom; row++) {\n                    this.computeAndSave({ sheetId, col, row });\n                }\n            }\n        }\n    }\n    updateCompilationParameters() {\n        // rebuild the compilation parameters (with a clean cache)\n        this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));\n        this.compilationParams.evalContext.updateDependencies = this.updateDependencies.bind(this);\n        this.compilationParams.evalContext.addDependencies = this.addDependencies.bind(this);\n    }\n    createEmptyPositionSet() {\n        const sheetSizes = {};\n        for (const sheetId of this.getters.getSheetIds()) {\n            sheetSizes[sheetId] = {\n                rows: this.getters.getNumberRows(sheetId),\n                cols: this.getters.getNumberCols(sheetId),\n            };\n        }\n        return new PositionSet(sheetSizes);\n    }\n    evaluateCells(positions) {\n        const start = performance.now();\n        const cellsToCompute = this.createEmptyPositionSet();\n        cellsToCompute.addMany(positions);\n        const arrayFormulasPositions = this.getArrayFormulasImpactedByChangesOf(positions);\n        cellsToCompute.addMany(this.getCellsDependingOn(positions));\n        cellsToCompute.addMany(arrayFormulasPositions);\n        cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));\n        this.evaluate(cellsToCompute);\n        console.debug(\"evaluate Cells\", performance.now() - start, \"ms\");\n    }\n    getArrayFormulasImpactedByChangesOf(positions) {\n        const impactedPositions = this.createEmptyPositionSet();\n        for (const position of positions) {\n            const content = this.getters.getCell(position)?.content;\n            const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);\n            if (arrayFormulaPosition !== undefined) {\n                // take into account new collisions.\n                impactedPositions.add(arrayFormulaPosition);\n            }\n            if (!content) {\n                // The previous content could have blocked some array formulas\n                impactedPositions.add(position);\n            }\n        }\n        const zonesBySheetIds = aggregatePositionsToZones(impactedPositions);\n        for (const sheetId in zonesBySheetIds) {\n            for (const zone of zonesBySheetIds[sheetId]) {\n                impactedPositions.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));\n            }\n        }\n        return impactedPositions;\n    }\n    buildDependencyGraph() {\n        this.blockedArrayFormulas = this.createEmptyPositionSet();\n        this.spreadingRelations = new SpreadingRelation();\n        this.formulaDependencies = lazy(() => {\n            const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position)\n                .filter((range) => !range.invalidSheetName && !range.invalidXc)\n                .map((range) => ({\n                data: position,\n                boundingBox: {\n                    zone: range.zone,\n                    sheetId: range.sheetId,\n                },\n            })));\n            return new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this), dependencies);\n        });\n    }\n    evaluateAllCells() {\n        const start = performance.now();\n        this.evaluatedCells = new PositionMap();\n        this.evaluate(this.getAllCells());\n        console.debug(\"evaluate all cells\", performance.now() - start, \"ms\");\n    }\n    evaluateFormulaResult(sheetId, formulaString) {\n        const compiledFormula = compile(formulaString);\n        const ranges = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));\n        this.updateCompilationParameters();\n        return this.evaluateCompiledFormula(sheetId, {\n            ...compiledFormula,\n            dependencies: ranges,\n        });\n    }\n    evaluateCompiledFormula(sheetId, compiledFormula, getContextualSymbolValue) {\n        try {\n            const result = updateEvalContextAndExecute(compiledFormula, this.compilationParams, sheetId, this.buildSafeGetSymbolValue(getContextualSymbolValue), this.compilationParams.evalContext.__originCellPosition);\n            if (isMatrix(result)) {\n                return matrixMap(result, nullValueToZeroValue);\n            }\n            return nullValueToZeroValue(result);\n        }\n        catch (error) {\n            return handleError(error, \"\");\n        }\n    }\n    getAllCells() {\n        const positions = this.createEmptyPositionSet();\n        positions.fillAllPositions();\n        return positions;\n    }\n    /**\n     * Return the position of formulas blocked by the given positions\n     * as well as all their dependencies.\n     */\n    getArrayFormulasBlockedBy(sheetId, zone) {\n        const arrayFormulaPositions = this.createEmptyPositionSet();\n        const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(sheetId, zone);\n        arrayFormulaPositions.addMany(arrayFormulas);\n        const spilledPositions = [...arrayFormulas].filter((position) => !this.blockedArrayFormulas.has(position));\n        if (spilledPositions.length) {\n            // ignore the formula spreading on the position. Keep only the blocked ones\n            arrayFormulaPositions.deleteMany(spilledPositions);\n        }\n        arrayFormulaPositions.addMany(this.getCellsDependingOn(arrayFormulaPositions));\n        return arrayFormulaPositions;\n    }\n    nextPositionsToUpdate = new PositionSet({});\n    cellsBeingComputed = new Set();\n    symbolsBeingComputed = new Set();\n    evaluate(positions) {\n        this.cellsBeingComputed = new Set();\n        this.nextPositionsToUpdate = positions;\n        let currentIteration = 0;\n        while (!this.nextPositionsToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {\n            this.updateCompilationParameters();\n            const positions = this.nextPositionsToUpdate.clear();\n            for (let i = 0; i < positions.length; ++i) {\n                this.evaluatedCells.delete(positions[i]);\n            }\n            for (let i = 0; i < positions.length; ++i) {\n                const position = positions[i];\n                if (this.nextPositionsToUpdate.has(position)) {\n                    continue;\n                }\n                const evaluatedCell = this.computeCell(position);\n                if (evaluatedCell !== EMPTY_CELL) {\n                    this.evaluatedCells.set(position, evaluatedCell);\n                }\n            }\n            onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));\n        }\n        if (currentIteration >= MAX_ITERATION) {\n            console.warn(\"Maximum iteration reached while evaluating cells\");\n        }\n    }\n    computeCell(position) {\n        const evaluation = this.evaluatedCells.get(position);\n        if (evaluation) {\n            return evaluation; // already computed\n        }\n        if (!this.blockedArrayFormulas.has(position)) {\n            this.invalidateSpreading(position);\n        }\n        if (this.spreadingRelations.isArrayFormula(position)) {\n            this.spreadingRelations.removeNode(position);\n        }\n        const cell = this.getters.getCell(position);\n        if (cell === undefined) {\n            return EMPTY_CELL;\n        }\n        const cellId = cell.id;\n        const localeFormat = { format: cell.format, locale: this.getters.getLocale() };\n        try {\n            if (this.cellsBeingComputed.has(cellId)) {\n                return ERROR_CYCLE_CELL;\n            }\n            this.cellsBeingComputed.add(cellId);\n            return cell.isFormula\n                ? this.computeFormulaCell(position, cell)\n                : evaluateLiteral(cell, localeFormat);\n        }\n        catch (e) {\n            e.value = e?.value || CellErrorType.GenericError;\n            e.message = e?.message || implementationErrorMessage;\n            return createEvaluatedCell(e);\n        }\n        finally {\n            this.cellsBeingComputed.delete(cellId);\n        }\n    }\n    computeAndSave(position) {\n        const evaluatedCell = this.computeCell(position);\n        if (!this.evaluatedCells.has(position)) {\n            this.evaluatedCells.set(position, evaluatedCell);\n        }\n        return evaluatedCell;\n    }\n    computeFormulaCell(formulaPosition, cellData) {\n        const formulaReturn = updateEvalContextAndExecute(cellData.compiledFormula, this.compilationParams, formulaPosition.sheetId, this.buildSafeGetSymbolValue(), formulaPosition);\n        if (!isMatrix(formulaReturn)) {\n            return createEvaluatedCell(nullValueToZeroValue(formulaReturn), this.getters.getLocale(), cellData);\n        }\n        this.assertSheetHasEnoughSpaceToSpreadFormulaResult(formulaPosition, formulaReturn);\n        const nbColumns = formulaReturn.length;\n        const nbRows = formulaReturn[0].length;\n        const resultZone = {\n            top: formulaPosition.row,\n            bottom: formulaPosition.row + nbRows - 1,\n            left: formulaPosition.col,\n            right: formulaPosition.col + nbColumns - 1,\n        };\n        this.spreadingRelations.addRelation({ resultZone, arrayFormulaPosition: formulaPosition });\n        this.assertNoMergedCellsInSpreadZone(formulaPosition, formulaReturn);\n        forEachSpreadPositionInMatrix(nbColumns, nbRows, this.checkCollision(formulaPosition));\n        forEachSpreadPositionInMatrix(nbColumns, nbRows, \n        // thanks to the isMatrix check above, we know that formulaReturn is MatrixFunctionReturn\n        this.spreadValues(formulaPosition, formulaReturn));\n        this.invalidatePositionsDependingOnSpread(formulaPosition.sheetId, resultZone);\n        return createEvaluatedCell(nullValueToZeroValue(formulaReturn[0][0]), this.getters.getLocale(), cellData);\n    }\n    invalidatePositionsDependingOnSpread(sheetId, resultZone) {\n        // the result matrix is split in 2 zones to exclude the array formula position\n        const invalidatedPositions = this.formulaDependencies().getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));\n        invalidatedPositions.delete({ sheetId, col: resultZone.left, row: resultZone.top });\n        this.nextPositionsToUpdate.addMany(invalidatedPositions);\n    }\n    assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {\n        const numberOfCols = this.getters.getNumberCols(sheetId);\n        const numberOfRows = this.getters.getNumberRows(sheetId);\n        const enoughCols = col + matrixResult.length <= numberOfCols;\n        const enoughRows = row + matrixResult[0].length <= numberOfRows;\n        if (enoughCols && enoughRows) {\n            return;\n        }\n        if (enoughCols) {\n            throw new SplillBlockedError(_t(\"Result couldn't be automatically expanded. Please insert more rows.\"));\n        }\n        if (enoughRows) {\n            throw new SplillBlockedError(_t(\"Result couldn't be automatically expanded. Please insert more columns.\"));\n        }\n        throw new SplillBlockedError(_t(\"Result couldn't be automatically expanded. Please insert more columns and rows.\"));\n    }\n    assertNoMergedCellsInSpreadZone({ sheetId, col, row }, matrixResult) {\n        const mergedCells = this.getters.getMergesInZone(sheetId, {\n            top: row,\n            bottom: row + matrixResult[0].length - 1,\n            left: col,\n            right: col + matrixResult.length - 1,\n        });\n        if (mergedCells.length === 0) {\n            return;\n        }\n        throw new SplillBlockedError(_t(\"Merged cells found in the spill zone. Please unmerge cells before using array formulas.\"));\n    }\n    checkCollision(formulaPosition) {\n        const { sheetId, col, row } = formulaPosition;\n        const checkCollision = (i, j) => {\n            const position = { sheetId: sheetId, col: i + col, row: j + row };\n            const rawCell = this.getters.getCell(position);\n            if (rawCell?.content ||\n                this.getters.getEvaluatedCell(position).type !== CellValueType.empty) {\n                this.blockedArrayFormulas.add(formulaPosition);\n                throw new SplillBlockedError(_t(\"Array result was not expanded because it would overwrite data in %s.\", toXC(position.col, position.row)));\n            }\n            this.blockedArrayFormulas.delete(formulaPosition);\n        };\n        return checkCollision;\n    }\n    spreadValues({ sheetId, col, row }, matrixResult) {\n        const spreadValues = (i, j) => {\n            const position = { sheetId, col: i + col, row: j + row };\n            const cell = this.getters.getCell(position);\n            const evaluatedCell = createEvaluatedCell(nullValueToZeroValue(matrixResult[i][j]), this.getters.getLocale(), cell);\n            this.evaluatedCells.set(position, evaluatedCell);\n        };\n        return spreadValues;\n    }\n    invalidateSpreading(position) {\n        const zone = this.spreadingRelations.getArrayResultZone(position);\n        if (!zone) {\n            return;\n        }\n        for (let col = zone.left; col <= zone.right; col++) {\n            for (let row = zone.top; row <= zone.bottom; row++) {\n                const resultPosition = { sheetId: position.sheetId, col, row };\n                const content = this.getters.getCell(resultPosition)?.content;\n                if (content) {\n                    // there's no point at re-evaluating overlapping array formulas,\n                    // there's still a collision\n                    continue;\n                }\n                this.evaluatedCells.delete(resultPosition);\n            }\n        }\n        const sheetId = position.sheetId;\n        this.invalidatePositionsDependingOnSpread(sheetId, zone);\n        this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));\n    }\n    /**\n     * Wraps a GetSymbolValue function to add cycle detection\n     * and error handling.\n     */\n    buildSafeGetSymbolValue(getContextualSymbolValue) {\n        const getSymbolValue = (symbolName) => {\n            if (this.symbolsBeingComputed.has(symbolName)) {\n                return ERROR_CYCLE_CELL;\n            }\n            this.symbolsBeingComputed.add(symbolName);\n            try {\n                const symbolValue = getContextualSymbolValue?.(symbolName);\n                if (symbolValue) {\n                    return symbolValue;\n                }\n                return new BadExpressionError(_t(\"Invalid formula\"));\n            }\n            finally {\n                this.symbolsBeingComputed.delete(symbolName);\n            }\n        };\n        return getSymbolValue;\n    }\n    // ----------------------------------------------------------\n    //                 COMMON FUNCTIONALITY\n    // ----------------------------------------------------------\n    getDirectDependencies(position) {\n        const cell = this.getters.getCell(position);\n        if (!cell?.isFormula) {\n            return [];\n        }\n        return cell.compiledFormula.dependencies;\n    }\n    getCellsDependingOn(positions) {\n        const ranges = [];\n        const zonesBySheetIds = aggregatePositionsToZones(positions);\n        for (const sheetId in zonesBySheetIds) {\n            ranges.push(...zonesBySheetIds[sheetId].map((zone) => ({ sheetId, zone })));\n        }\n        return this.formulaDependencies().getCellsDependingOn(ranges);\n    }\n}\nfunction forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {\n    for (let i = 0; i < nbColumns; ++i) {\n        for (let j = 0; j < nbRows; ++j) {\n            if (i === 0 && j === 0) {\n                continue;\n            }\n            callback(i, j);\n        }\n    }\n}\n/**\n * This function replaces null payload values with 0.\n * This aids in the UI by ensuring that a cell with a\n * formula referencing an empty cell displays a value (0),\n * rather than appearing empty. This indicates that the\n * cell is the result of a non-empty content.\n */\nfunction nullValueToZeroValue(functionResult) {\n    if (functionResult.value === null || functionResult.value === undefined) {\n        // 'functionResult.value === undefined' is supposed to never happen, it's a safety net for javascript use\n        return { ...functionResult, value: 0 };\n    }\n    return functionResult;\n}\nfunction updateEvalContextAndExecute(compiledFormula, compilationParams, sheetId, getSymbolValue, originCellPosition) {\n    compilationParams.evalContext.__originCellPosition = originCellPosition;\n    compilationParams.evalContext.__originSheetId = sheetId;\n    return compiledFormula.execute(compiledFormula.dependencies, compilationParams.referenceDenormalizer, compilationParams.ensureRange, getSymbolValue, compilationParams.evalContext);\n}\n\n//#region\n// ---------------------------------------------------------------------------\n// INTRODUCTION\n// ---------------------------------------------------------------------------\n// The evaluation plugin is in charge of computing the values of the cells.\n// This is a fairly complex task for several reasons:\n// Reason n\u00b01: Cells can contain formulas that must be interpreted to know\n// the final value of the cell. And these formulas can depend on other cells.\n// ex A1:\"=SUM(B1:B2)\" we have to evaluate B1:B2 first to be able to evaluate A1.\n// We say here that we have a 'formula dependency' between A1 and B1:B2.\n// Reason n\u00b02: A cell can assign value to other cells that haven't content.\n// This concerns cells containing a formula that returns an array of values.\n// ex A1:\"=SPLIT('Odoo','d')\" Evaluating A1 must assign the value \"O\" to A1 and\n// \"oo\" to B1. We say here that we have a 'spread relation' between A1 and B1.\n// B1 have a spread value from A1.\n// Note that a cell can contain a formula which depends on other cells which\n// themselves can:\n// - contain formulas which depends on other cells (and so on).\n// - contain a spread value from other formulas which depends on other cells\n//   (and so on).\n// I - How to build the evaluation ?\n//    If we had only formulas dependencies to treat, the evaluation would be\n//    simple: the formulas dependencies are directly deduced from the content\n//    of the formulas. With the dependencies we are able to calculate which\n//    formula must be evaluated before another.\n//    Cycles\n//    ------\n//    We can also easily detect if the cells are included in reference cycles\n//    and return an error in this case. ex: A1:\"=B1\" B1:\"=C1\" C1:\"=A1\"\n//    The \"#CYCLE\" error must be returned for\n//    all three cells.\n//    But there's more! There are formulas referring to other cells but never\n//    use them. This is the case for example\n//    with the \"IF\" formula. ex:\n//    A1:\"=IF(D1,A2,B1)\"\n//    A2:\"=A1\"\n//    In this case it is obvious that we have a cyclic dependency. But in practice\n//    this will only exist if D1 is true.\n//    For this reason, we believe that the evaluation should be **partly recursive**:\n//    The function computing a formula cell starts by marking them as 'being evaluated'\n//    and then call itself on the dependencies of the concerned cell. This allows\n//    to evaluate the dependencies before the cell itself and to detect\n//    if the cell that is being evaluated isn't part of a cycle.\n// II - The spread relation anticipation problem\n//    The biggest difficulty to solve with the evaluation lies in the fact that\n//    we cannot anticipate the spread relations: cells impacted by the result array\n//    of a formula are only determined after the array formula has been\n//    evaluated. In the case where the impacted cells are used in other formulas,\n//    this will require to re-evaluation other formulas (and so on...). ex:\n//    A1:\"=B2\"\n//    A2:\"=SPLIT('Odoo','d')\"\n//    in the example above, A2 spreads on B2, but we will know it only after\n//    the evaluation of A2. To be able to evaluate A1 correctly, we must therefore\n//    reevaluate A1 after the evaluation of A2.\n//    We could evaluate which formula spreads first. Except that the array formulas\n//    can themselves depend on the spreads of other formulas. ex:\n//    A1:\"=SPLIT(B3,'d')\"\n//    A2:=\"odoo odoo\"\n//    A3:\"=SPLIT(A2,' ')\"\n//    In the example above, A3 must be evaluated before A1 because A1 needs B3 which\n//    can be modified by A3.\n//    Therefore, there would be a spatial evaluation order to be respected between\n//    the array formulas. We could imagine that, when an array formula depends\n//    on a cell, then we evaluate the first formula that spreads located in the upper\n//    left corner of this cell.\n//    Although this possibility has been explored, it remains complicated to spatially\n//    predict which formula should be evaluated before another, especially when\n//    the array formulas are located in different sheets or when the array formulas\n//    depends on the spreads of each other. ex:\n//\n//    A1:\"=ARRAY_FORMULA_ALPHA(B2)\"\n//    A2:\"=ARRAY_FORMULA_BETA(B1)\"\n//    In the example above, ARRAY_FORMULA_ALPHA and ARRAY_FORMULA_BETA are some\n//    formulas that could spread depending on the value of B2 and B1. This could be a\n//    cyclic dependency that we cannot anticipate.\n//    And as with the \"IF\" formula, array formulas may not use their dependency.\n//    It then becomes very difficult to manage...\n//    Also, if we have a cycle, that doesn't mean it's bad. The cycle can converge to\n//    a stable state at the scale of the sheets. Functionally, we don't want to forbid\n//    convergent cycles. It is an interesting feature but which requires to re-evaluate\n//    the cycle as many times as convergence is not reached.\n// Thus, in order to respect the relations between the cells (formula dependencies and\n// spread relations), the evaluation of the cells must:\n// - respect a precise order (cells values used by another must be evaluated first) : As\n//   we cannot anticipate which dependencies are really used by the formulas, we must\n//   evaluate the cells in a recursive way;\n// - be done as many times as necessary to ensure that all the cells have been correctly\n//   evaluated in the correct order (in case of, for example, spreading relation cycles).\n// The chosen solution is to reevaluate the formulas impacted by spreads as many times\n// as necessary in several iterations, where evaluated cells can trigger the evaluation\n// of other cells depending on it, at the next iteration.\n//#endregion\nclass EvaluationPlugin extends UIPlugin {\n    static getters = [\n        \"evaluateFormula\",\n        \"evaluateFormulaResult\",\n        \"evaluateCompiledFormula\",\n        \"getCorrespondingFormulaCell\",\n        \"getRangeFormattedValues\",\n        \"getRangeValues\",\n        \"getRangeFormats\",\n        \"getEvaluatedCell\",\n        \"getEvaluatedCells\",\n        \"getEvaluatedCellsInZone\",\n        \"getEvaluatedCellsPositions\",\n        \"getSpreadZone\",\n        \"getArrayFormulaSpreadingOn\",\n        \"isEmpty\",\n    ];\n    shouldRebuildDependenciesGraph = true;\n    evaluator;\n    positionsToUpdate = [];\n    constructor(config) {\n        super(config);\n        this.evaluator = new Evaluator(config.custom, this.getters);\n    }\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    beforeHandle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            invalidateDependenciesCommands.has(cmd.type)) {\n            this.shouldRebuildDependenciesGraph = true;\n        }\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"UPDATE_CELL\":\n                if (!(\"content\" in cmd || \"format\" in cmd) || this.shouldRebuildDependenciesGraph) {\n                    return;\n                }\n                const position = { sheetId: cmd.sheetId, row: cmd.row, col: cmd.col };\n                this.positionsToUpdate.push(position);\n                if (\"content\" in cmd) {\n                    this.evaluator.updateDependencies(position);\n                }\n                break;\n            case \"EVALUATE_CELLS\":\n                this.evaluator.evaluateAllCells();\n                break;\n        }\n    }\n    finalize() {\n        if (this.shouldRebuildDependenciesGraph) {\n            this.evaluator.buildDependencyGraph();\n            this.evaluator.evaluateAllCells();\n            this.shouldRebuildDependenciesGraph = false;\n        }\n        else if (this.positionsToUpdate.length) {\n            this.evaluator.evaluateCells(this.positionsToUpdate);\n        }\n        this.positionsToUpdate = [];\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    evaluateFormula(sheetId, formulaString) {\n        const result = this.evaluateFormulaResult(sheetId, formulaString);\n        if (isMatrix(result)) {\n            return matrixMap(result, (cell) => cell.value);\n        }\n        return result.value;\n    }\n    evaluateFormulaResult(sheetId, formulaString) {\n        return this.evaluator.evaluateFormulaResult(sheetId, formulaString);\n    }\n    evaluateCompiledFormula(sheetId, compiledFormula, getSymbolValue) {\n        return this.evaluator.evaluateCompiledFormula(sheetId, compiledFormula, getSymbolValue);\n    }\n    /**\n     * Return the value of each cell in the range as they are displayed in the grid.\n     */\n    getRangeFormattedValues(range) {\n        const sheet = this.getters.tryGetSheet(range.sheetId);\n        if (sheet === undefined)\n            return [];\n        return this.mapVisiblePositions(range, (p) => this.getters.getEvaluatedCell(p).formattedValue);\n    }\n    /**\n     * Return the value of each cell in the range.\n     */\n    getRangeValues(range) {\n        const sheet = this.getters.tryGetSheet(range.sheetId);\n        if (sheet === undefined)\n            return [];\n        return this.mapVisiblePositions(range, (p) => this.getters.getEvaluatedCell(p).value);\n    }\n    /**\n     * Return the format of each cell in the range.\n     */\n    getRangeFormats(range) {\n        const sheet = this.getters.tryGetSheet(range.sheetId);\n        if (sheet === undefined)\n            return [];\n        return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.format);\n    }\n    getEvaluatedCell(position) {\n        return this.evaluator.getEvaluatedCell(position);\n    }\n    getEvaluatedCells(sheetId) {\n        return this.evaluator\n            .getEvaluatedPositionsInSheet(sheetId)\n            .map((position) => this.getEvaluatedCell(position));\n    }\n    getEvaluatedCellsPositions(sheetId) {\n        return this.evaluator.getEvaluatedPositionsInSheet(sheetId);\n    }\n    getEvaluatedCellsInZone(sheetId, zone) {\n        return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));\n    }\n    /**\n     * Return the spread zone the position is part of, if any\n     */\n    getSpreadZone(position, options = { ignoreSpillError: false }) {\n        return this.evaluator.getSpreadZone(position, options);\n    }\n    getArrayFormulaSpreadingOn(position) {\n        return this.evaluator.getArrayFormulaSpreadingOn(position);\n    }\n    /**\n     * Check if a zone only contains empty cells\n     */\n    isEmpty(sheetId, zone) {\n        return positions(zone)\n            .map(({ col, row }) => this.getEvaluatedCell({ sheetId, col, row }))\n            .every((cell) => cell.type === CellValueType.empty);\n    }\n    /**\n     * Maps the visible positions of a range  according to a provided callback\n     * @param range - the range we filter out\n     * @param evaluationCallback - the callback applied to the filtered positions\n     * @returns the values filtered (ie we keep only the not hidden values)\n     */\n    mapVisiblePositions(range, evaluationCallback) {\n        const { sheetId, zone } = range;\n        const xcPositions = positions(zone);\n        return xcPositions.reduce((acc, position) => {\n            const { col, row } = position;\n            if (!this.getters.isColHidden(sheetId, col) && !this.getters.isRowHidden(sheetId, row)) {\n                acc.push(evaluationCallback({ sheetId, ...position }));\n            }\n            return acc;\n        }, []);\n    }\n    // ---------------------------------------------------------------------------\n    // Export\n    // ---------------------------------------------------------------------------\n    exportForExcel(data) {\n        for (const position of this.evaluator.getEvaluatedPositions()) {\n            const evaluatedCell = this.evaluator.getEvaluatedCell(position);\n            const xc = toXC(position.col, position.row);\n            const value = evaluatedCell.value;\n            let isFormula = false;\n            let newContent = undefined;\n            let newFormat = undefined;\n            let isExported = true;\n            const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);\n            const formulaCell = this.getCorrespondingFormulaCell(position);\n            if (formulaCell) {\n                isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);\n                isFormula = isExported;\n                if (!isExported) {\n                    // If the cell contains a non-exported formula and that is evaluates to\n                    // nothing* ,we don't export it.\n                    // * non-falsy value are relevant and so are 0 and FALSE, which only leaves\n                    // the empty string.\n                    if (value !== \"\") {\n                        newContent = (value ?? \"\").toString();\n                        newFormat = evaluatedCell.format;\n                    }\n                }\n            }\n            const exportedCellData = exportedSheetData.cells[xc] || {};\n            const format = newFormat\n                ? getItemId(newFormat, data.formats)\n                : exportedCellData.format;\n            let content;\n            if (isExported && isFormula && formulaCell instanceof FormulaCellWithDependencies) {\n                content = formulaCell.contentWithFixedReferences;\n            }\n            else {\n                content = !isExported ? newContent : exportedCellData.content;\n            }\n            exportedSheetData.cells[xc] = { ...exportedCellData, value, isFormula, content, format };\n        }\n    }\n    /**\n     * Returns the corresponding formula cell of a given cell\n     * It could be the formula present in the cell itself or the\n     * formula of the array formula that spreads to the cell\n     */\n    getCorrespondingFormulaCell(position) {\n        const cell = this.getters.getCell(position);\n        if (cell && cell.isFormula) {\n            return cell.compiledFormula.isBadExpression ? undefined : cell;\n        }\n        else if (cell && cell.content) {\n            return undefined;\n        }\n        const spreadingFormulaPosition = this.getArrayFormulaSpreadingOn(position);\n        if (spreadingFormulaPosition === undefined) {\n            return undefined;\n        }\n        const spreadingFormulaCell = this.getters.getCell(spreadingFormulaPosition);\n        if (spreadingFormulaCell?.isFormula) {\n            return spreadingFormulaCell;\n        }\n        return undefined;\n    }\n}\n\nconst chartColorRegex = /\"(#[0-9a-fA-F]{6})\"/g;\n/**\n * https://tomekdev.com/posts/sorting-colors-in-js\n */\nfunction sortWithClusters(colorsToSort) {\n    const clusters = [\n        { leadColor: rgba(255, 0, 0), colors: [] }, // red\n        { leadColor: rgba(255, 128, 0), colors: [] }, // orange\n        { leadColor: rgba(128, 128, 0), colors: [] }, // yellow\n        { leadColor: rgba(128, 255, 0), colors: [] }, // chartreuse\n        { leadColor: rgba(0, 255, 0), colors: [] }, // green\n        { leadColor: rgba(0, 255, 128), colors: [] }, // spring green\n        { leadColor: rgba(0, 255, 255), colors: [] }, // cyan\n        { leadColor: rgba(0, 127, 255), colors: [] }, // azure\n        { leadColor: rgba(0, 0, 255), colors: [] }, // blue\n        { leadColor: rgba(127, 0, 255), colors: [] }, // violet\n        { leadColor: rgba(128, 0, 128), colors: [] }, // magenta\n        { leadColor: rgba(255, 0, 128), colors: [] }, // rose\n    ];\n    for (const color of colorsToSort.map(colorToRGBA)) {\n        let currentDistance = 500; //max distance is 441;\n        let currentIndex = 0;\n        clusters.forEach((cluster, clusterIndex) => {\n            const distance = colorDistance(color, cluster.leadColor);\n            if (currentDistance > distance) {\n                currentDistance = distance;\n                currentIndex = clusterIndex;\n            }\n        });\n        clusters[currentIndex].colors.push(color);\n    }\n    return clusters\n        .map((cluster) => cluster.colors.sort((a, b) => rgbaToHSLA(a).s - rgbaToHSLA(b).s))\n        .flat()\n        .map(rgbaToHex);\n}\nfunction colorDistance(color1, color2) {\n    return Math.sqrt(Math.pow(color1.r - color2.r, 2) +\n        Math.pow(color1.g - color2.g, 2) +\n        Math.pow(color1.b - color2.b, 2));\n}\n/**\n * CustomColors plugin\n * This plugins aims to compute and keep to custom colors used in the\n * current spreadsheet\n */\nclass CustomColorsPlugin extends UIPlugin {\n    customColors = {};\n    shouldUpdateColors = true;\n    static getters = [\"getCustomColors\"];\n    constructor(config) {\n        super(config);\n        this.tryToAddColors(config.customColors ?? []);\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"START\":\n                for (const sheetId of this.getters.getSheetIds()) {\n                    for (const chartId of this.getters.getChartIds(sheetId)) {\n                        this.tryToAddColors(this.getChartColors(chartId));\n                    }\n                }\n                break;\n            case \"UPDATE_CHART\":\n            case \"CREATE_CHART\":\n                this.tryToAddColors(this.getChartColors(cmd.id));\n                break;\n            case \"UPDATE_CELL\":\n            case \"ADD_CONDITIONAL_FORMAT\":\n            case \"SET_BORDER\":\n            case \"SET_ZONE_BORDERS\":\n            case \"SET_FORMATTING\":\n            case \"CREATE_TABLE\":\n            case \"UPDATE_TABLE\":\n                this.history.update(\"shouldUpdateColors\", true);\n                break;\n        }\n    }\n    finalize() {\n        if (this.shouldUpdateColors) {\n            this.history.update(\"shouldUpdateColors\", false);\n            this.tryToAddColors(this.computeCustomColors());\n        }\n    }\n    getCustomColors() {\n        return sortWithClusters(Object.keys(this.customColors));\n    }\n    computeCustomColors() {\n        let usedColors = [];\n        for (const sheetId of this.getters.getSheetIds()) {\n            usedColors = usedColors.concat(this.getColorsFromCells(sheetId), this.getFormattingColors(sheetId), this.getTableColors(sheetId));\n        }\n        return [...new Set([...usedColors])];\n    }\n    getColorsFromCells(sheetId) {\n        const cells = Object.values(this.getters.getCells(sheetId));\n        const colors = new Set();\n        for (const cell of cells) {\n            if (cell.style?.textColor) {\n                colors.add(cell.style.textColor);\n            }\n            if (cell.style?.fillColor) {\n                colors.add(cell.style.fillColor);\n            }\n        }\n        for (const color of this.getters.getBordersColors(sheetId)) {\n            colors.add(color);\n        }\n        return [...colors];\n    }\n    getFormattingColors(sheetId) {\n        const formats = this.getters.getConditionalFormats(sheetId);\n        const formatColors = [];\n        for (const format of formats) {\n            const rule = format.rule;\n            if (rule.type === \"CellIsRule\") {\n                formatColors.push(rule.style.textColor);\n                formatColors.push(rule.style.fillColor);\n            }\n            else if (rule.type === \"ColorScaleRule\") {\n                formatColors.push(colorNumberString(rule.minimum.color));\n                formatColors.push(rule.midpoint ? colorNumberString(rule.midpoint.color) : undefined);\n                formatColors.push(colorNumberString(rule.maximum.color));\n            }\n        }\n        return formatColors.filter(isDefined);\n    }\n    getChartColors(chartId) {\n        const chart = this.getters.getChart(chartId);\n        if (chart === undefined) {\n            return [];\n        }\n        const stringifiedChart = JSON.stringify(chart.getDefinition());\n        const colors = stringifiedChart.matchAll(chartColorRegex);\n        return [...colors].map((color) => color[1]); // color[1] is the first capturing group of the regex\n    }\n    getTableColors(sheetId) {\n        const tables = this.getters.getTables(sheetId);\n        return tables.flatMap((table) => {\n            const config = table.config;\n            const style = this.getters.getTableStyle(config.styleId);\n            return [\n                this.getTableStyleElementColors(style.wholeTable),\n                config.numberOfHeaders > 0 ? this.getTableStyleElementColors(style.headerRow) : [],\n                config.totalRow ? this.getTableStyleElementColors(style.totalRow) : [],\n                config.bandedColumns ? this.getTableStyleElementColors(style.firstColumnStripe) : [],\n                config.bandedColumns ? this.getTableStyleElementColors(style.secondColumnStripe) : [],\n                config.bandedRows ? this.getTableStyleElementColors(style.firstRowStripe) : [],\n                config.bandedRows ? this.getTableStyleElementColors(style.secondRowStripe) : [],\n                config.firstColumn ? this.getTableStyleElementColors(style.firstColumn) : [],\n                config.lastColumn ? this.getTableStyleElementColors(style.lastColumn) : [],\n            ].flat();\n        });\n    }\n    getTableStyleElementColors(element) {\n        if (!element) {\n            return [];\n        }\n        return [\n            element.style?.fillColor,\n            element.style?.textColor,\n            element.border?.bottom?.color,\n            element.border?.top?.color,\n            element.border?.left?.color,\n            element.border?.right?.color,\n            element.border?.horizontal?.color,\n            element.border?.vertical?.color,\n        ].filter(isDefined);\n    }\n    tryToAddColors(colors) {\n        for (const color of colors) {\n            if (!isColorValid(color)) {\n                continue;\n            }\n            const formattedColor = toHex(color);\n            if (color && !COLOR_PICKER_DEFAULTS.includes(formattedColor)) {\n                this.history.update(\"customColors\", formattedColor, true);\n            }\n        }\n    }\n}\n\nclass EvaluationChartPlugin extends UIPlugin {\n    static getters = [\"getChartRuntime\", \"getStyleOfSingleCellChart\"];\n    charts = {};\n    createRuntimeChart = chartRuntimeFactory(this.getters);\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            invalidateCFEvaluationCommands.has(cmd.type) ||\n            invalidateChartEvaluationCommands.has(cmd.type)) {\n            for (const chartId in this.charts) {\n                this.charts[chartId] = undefined;\n            }\n        }\n        switch (cmd.type) {\n            case \"UPDATE_CHART\":\n            case \"CREATE_CHART\":\n            case \"DELETE_FIGURE\":\n                this.charts[cmd.id] = undefined;\n                break;\n            case \"DELETE_SHEET\":\n                for (let chartId in this.charts) {\n                    if (!this.getters.isChartDefined(chartId)) {\n                        this.charts[chartId] = undefined;\n                    }\n                }\n                break;\n        }\n    }\n    getChartRuntime(figureId) {\n        if (!this.charts[figureId]) {\n            const chart = this.getters.getChart(figureId);\n            if (!chart) {\n                throw new Error(`No chart for the given id: ${figureId}`);\n            }\n            this.charts[figureId] = this.createRuntimeChart(chart);\n        }\n        return this.charts[figureId];\n    }\n    /**\n     * Get the background and textColor of a chart based on the color of the first cell of the main range of the chart.\n     */\n    getStyleOfSingleCellChart(chartBackground, mainRange) {\n        if (chartBackground)\n            return { background: chartBackground, fontColor: chartFontColor(chartBackground) };\n        if (!mainRange) {\n            return {\n                background: BACKGROUND_CHART_COLOR,\n                fontColor: chartFontColor(BACKGROUND_CHART_COLOR),\n            };\n        }\n        const col = mainRange.zone.left;\n        const row = mainRange.zone.top;\n        const sheetId = mainRange.sheetId;\n        const style = this.getters.getCellComputedStyle({ sheetId, col, row });\n        const background = style.fillColor || BACKGROUND_CHART_COLOR;\n        return {\n            background,\n            fontColor: style.textColor || chartFontColor(background),\n        };\n    }\n    exportForExcel(data) {\n        for (const sheet of data.sheets) {\n            if (!sheet.images) {\n                sheet.images = [];\n            }\n            const sheetFigures = this.getters.getFigures(sheet.id);\n            const figures = [];\n            for (const figure of sheetFigures) {\n                if (!figure || figure.tag !== \"chart\") {\n                    continue;\n                }\n                const figureId = figure.id;\n                const figureData = this.getters.getChart(figureId)?.getDefinitionForExcel();\n                if (figureData) {\n                    figures.push({\n                        ...figure,\n                        data: figureData,\n                    });\n                }\n                else {\n                    const chart = this.getters.getChart(figureId);\n                    if (!chart) {\n                        continue;\n                    }\n                    const type = this.getters.getChartType(figureId);\n                    const runtime = this.getters.getChartRuntime(figureId);\n                    const img = chartToImage(runtime, figure, type);\n                    if (img) {\n                        sheet.images.push({\n                            ...figure,\n                            tag: \"image\",\n                            data: {\n                                mimetype: \"image/png\",\n                                path: img,\n                                size: { width: figure.width, height: figure.height },\n                            },\n                        });\n                    }\n                }\n            }\n            sheet.charts = figures;\n        }\n    }\n}\n\nclass EvaluationConditionalFormatPlugin extends UIPlugin {\n    static getters = [\n        \"getConditionalIcon\",\n        \"getCellConditionalFormatStyle\",\n        \"getConditionalDataBar\",\n    ];\n    isStale = true;\n    // stores the computed styles in the format of computedStyles.sheetName[col][row] = Style\n    computedStyles = {};\n    computedIcons = {};\n    computedDataBars = {};\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            invalidateCFEvaluationCommands.has(cmd.type) ||\n            (cmd.type === \"UPDATE_CELL\" && (\"content\" in cmd || \"format\" in cmd))) {\n            this.isStale = true;\n        }\n    }\n    finalize() {\n        if (this.isStale) {\n            for (const sheetId of this.getters.getSheetIds()) {\n                this.computedStyles[sheetId] = lazy(() => this.getComputedStyles(sheetId));\n                this.computedIcons[sheetId] = lazy(() => this.getComputedIcons(sheetId));\n                this.computedDataBars[sheetId] = lazy(() => this.getComputedDataBars(sheetId));\n            }\n            this.isStale = false;\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getCellConditionalFormatStyle(position) {\n        const { sheetId, col, row } = position;\n        const styles = this.computedStyles[sheetId]();\n        return styles && styles[col]?.[row];\n    }\n    getConditionalIcon({ sheetId, col, row }) {\n        const icons = this.computedIcons[sheetId]();\n        return icons && icons[col]?.[row];\n    }\n    getConditionalDataBar({ sheetId, col, row }) {\n        const dataBars = this.computedDataBars[sheetId]();\n        return dataBars && dataBars[col]?.[row];\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    /**\n     * Compute the styles according to the conditional formatting.\n     * This computation must happen after the cell values are computed if they change\n     *\n     * This result of the computation will be in the state.cell[XC].conditionalStyle and will be the union of all the style\n     * properties of the rules applied (in order).\n     * So if a cell has multiple conditional formatting applied to it, and each affect a different value of the style,\n     * the resulting style will have the combination of all those values.\n     * If multiple conditional formatting use the same style value, they will be applied in order so that the last applied wins\n     */\n    getComputedStyles(sheetId) {\n        const computedStyle = {};\n        for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {\n            switch (cf.rule.type) {\n                case \"ColorScaleRule\":\n                    for (let range of cf.ranges) {\n                        this.applyColorScale(sheetId, range, cf.rule, computedStyle);\n                    }\n                    break;\n                case \"CellIsRule\":\n                    const formulas = cf.rule.values.map((value) => value.startsWith(\"=\") ? compile(value) : undefined);\n                    for (let ref of cf.ranges) {\n                        const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;\n                        for (let row = zone.top; row <= zone.bottom; row++) {\n                            for (let col = zone.left; col <= zone.right; col++) {\n                                const predicate = this.rulePredicate[cf.rule.type];\n                                const target = { sheetId, col, row };\n                                const values = cf.rule.values.map((value, i) => {\n                                    const compiledFormula = formulas[i];\n                                    if (compiledFormula) {\n                                        return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, compiledFormula.tokens);\n                                    }\n                                    return value;\n                                });\n                                if (predicate && predicate(target, { ...cf.rule, values })) {\n                                    if (!computedStyle[col])\n                                        computedStyle[col] = [];\n                                    // we must combine all the properties of all the CF rules applied to the given cell\n                                    computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);\n                                }\n                            }\n                        }\n                    }\n                    break;\n            }\n        }\n        return computedStyle;\n    }\n    getComputedIcons(sheetId) {\n        const computedIcons = {};\n        for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {\n            if (cf.rule.type !== \"IconSetRule\")\n                continue;\n            for (let range of cf.ranges) {\n                this.applyIcon(sheetId, range, cf.rule, computedIcons);\n            }\n        }\n        return computedIcons;\n    }\n    getComputedDataBars(sheetId) {\n        const computedDataBars = {};\n        for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {\n            if (cf.rule.type !== \"DataBarRule\")\n                continue;\n            for (let range of cf.ranges) {\n                this.applyDataBar(sheetId, range, cf.rule, computedDataBars);\n            }\n        }\n        return computedDataBars;\n    }\n    parsePoint(sheetId, range, threshold, functionName) {\n        const rangeValues = this.getters\n            .getEvaluatedCellsInZone(sheetId, this.getters.getRangeFromSheetXC(sheetId, range).zone)\n            .filter((cell) => cell.type === CellValueType.number)\n            .map((cell) => cell.value);\n        switch (threshold.type) {\n            case \"value\":\n                const result = functionName === \"max\" ? largeMax(rangeValues) : largeMin(rangeValues);\n                return result;\n            case \"number\":\n                return Number(threshold.value);\n            case \"percentage\":\n                const min = largeMin(rangeValues);\n                const max = largeMax(rangeValues);\n                const delta = max - min;\n                return min + (delta * Number(threshold.value)) / 100;\n            case \"percentile\":\n                return percentile(rangeValues, Number(threshold.value) / 100, true);\n            case \"formula\":\n                const value = threshold.value && this.getters.evaluateFormula(sheetId, threshold.value);\n                return typeof value === \"number\" ? value : null;\n            default:\n                return null;\n        }\n    }\n    /** Compute the CF icons for the given range and CF rule, and apply in in the given computedIcons object */\n    applyIcon(sheetId, range, rule, computedIcons) {\n        const lowerInflectionPoint = this.parsePoint(sheetId, range, rule.lowerInflectionPoint);\n        const upperInflectionPoint = this.parsePoint(sheetId, range, rule.upperInflectionPoint);\n        if (lowerInflectionPoint === null ||\n            upperInflectionPoint === null ||\n            lowerInflectionPoint > upperInflectionPoint) {\n            return;\n        }\n        const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;\n        const iconSet = [rule.icons.upper, rule.icons.middle, rule.icons.lower];\n        for (let row = zone.top; row <= zone.bottom; row++) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                const cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n                if (cell.type !== CellValueType.number) {\n                    continue;\n                }\n                const icon = this.computeIcon(cell.value, upperInflectionPoint, rule.upperInflectionPoint.operator, lowerInflectionPoint, rule.lowerInflectionPoint.operator, iconSet);\n                if (!computedIcons[col]) {\n                    computedIcons[col] = [];\n                }\n                computedIcons[col][row] = icon;\n            }\n        }\n    }\n    computeIcon(value, upperInflectionPoint, upperOperator, lowerInflectionPoint, lowerOperator, icons) {\n        if ((upperOperator === \"ge\" && value >= upperInflectionPoint) ||\n            (upperOperator === \"gt\" && value > upperInflectionPoint)) {\n            return icons[0];\n        }\n        else if ((lowerOperator === \"ge\" && value >= lowerInflectionPoint) ||\n            (lowerOperator === \"gt\" && value > lowerInflectionPoint)) {\n            return icons[1];\n        }\n        return icons[2];\n    }\n    applyDataBar(sheetId, range, rule, computedDataBars) {\n        const rangeValues = this.getters.getRangeFromSheetXC(sheetId, rule.rangeValues || range);\n        const allValues = this.getters\n            .getEvaluatedCellsInZone(sheetId, rangeValues.zone)\n            .filter((cell) => cell.type === CellValueType.number)\n            .map((cell) => cell.value);\n        const max = largeMax(allValues);\n        if (max <= 0) {\n            // no need to apply the data bar if all values are negative or 0\n            return;\n        }\n        const color = rule.color;\n        const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;\n        const zoneOfValues = rangeValues.zone;\n        for (let row = zone.top; row <= zone.bottom; row++) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                const targetCol = col - zone.left + zoneOfValues.left;\n                const targetRow = row - zone.top + zoneOfValues.top;\n                const cell = this.getters.getEvaluatedCell({ sheetId, col: targetCol, row: targetRow });\n                if (!isInside(targetCol, targetRow, zoneOfValues) ||\n                    cell.type !== CellValueType.number ||\n                    cell.value <= 0) {\n                    // values negatives or 0 are ignored\n                    continue;\n                }\n                if (!computedDataBars[col])\n                    computedDataBars[col] = [];\n                computedDataBars[col][row] = {\n                    color: colorNumberString(color),\n                    percentage: (cell.value * 100) / max,\n                };\n            }\n        }\n    }\n    /** Compute the color scale for the given range and CF rule, and apply in in the given computedStyle object */\n    applyColorScale(sheetId, range, rule, computedStyle) {\n        const minValue = this.parsePoint(sheetId, range, rule.minimum, \"min\");\n        const midValue = rule.midpoint\n            ? this.parsePoint(sheetId, range, rule.midpoint)\n            : null;\n        const maxValue = this.parsePoint(sheetId, range, rule.maximum, \"max\");\n        if (minValue === null ||\n            maxValue === null ||\n            minValue >= maxValue ||\n            (midValue && (minValue >= midValue || midValue >= maxValue))) {\n            return;\n        }\n        const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;\n        const colorCellArgs = [];\n        if (rule.midpoint && midValue) {\n            colorCellArgs.push({\n                minValue,\n                minColor: rule.minimum.color,\n                colorDiffUnit: this.computeColorDiffUnits(minValue, midValue, rule.minimum.color, rule.midpoint.color),\n            });\n            colorCellArgs.push({\n                minValue: midValue,\n                minColor: rule.midpoint.color,\n                colorDiffUnit: this.computeColorDiffUnits(midValue, maxValue, rule.midpoint.color, rule.maximum.color),\n            });\n        }\n        else {\n            colorCellArgs.push({\n                minValue,\n                minColor: rule.minimum.color,\n                colorDiffUnit: this.computeColorDiffUnits(minValue, maxValue, rule.minimum.color, rule.maximum.color),\n            });\n        }\n        for (let row = zone.top; row <= zone.bottom; row++) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                const cell = this.getters.getEvaluatedCell({ sheetId, col, row });\n                if (cell.type === CellValueType.number) {\n                    const value = clip(cell.value, minValue, maxValue);\n                    let color;\n                    if (colorCellArgs.length === 2 && midValue) {\n                        color =\n                            value <= midValue\n                                ? this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit)\n                                : this.colorCell(value, colorCellArgs[1].minValue, colorCellArgs[1].minColor, colorCellArgs[1].colorDiffUnit);\n                    }\n                    else {\n                        color = this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit);\n                    }\n                    if (!computedStyle[col])\n                        computedStyle[col] = [];\n                    computedStyle[col][row] = computedStyle[col]?.[row] || {};\n                    computedStyle[col][row].fillColor = colorNumberString(color);\n                }\n            }\n        }\n    }\n    computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {\n        const deltaValue = maxValue - minValue;\n        const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);\n        const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);\n        const deltaColorB = (minColor % 256) - (maxColor % 256);\n        const colorDiffUnitR = deltaColorR / deltaValue;\n        const colorDiffUnitG = deltaColorG / deltaValue;\n        const colorDiffUnitB = deltaColorB / deltaValue;\n        return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];\n    }\n    colorCell(value, minValue, minColor, colorDiffUnit) {\n        const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;\n        const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));\n        const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));\n        const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));\n        return (r << 16) | (g << 8) | b;\n    }\n    /**\n     * Execute the predicate to know if a conditional formatting rule should be applied to a cell\n     */\n    rulePredicate = {\n        CellIsRule: (target, rule) => {\n            const cell = this.getters.getEvaluatedCell(target);\n            if (cell.type === CellValueType.error) {\n                return false;\n            }\n            let [value0, value1] = rule.values.map((val) => {\n                if (val.startsWith(\"=\")) {\n                    return this.getters.evaluateFormula(target.sheetId, val) ?? \"\";\n                }\n                return parseLiteral(val, DEFAULT_LOCALE);\n            });\n            if (isMatrix(value0) || isMatrix(value1)) {\n                return false;\n            }\n            const cellValue = cell.value ?? \"\";\n            value0 = value0 ?? \"\";\n            value1 = value1 ?? \"\";\n            switch (rule.operator) {\n                case \"IsEmpty\":\n                    return cellValue.toString().trim() === \"\";\n                case \"IsNotEmpty\":\n                    return cellValue.toString().trim() !== \"\";\n                case \"BeginsWith\":\n                    if (value0 === \"\") {\n                        return false;\n                    }\n                    return cellValue.toString().startsWith(value0.toString());\n                case \"EndsWith\":\n                    if (value0 === \"\") {\n                        return false;\n                    }\n                    return cellValue.toString().endsWith(value0.toString());\n                case \"Between\":\n                    return cellValue >= value0 && cellValue <= value1;\n                case \"NotBetween\":\n                    return !(cellValue >= value0 && cellValue <= value1);\n                case \"ContainsText\":\n                    return cellValue.toString().indexOf(value0.toString()) > -1;\n                case \"NotContains\":\n                    return !cellValue || cellValue.toString().indexOf(value0.toString()) === -1;\n                case \"GreaterThan\":\n                    return cellValue > value0;\n                case \"GreaterThanOrEqual\":\n                    return cellValue >= value0;\n                case \"LessThan\":\n                    return cellValue < value0;\n                case \"LessThanOrEqual\":\n                    return cellValue <= value0;\n                case \"NotEqual\":\n                    if (value0 === \"\") {\n                        return false;\n                    }\n                    return cellValue !== value0;\n                case \"Equal\":\n                    if (value0 === \"\") {\n                        return true;\n                    }\n                    return cellValue === value0;\n                default:\n                    console.warn(_t(\"Not implemented operator %s for kind of conditional formatting:  %s\", rule.operator, rule.type));\n            }\n            return false;\n        },\n    };\n}\n\nconst VALID_RESULT = { isValid: true };\nclass EvaluationDataValidationPlugin extends UIPlugin {\n    static getters = [\n        \"getDataValidationInvalidCriterionValueMessage\",\n        \"getInvalidDataValidationMessage\",\n        \"getValidationResultForCellValue\",\n        \"isCellValidCheckbox\",\n        \"isDataValidationInvalid\",\n    ];\n    validationResults = {};\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            cmd.type === \"EVALUATE_CELLS\" ||\n            (cmd.type === \"UPDATE_CELL\" && (\"content\" in cmd || \"format\" in cmd))) {\n            this.validationResults = {};\n            return;\n        }\n        switch (cmd.type) {\n            case \"ADD_DATA_VALIDATION_RULE\":\n            case \"REMOVE_DATA_VALIDATION_RULE\":\n                delete this.validationResults[cmd.sheetId];\n                break;\n        }\n    }\n    isDataValidationInvalid(cellPosition) {\n        return !this.getValidationResultForCell(cellPosition).isValid;\n    }\n    getInvalidDataValidationMessage(cellPosition) {\n        const validationResult = this.getValidationResultForCell(cellPosition);\n        return validationResult.isValid ? undefined : validationResult.error;\n    }\n    /**\n     * Check if the value is valid for the given criterion, and return an error message if not.\n     *\n     * The value must be canonicalized.\n     */\n    getDataValidationInvalidCriterionValueMessage(criterionType, value) {\n        const evaluator = dataValidationEvaluatorRegistry.get(criterionType);\n        if (value.startsWith(\"=\")) {\n            return evaluator.allowedValues === \"onlyLiterals\"\n                ? _t(\"The value must not be a formula\")\n                : undefined;\n        }\n        else if (evaluator.allowedValues === \"onlyFormulas\") {\n            return _t(\"The value must be a formula\");\n        }\n        return evaluator.isCriterionValueValid(value) ? undefined : evaluator.criterionValueErrorString;\n    }\n    isCellValidCheckbox(cellPosition) {\n        if (!this.getters.isMainCellPosition(cellPosition)) {\n            return false;\n        }\n        const rule = this.getters.getValidationRuleForCell(cellPosition);\n        if (!rule || rule.criterion.type !== \"isBoolean\") {\n            return false;\n        }\n        return this.getValidationResultForCell(cellPosition).isValid;\n    }\n    /** Get the validation result if the cell on the given position had the given value */\n    getValidationResultForCellValue(cellValue, cellPosition) {\n        const rule = this.getters.getValidationRuleForCell(cellPosition);\n        if (!rule) {\n            return VALID_RESULT;\n        }\n        const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);\n        return error ? { error, rule, isValid: false } : VALID_RESULT;\n    }\n    getValidationResultForCell(cellPosition) {\n        const { col, row, sheetId } = cellPosition;\n        if (!this.validationResults[sheetId]) {\n            this.validationResults[sheetId] = this.computeSheetValidationResults(sheetId);\n        }\n        return this.validationResults[sheetId][col]?.[row]?.() || VALID_RESULT;\n    }\n    computeSheetValidationResults(sheetId) {\n        const validationResults = {};\n        const ranges = this.getters.getDataValidationRules(sheetId).map((rule) => rule.ranges);\n        for (const cellPosition of getCellPositionsInRanges(ranges.flat())) {\n            const { col, row } = cellPosition;\n            if (!validationResults[col]) {\n                validationResults[col] = [];\n            }\n            validationResults[col][row] = lazy(() => {\n                const evaluatedCell = this.getters.getEvaluatedCell(cellPosition);\n                if (evaluatedCell.type === CellValueType.empty) {\n                    return VALID_RESULT;\n                }\n                return this.getValidationResultForCellValue(evaluatedCell.value, cellPosition);\n            });\n        }\n        return validationResults;\n    }\n    getRuleErrorForCellValue(cellValue, cellPosition, rule) {\n        const { sheetId } = cellPosition;\n        const criterion = rule.criterion;\n        const evaluator = dataValidationEvaluatorRegistry.get(criterion.type);\n        const offset = this.getCellOffsetInRule(cellPosition, rule);\n        const evaluatedCriterionValues = this.getEvaluatedCriterionValues(sheetId, offset, criterion);\n        const evaluatedCriterion = { ...criterion, values: evaluatedCriterionValues };\n        if (evaluator.isValueValid(cellValue, evaluatedCriterion, this.getters, sheetId)) {\n            return undefined;\n        }\n        return evaluator.getErrorString(evaluatedCriterion, this.getters, sheetId);\n    }\n    /** Get the offset of the cell inside the ranges of the rule. Throws an error if the cell isn't inside the rule. */\n    getCellOffsetInRule(cellPosition, rule) {\n        const range = rule.ranges.find((range) => isInside(cellPosition.col, cellPosition.row, range.zone));\n        if (!range) {\n            throw new Error(\"The cell is not in any range of the rule\");\n        }\n        return {\n            col: cellPosition.col - range.zone.left,\n            row: cellPosition.row - range.zone.top,\n        };\n    }\n    getEvaluatedCriterionValues(sheetId, offset, criterion) {\n        return criterion.values.map((value) => {\n            if (!value.startsWith(\"=\")) {\n                return value;\n            }\n            const formula = compile(value);\n            const translatedFormula = this.getters.getTranslatedCellFormula(sheetId, offset.col, offset.row, formula.tokens);\n            const evaluated = this.getters.evaluateFormula(sheetId, translatedFormula);\n            return evaluated && !isMatrix(evaluated) ? evaluated.toString() : \"\";\n        });\n    }\n}\n\nclass DynamicTablesPlugin extends UIPlugin {\n    static getters = [\n        \"canCreateDynamicTableOnZones\",\n        \"doesZonesContainFilter\",\n        \"getFilter\",\n        \"getFilters\",\n        \"getTable\",\n        \"getTables\",\n        \"getTablesOverlappingZones\",\n        \"getFilterId\",\n        \"getFilterHeaders\",\n        \"isFilterHeader\",\n    ];\n    tables = {};\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            (cmd.type === \"UPDATE_CELL\" && \"content\" in cmd) ||\n            cmd.type === \"EVALUATE_CELLS\") {\n            this.tables = {};\n            return;\n        }\n        switch (cmd.type) {\n            case \"CREATE_TABLE\":\n            case \"REMOVE_TABLE\":\n            case \"UPDATE_TABLE\":\n            case \"DELETE_CONTENT\":\n                this.tables = {};\n                break;\n        }\n    }\n    finalize() {\n        for (const sheetId of this.getters.getSheetIds()) {\n            if (!this.tables[sheetId]) {\n                this.tables[sheetId] = this.computeTables(sheetId);\n            }\n        }\n    }\n    computeTables(sheetId) {\n        const tables = [];\n        const coreTables = this.getters.getCoreTables(sheetId);\n        // First we create the static tables, so we can use them to compute collision with dynamic tables\n        for (const table of coreTables) {\n            if (table.type === \"dynamic\")\n                continue;\n            tables.push(table);\n        }\n        const staticTables = [...tables];\n        // Then we create the dynamic tables\n        for (const coreTable of coreTables) {\n            if (coreTable.type !== \"dynamic\")\n                continue;\n            const table = this.coreTableToTable(sheetId, coreTable);\n            let tableZone = table.range.zone;\n            // Reduce the zone to avoid collision with static tables. Per design, dynamic tables can't overlap with other\n            // dynamic tables, because formulas cannot spread on the same area, so we don't need to check for that.\n            for (const staticTable of staticTables) {\n                if (overlap(tableZone, staticTable.range.zone)) {\n                    tableZone = { ...tableZone, right: staticTable.range.zone.left - 1 };\n                }\n            }\n            tables.push({ ...table, range: this.getters.getRangeFromZone(sheetId, tableZone) });\n        }\n        return tables;\n    }\n    getFilters(sheetId) {\n        return this.getTables(sheetId)\n            .filter((table) => table.config.hasFilters)\n            .map((table) => table.filters)\n            .flat();\n    }\n    getTables(sheetId) {\n        return this.tables[sheetId] || [];\n    }\n    getFilter(position) {\n        const table = this.getTable(position);\n        if (!table || !table.config.hasFilters) {\n            return undefined;\n        }\n        return table.filters.find((filter) => filter.col === position.col);\n    }\n    getFilterId(position) {\n        return this.getFilter(position)?.id;\n    }\n    getTable({ sheetId, col, row }) {\n        return this.getTables(sheetId).find((table) => isInside(col, row, table.range.zone));\n    }\n    getTablesOverlappingZones(sheetId, zones) {\n        return this.getTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));\n    }\n    doesZonesContainFilter(sheetId, zones) {\n        return this.getTablesOverlappingZones(sheetId, zones).some((table) => table.config.hasFilters);\n    }\n    getFilterHeaders(sheetId) {\n        const headers = [];\n        for (const table of this.getTables(sheetId)) {\n            if (!table.config.hasFilters) {\n                continue;\n            }\n            const zone = table.range.zone;\n            const row = zone.top;\n            for (let col = zone.left; col <= zone.right; col++) {\n                headers.push({ sheetId, col, row });\n            }\n        }\n        return headers;\n    }\n    isFilterHeader({ sheetId, col, row }) {\n        const headers = this.getFilterHeaders(sheetId);\n        return headers.some((header) => header.col === col && header.row === row);\n    }\n    /**\n     * Check if we can create a dynamic table on the given zones.\n     * - The zones must be continuous\n     * - The union of the zones must be either:\n     *    - A single cell that contains an array formula\n     *    - All the spread cells of a single array formula\n     */\n    canCreateDynamicTableOnZones(sheetId, zones) {\n        if (!areZonesContinuous(zones)) {\n            return false;\n        }\n        const unionZone = union(...zones);\n        const topLeft = { col: unionZone.left, row: unionZone.top, sheetId };\n        const parentSpreadingCell = this.getters.getArrayFormulaSpreadingOn(topLeft);\n        if (!parentSpreadingCell) {\n            return false;\n        }\n        else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {\n            return true;\n        }\n        const zone = this.getters.getSpreadZone(parentSpreadingCell);\n        return deepEquals(unionZone, zone);\n    }\n    coreTableToTable(sheetId, table) {\n        if (table.type !== \"dynamic\") {\n            return table;\n        }\n        const tableZone = table.range.zone;\n        const tablePosition = { sheetId, col: tableZone.left, row: tableZone.top };\n        const zone = this.getters.getSpreadZone(tablePosition) ?? table.range.zone;\n        const range = this.getters.getRangeFromZone(sheetId, zone);\n        const filters = this.getDynamicTableFilters(sheetId, table, zone);\n        return { id: table.id, range, filters, config: table.config };\n    }\n    getDynamicTableFilters(sheetId, table, tableZone) {\n        const filters = [];\n        const { top, bottom, left, right } = tableZone;\n        for (let col = left; col <= right; col++) {\n            const tableColIndex = col - left;\n            const zone = { left: col, right: col, top, bottom };\n            const filter = createFilter(this.getDynamicTableFilterId(table.id, tableColIndex), this.getters.getRangeFromZone(sheetId, zone), table.config, this.getters.getRangeFromZone);\n            filters.push(filter);\n        }\n        return filters;\n    }\n    getDynamicTableFilterId(tableId, tableCol) {\n        return tableId + \"_\" + tableCol;\n    }\n    exportForExcel(data) {\n        for (const sheet of data.sheets) {\n            for (const tableData of sheet.tables) {\n                const zone = toZone(tableData.range);\n                const topLeft = { sheetId: sheet.id, col: zone.left, row: zone.top };\n                const coreTable = this.getters.getCoreTable(topLeft);\n                const table = this.getTable(topLeft);\n                if (coreTable?.type !== \"dynamic\" || !table) {\n                    continue;\n                }\n                tableData.range = zoneToXc(table.range.zone);\n            }\n        }\n    }\n}\n\nclass HeaderSizeUIPlugin extends UIPlugin {\n    static getters = [\"getRowSize\", \"getHeaderSize\"];\n    tallestCellInRow = {};\n    ctx = document.createElement(\"canvas\").getContext(\"2d\");\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"START\":\n                for (const sheetId of this.getters.getSheetIds()) {\n                    this.initializeSheet(sheetId);\n                }\n                break;\n            case \"CREATE_SHEET\": {\n                this.initializeSheet(cmd.sheetId);\n                break;\n            }\n            case \"DUPLICATE_SHEET\": {\n                const tallestCells = deepCopy(this.tallestCellInRow[cmd.sheetId]);\n                this.history.update(\"tallestCellInRow\", cmd.sheetIdTo, tallestCells);\n                break;\n            }\n            case \"DELETE_SHEET\":\n                const tallestCells = { ...this.tallestCellInRow };\n                delete tallestCells[cmd.sheetId];\n                this.history.update(\"tallestCellInRow\", tallestCells);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\": {\n                if (cmd.dimension === \"COL\") {\n                    return;\n                }\n                const tallestCells = removeIndexesFromArray(this.tallestCellInRow[cmd.sheetId], cmd.elements);\n                this.history.update(\"tallestCellInRow\", cmd.sheetId, tallestCells);\n                break;\n            }\n            case \"ADD_COLUMNS_ROWS\": {\n                if (cmd.dimension === \"COL\") {\n                    return;\n                }\n                const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);\n                const newCells = Array(cmd.quantity).fill(undefined);\n                const newTallestCells = insertItemsAtIndex(this.tallestCellInRow[cmd.sheetId], newCells, addIndex);\n                this.history.update(\"tallestCellInRow\", cmd.sheetId, newTallestCells);\n                break;\n            }\n            case \"RESIZE_COLUMNS_ROWS\":\n                {\n                    const sheetId = cmd.sheetId;\n                    if (cmd.dimension === \"ROW\") {\n                        for (const row of cmd.elements) {\n                            const tallestCell = this.getRowTallestCell(sheetId, row);\n                            this.history.update(\"tallestCellInRow\", sheetId, row, tallestCell);\n                        }\n                    }\n                    else {\n                        // Recompute row heights on col size change, they might have changed because of wrapped text\n                        for (const row of range(0, this.getters.getNumberRows(sheetId))) {\n                            for (const col of cmd.elements) {\n                                this.updateRowSizeForCellChange(sheetId, row, col);\n                            }\n                        }\n                    }\n                }\n                break;\n            case \"UPDATE_CELL\":\n                this.updateRowSizeForCellChange(cmd.sheetId, cmd.row, cmd.col);\n                break;\n            case \"ADD_MERGE\":\n            case \"REMOVE_MERGE\":\n                for (const target of cmd.target) {\n                    for (const position of positions(target)) {\n                        this.updateRowSizeForCellChange(cmd.sheetId, position.row, position.col);\n                    }\n                }\n        }\n        return;\n    }\n    getRowSize(sheetId, row) {\n        return Math.round(this.getters.getUserRowSize(sheetId, row) ??\n            this.tallestCellInRow[sheetId][row]?.size ??\n            DEFAULT_CELL_HEIGHT);\n    }\n    getHeaderSize(sheetId, dimension, index) {\n        if (this.getters.isHeaderHidden(sheetId, dimension, index)) {\n            return 0;\n        }\n        return dimension === \"ROW\"\n            ? this.getRowSize(sheetId, index)\n            : this.getters.getColSize(sheetId, index);\n    }\n    updateRowSizeForCellChange(sheetId, row, col) {\n        const tallestCellInRow = this.tallestCellInRow[sheetId]?.[row];\n        if (tallestCellInRow?.cell.col === col) {\n            const newTallestCell = this.getRowTallestCell(sheetId, row);\n            this.history.update(\"tallestCellInRow\", sheetId, row, newTallestCell);\n        }\n        const updatedCellHeight = this.getCellHeight({ sheetId, col, row });\n        if (updatedCellHeight <= DEFAULT_CELL_HEIGHT) {\n            return;\n        }\n        if ((!tallestCellInRow && updatedCellHeight > DEFAULT_CELL_HEIGHT) ||\n            (tallestCellInRow && updatedCellHeight > tallestCellInRow.size)) {\n            const newTallestCell = { cell: { sheetId, col, row }, size: updatedCellHeight };\n            this.history.update(\"tallestCellInRow\", sheetId, row, newTallestCell);\n        }\n    }\n    initializeSheet(sheetId) {\n        const tallestCells = [];\n        for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {\n            const tallestCell = this.getRowTallestCell(sheetId, row);\n            tallestCells.push(tallestCell);\n        }\n        this.history.update(\"tallestCellInRow\", sheetId, tallestCells);\n    }\n    /**\n     * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row\n     * merge, or the height of the cell computed based on its style/content.\n     */\n    getCellHeight(position) {\n        if (this.isInMultiRowMerge(position)) {\n            return DEFAULT_CELL_HEIGHT;\n        }\n        const cell = this.getters.getCell(position);\n        const colSize = this.getters.getColSize(position.sheetId, position.col);\n        return getDefaultCellHeight(this.ctx, cell, colSize);\n    }\n    isInMultiRowMerge(position) {\n        const merge = this.getters.getMerge(position);\n        return !!merge && merge.bottom !== merge.top;\n    }\n    /**\n     * Get the tallest cell of a row and its size.\n     */\n    getRowTallestCell(sheetId, row) {\n        const userRowSize = this.getters.getUserRowSize(sheetId, row);\n        if (userRowSize !== undefined) {\n            return undefined;\n        }\n        const cellIds = this.getters.getRowCells(sheetId, row);\n        let maxHeight = 0;\n        let tallestCell = undefined;\n        for (let i = 0; i < cellIds.length; i++) {\n            const cell = this.getters.getCellById(cellIds[i]);\n            if (!cell) {\n                continue;\n            }\n            const position = this.getters.getCellPosition(cell.id);\n            const cellHeight = this.getCellHeight(position);\n            if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {\n                maxHeight = cellHeight;\n                tallestCell = { cell: position, size: cellHeight };\n            }\n        }\n        if (tallestCell && tallestCell.size > DEFAULT_CELL_HEIGHT) {\n            return tallestCell;\n        }\n        return undefined;\n    }\n}\n\nconst PERCENT_FORMAT = \"0.00%\";\n/**\n * Dynamically creates a presentation layer wrapper around a given pivot class.\n *\n * It allows to implement additional behaviors and features that can be applied\n * to all pivots, regardless of the specific pivot implementation.\n * Examples of such features include calculated measures or \"Show value as\" options.\n */\nfunction withPivotPresentationLayer (PivotClass) {\n    class PivotPresentationLayer extends PivotClass {\n        getters;\n        cache = {};\n        rankAsc = {};\n        rankDesc = {};\n        runningTotal = {};\n        runningTotalInPercent = {};\n        constructor(custom, params) {\n            super(custom, params);\n            this.getters = params.getters;\n        }\n        init(params) {\n            this.cache = {};\n            this.rankAsc = {};\n            this.rankDesc = {};\n            this.runningTotal = {};\n            this.runningTotalInPercent = {};\n            super.init(params);\n        }\n        getPivotCellValueAndFormat(measureName, domain) {\n            return this.getMeasureDisplayValue(measureName, domain);\n        }\n        _getPivotCellValueAndFormat(measureName, domain) {\n            const cacheKey = `${measureName}-${domain\n                .map((node) => node.field + \"=\" + node.value)\n                .join(\",\")}`;\n            if (this.cache[cacheKey]) {\n                return this.cache[cacheKey];\n            }\n            const measure = this.getMeasure(measureName);\n            const result = measure.computedBy\n                ? this.computeMeasure(measure, domain)\n                : super.getPivotCellValueAndFormat(measureName, domain);\n            if (measure.format) {\n                this.cache[cacheKey] = { ...result, format: measure.format };\n            }\n            else {\n                this.cache[cacheKey] = result;\n            }\n            return this.cache[cacheKey];\n        }\n        computeMeasure(measure, domain) {\n            if (!measure.computedBy) {\n                return { value: 0 };\n            }\n            const { columns, rows } = super.definition;\n            if (columns.length + rows.length !== domain.length) {\n                const values = this.getValuesToAggregate(measure, domain);\n                const aggregator = AGGREGATORS_FN[measure.aggregator];\n                if (!aggregator) {\n                    return { value: 0 };\n                }\n                try {\n                    return aggregator([values], this.getters.getLocale());\n                }\n                catch (error) {\n                    return handleError(error, measure.aggregator.toUpperCase());\n                }\n            }\n            const formula = this.getters.getMeasureCompiledFormula(measure);\n            const getSymbolValue = (symbolName) => {\n                const { columns, rows } = this.definition;\n                if (columns.find((col) => col.nameWithGranularity === symbolName)) {\n                    const { colDomain } = domainToColRowDomain(this, domain);\n                    const symbolIndex = colDomain.findIndex((node) => node.field === symbolName);\n                    return this.getPivotHeaderValueAndFormat(colDomain.slice(0, symbolIndex + 1));\n                }\n                if (rows.find((row) => row.nameWithGranularity === symbolName)) {\n                    const { rowDomain } = domainToColRowDomain(this, domain);\n                    const symbolIndex = rowDomain.findIndex((row) => row.field === symbolName);\n                    return this.getPivotHeaderValueAndFormat(rowDomain.slice(0, symbolIndex + 1));\n                }\n                return this._getPivotCellValueAndFormat(symbolName, domain);\n            };\n            const result = this.getters.evaluateCompiledFormula(measure.computedBy.sheetId, formula, getSymbolValue);\n            if (isMatrix(result)) {\n                return result[0][0];\n            }\n            return result;\n        }\n        getValuesToAggregate(measure, domain) {\n            const { rowDomain, colDomain } = domainToColRowDomain(this, domain);\n            const table = this.getTableStructure();\n            const values = [];\n            if (colDomain.length === 0 &&\n                rowDomain.length < this.definition.rows.length &&\n                this.definition.rows.length &&\n                this.definition.columns.length) {\n                const colDomains = this.treeToLeafDomains(table.getColTree());\n                const rowSubTree = this.getSubTreeMatchingDomain(table.getRowTree(), rowDomain);\n                const rowDomains = this.treeToLeafDomains(rowSubTree);\n                for (const colDomain of colDomains) {\n                    for (const subRowDomain of rowDomains) {\n                        values.push(this._getPivotCellValueAndFormat(measure.id, rowDomain.concat(subRowDomain).concat(colDomain)));\n                    }\n                }\n                return values;\n            }\n            else if (rowDomain.length === this.definition.rows.length && colDomain.length === 0) {\n                // aggregate a row in the last column\n                const tree = table.getColTree();\n                const subTree = this.getSubTreeMatchingDomain(tree, colDomain);\n                const domains = this.treeToLeafDomains(subTree, colDomain);\n                for (const domain of domains) {\n                    values.push(this._getPivotCellValueAndFormat(measure.id, rowDomain.concat(domain)));\n                }\n                return values;\n            }\n            else {\n                const tree = table.getRowTree();\n                const subTree = this.getSubTreeMatchingDomain(tree, rowDomain);\n                const domains = this.treeToLeafDomains(subTree, rowDomain);\n                for (const domain of domains) {\n                    values.push(this._getPivotCellValueAndFormat(measure.id, domain.concat(colDomain)));\n                }\n                return values;\n            }\n        }\n        getSubTreeMatchingDomain(tree, domain, domainLevel = 0) {\n            if (domainLevel > domain.length) {\n                return [];\n            }\n            if (domain.length === domainLevel) {\n                return tree;\n            }\n            for (const node of tree) {\n                const dimension = this.definition.getDimension(node.field);\n                const normalizedValue = toNormalizedPivotValue(dimension, domain[domainLevel]?.value);\n                if (node.field === domain[domainLevel]?.field && node.value === normalizedValue) {\n                    return this.getSubTreeMatchingDomain(node.children, domain, domainLevel + 1);\n                }\n            }\n            return tree;\n        }\n        treeToLeafDomains(tree, parentDomain = []) {\n            const domains = [];\n            for (const node of tree) {\n                const dimension = this.definition.getDimension(node.field);\n                const nodeDomain = [\n                    ...parentDomain,\n                    { field: node.field, value: node.value, type: dimension.type },\n                ];\n                if (node.children.length === 0) {\n                    domains.push(nodeDomain);\n                }\n                else {\n                    domains.push(...this.treeToLeafDomains(node.children, nodeDomain));\n                }\n            }\n            return domains;\n        }\n        getMeasureDisplayValue(measureId, domain) {\n            const measure = this.getMeasure(measureId);\n            const rawValue = this._getPivotCellValueAndFormat(measureId, domain);\n            if (!measure.display || measure.display.type === \"no_calculations\" || rawValue.message) {\n                return rawValue;\n            }\n            const fieldName = measure.display.fieldNameWithGranularity;\n            if (fieldName && !this.isFieldInPivot(fieldName)) {\n                return {\n                    value: CellErrorType.NotAvailable,\n                    message: _t('Field \"%s\" not found in pivot for measure display calculation', fieldName),\n                };\n            }\n            try {\n                const display = measure.display;\n                switch (display.type) {\n                    case \"%_of_grand_total\":\n                        return this.asPercentOfGrandTotal(rawValue, measure);\n                    case \"%_of_col_total\":\n                        return this.asPercentOfColumnTotal(rawValue, measure, domain);\n                    case \"%_of_row_total\":\n                        return this.asPercentOfRowTotal(rawValue, measure, domain);\n                    case \"%_of_parent_row_total\":\n                        return this.asPercentOfParentRowTotal(rawValue, measure, domain);\n                    case \"%_of_parent_col_total\":\n                        return this.asPercentOfParentColumnTotal(rawValue, measure, domain);\n                    case \"index\":\n                        return this.asIndex(rawValue, measure, domain);\n                    case \"%_of_parent_total\":\n                        return this.asPercentOfParentTotal(rawValue, measure, domain, display);\n                    case \"running_total\":\n                        return this.asRunningTotal(rawValue, measure, domain, display, \"running_total\");\n                    case \"%_running_total\":\n                        return this.asRunningTotal(rawValue, measure, domain, display, \"%_running_total\");\n                    case \"rank_asc\":\n                        return this.asRank(rawValue, measure, domain, display, \"asc\");\n                    case \"rank_desc\":\n                        return this.asRank(rawValue, measure, domain, display, \"desc\");\n                    case \"%_of\":\n                        return this.asPercentOf(rawValue, measure, domain, display);\n                    case \"difference_from\":\n                        return this.asDifferenceFrom(rawValue, measure, domain, display);\n                    case \"%_difference_from\":\n                        return this.asDifferenceFromInPercent(rawValue, measure, domain, display);\n                }\n                return rawValue;\n            }\n            catch (e) {\n                return handleError(e, \"COMPUTE_MEASURE_DISPLAY_VALUE\");\n            }\n        }\n        asPercentOfGrandTotal(rawValue, measure) {\n            const grandTotal = this.getGrandTotal(measure.id);\n            return grandTotal === 0\n                ? { value: CellErrorType.DivisionByZero }\n                : { value: this.measureValueToNumber(rawValue) / grandTotal, format: PERCENT_FORMAT };\n        }\n        asPercentOfRowTotal(rawValue, measure, domain) {\n            const rowTotal = this.getRowTotal(measure.id, domain);\n            return rowTotal === 0\n                ? { value: CellErrorType.DivisionByZero }\n                : { value: this.measureValueToNumber(rawValue) / rowTotal, format: PERCENT_FORMAT };\n        }\n        asPercentOfColumnTotal(rawValue, measure, domain) {\n            const columnTotal = this.getColumnTotal(measure.id, domain);\n            return columnTotal === 0\n                ? { value: CellErrorType.DivisionByZero }\n                : { value: this.measureValueToNumber(rawValue) / columnTotal, format: PERCENT_FORMAT };\n        }\n        asPercentOfParentRowTotal(rawValue, measure, domain) {\n            const parentRowDomain = getDomainOfParentRow(this, domain);\n            const parentRowValue = this.measureValueToNumber(this._getPivotCellValueAndFormat(measure.id, parentRowDomain));\n            return parentRowValue === 0\n                ? { value: \"\" }\n                : { value: this.measureValueToNumber(rawValue) / parentRowValue, format: PERCENT_FORMAT };\n        }\n        asPercentOfParentColumnTotal(rawValue, measure, domain) {\n            const parentColumnDomain = getDomainOfParentCol(this, domain);\n            const parentColValue = this.measureValueToNumber(this._getPivotCellValueAndFormat(measure.id, parentColumnDomain));\n            return parentColValue === 0\n                ? { value: \"\" }\n                : { value: this.measureValueToNumber(rawValue) / parentColValue, format: PERCENT_FORMAT };\n        }\n        asPercentOfParentTotal(rawValue, measure, domain, display) {\n            const { fieldNameWithGranularity } = display;\n            if (!fieldNameWithGranularity) {\n                return rawValue;\n            }\n            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {\n                return { value: \"\" };\n            }\n            const parentDomain = getFieldParentDomain(this, fieldNameWithGranularity, domain);\n            const parentTotal = this._getPivotCellValueAndFormat(measure.id, parentDomain);\n            const parentTotalValue = this.measureValueToNumber(parentTotal);\n            return parentTotalValue === 0\n                ? { value: \"\" }\n                : { value: this.measureValueToNumber(rawValue) / parentTotalValue, format: PERCENT_FORMAT };\n        }\n        asIndex(rawValue, measure, domain) {\n            const value = this.measureValueToNumber(rawValue);\n            const parentRowTotal = this.getRowTotal(measure.id, domain);\n            const parentColTotal = this.getColumnTotal(measure.id, domain);\n            const grandTotal = this.getGrandTotal(measure.id);\n            return parentRowTotal === 0 || parentColTotal === 0\n                ? { value: CellErrorType.DivisionByZero }\n                : { value: (value * grandTotal) / (parentColTotal * parentRowTotal), format: undefined };\n        }\n        asRunningTotal(rawValue, measure, domain, display, mode) {\n            const { fieldNameWithGranularity } = display;\n            if (!fieldNameWithGranularity) {\n                return rawValue;\n            }\n            const totalCache = mode === \"running_total\" ? this.runningTotal : this.runningTotalInPercent;\n            let runningTotals = totalCache[measure.id]?.[fieldNameWithGranularity];\n            if (!runningTotals) {\n                runningTotals = this.computeRunningTotal(measure, fieldNameWithGranularity, mode);\n                if (!totalCache[measure.id]) {\n                    totalCache[measure.id] = {};\n                }\n                totalCache[measure.id][fieldNameWithGranularity] = runningTotals;\n            }\n            const { rowDomain, colDomain } = domainToColRowDomain(this, domain);\n            const colDomainKey = domainToString(colDomain);\n            const rowDomainKey = domainToString(rowDomain);\n            const runningTotal = runningTotals[colDomainKey]?.[rowDomainKey];\n            return {\n                value: runningTotal ?? \"\",\n                format: mode === \"running_total\" ? rawValue.format : PERCENT_FORMAT,\n            };\n        }\n        asPercentOf(rawValue, measure, domain, display) {\n            const { fieldNameWithGranularity, value } = display;\n            if (value === undefined || !fieldNameWithGranularity) {\n                return rawValue;\n            }\n            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {\n                return { value: \"\" };\n            }\n            let comparedValue = this.getComparisonValue(measure, domain, fieldNameWithGranularity, value);\n            let numberValue = this.strictMeasureValueToNumber(rawValue);\n            if (comparedValue === 0 || (comparedValue === \"sameValue\" && numberValue === 0)) {\n                return { value: CellErrorType.DivisionByZero };\n            }\n            else if (!comparedValue || (comparedValue === \"sameValue\" && !numberValue)) {\n                return { value: \"\" };\n            }\n            else if (comparedValue === \"sameValue\") {\n                return { value: 1, format: PERCENT_FORMAT };\n            }\n            else if (numberValue === undefined) {\n                return { value: CellErrorType.NullError };\n            }\n            return { value: numberValue / comparedValue, format: PERCENT_FORMAT };\n        }\n        asDifferenceFrom(rawValue, measure, domain, display) {\n            const { fieldNameWithGranularity, value } = display;\n            if (value === undefined || !fieldNameWithGranularity) {\n                return rawValue;\n            }\n            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {\n                return { value: \"\" };\n            }\n            const comparedValue = this.getComparisonValue(measure, domain, fieldNameWithGranularity, value) || 0;\n            return comparedValue === \"sameValue\"\n                ? { value: \"\" }\n                : {\n                    value: this.measureValueToNumber(rawValue) - comparedValue,\n                    format: rawValue.format,\n                };\n        }\n        asDifferenceFromInPercent(rawValue, measure, domain, display) {\n            const { fieldNameWithGranularity, value } = display;\n            if (value === undefined || !fieldNameWithGranularity) {\n                return rawValue;\n            }\n            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {\n                return { value: \"\" };\n            }\n            let comparedValue = this.getComparisonValue(measure, domain, fieldNameWithGranularity, value);\n            const numberValue = this.strictMeasureValueToNumber(rawValue);\n            if (comparedValue === 0) {\n                return { value: CellErrorType.DivisionByZero };\n            }\n            else if (!comparedValue || comparedValue === \"sameValue\") {\n                return { value: \"\" };\n            }\n            else if (numberValue === undefined) {\n                return { value: CellErrorType.NullError };\n            }\n            return { value: (numberValue - comparedValue) / comparedValue, format: PERCENT_FORMAT };\n        }\n        asRank(rawValue, measure, domain, display, order) {\n            const { fieldNameWithGranularity } = display;\n            if (!fieldNameWithGranularity) {\n                return rawValue;\n            }\n            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {\n                return { value: \"\" };\n            }\n            const rankingCache = order === \"asc\" ? this.rankAsc : this.rankDesc;\n            let ranking = rankingCache[measure.id]?.[fieldNameWithGranularity];\n            if (!ranking) {\n                ranking = this.computeRank(measure, fieldNameWithGranularity, order);\n                if (!rankingCache[measure.id]) {\n                    rankingCache[measure.id] = {};\n                }\n                rankingCache[measure.id][fieldNameWithGranularity] = ranking;\n            }\n            const { rowDomain, colDomain } = domainToColRowDomain(this, domain);\n            const colDomainKey = domainToString(colDomain);\n            const rowDomainKey = domainToString(rowDomain);\n            const rank = ranking[colDomainKey]?.[rowDomainKey];\n            return { value: rank ?? \"\" };\n        }\n        computeRank(measure, fieldNameWithGranularity, order) {\n            const ranking = {};\n            const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);\n            const secondaryDimension = mainDimension === \"row\" ? \"column\" : \"row\";\n            let pivotCells = this.getPivotValueCells(measure.id);\n            if (mainDimension === \"column\") {\n                // Transpose the pivot cells so we can do the same operations on the columns as on the rows\n                // This means that we need to transpose back the ranking at the end\n                pivotCells = transposeMatrix(pivotCells);\n            }\n            for (const col of pivotCells) {\n                const colDomain = getDimensionDomain(this, secondaryDimension, col[0].domain);\n                const colDomainKey = domainToString(colDomain);\n                const cells = col\n                    .map((cell) => ({\n                    ...cell,\n                    value: this.strictMeasureValueToNumber(this._getPivotCellValueAndFormat(measure.id, cell.domain)),\n                    rowDomain: getDimensionDomain(this, mainDimension, cell.domain),\n                }))\n                    .filter((cell) => isFieldInDomain(fieldNameWithGranularity, cell.rowDomain));\n                // Group the cells by ranking domain, and sort them to get the ranking\n                const groupedCell = Object.groupBy(cells, (cell) => getRankingDomainKey(cell.rowDomain, fieldNameWithGranularity));\n                for (const rankingDomainKey in groupedCell) {\n                    groupedCell[rankingDomainKey] = removeDuplicates$1(groupedCell[rankingDomainKey] || [], (cell) => cell.value)\n                        .filter((cell) => cell.value !== undefined)\n                        .sort((a, b) => (order === \"asc\" ? a.value - b.value : b.value - a.value));\n                }\n                ranking[colDomainKey] = {};\n                for (const cell of cells) {\n                    const rowDomain = getDimensionDomain(this, mainDimension, cell.domain);\n                    const rowDomainKey = domainToString(rowDomain);\n                    const rankingDomainKey = getRankingDomainKey(cell.rowDomain, fieldNameWithGranularity);\n                    const group = groupedCell[rankingDomainKey];\n                    if (!group) {\n                        continue;\n                    }\n                    const rank = group.findIndex((c) => c.value === cell.value);\n                    if (rank !== -1) {\n                        ranking[colDomainKey][rowDomainKey] = rank + 1; // Ranks start at 1\n                    }\n                }\n            }\n            return mainDimension === \"row\" ? ranking : transpose2dPOJO(ranking);\n        }\n        computeRunningTotal(measure, fieldNameWithGranularity, mode) {\n            const cellsRunningTotals = {};\n            const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);\n            const secondaryDimension = mainDimension === \"row\" ? \"column\" : \"row\";\n            let pivotCells = this.getPivotValueCells(measure.id);\n            if (mainDimension === \"column\") {\n                // Transpose the pivot cells so we can do the same operations on the columns as on the rows\n                // This means that we need to transpose back the totals at the end\n                pivotCells = transposeMatrix(pivotCells);\n            }\n            for (const col of pivotCells) {\n                const colDomain = getDimensionDomain(this, secondaryDimension, col[0].domain);\n                const colDomainKey = domainToString(colDomain);\n                cellsRunningTotals[colDomainKey] = {};\n                const runningTotals = {};\n                const cellsWithValue = col\n                    .map((cell) => ({\n                    ...cell,\n                    rowDomain: getDimensionDomain(this, mainDimension, cell.domain),\n                    value: this.measureValueToNumber(this._getPivotCellValueAndFormat(measure.id, cell.domain)),\n                }))\n                    .filter((cell) => isFieldInDomain(fieldNameWithGranularity, cell.rowDomain));\n                for (const cell of cellsWithValue) {\n                    const rowDomainKey = domainToString(cell.rowDomain);\n                    const runningTotalKey = getRunningTotalDomainKey(cell.rowDomain, fieldNameWithGranularity);\n                    const runningTotal = (runningTotals[runningTotalKey] || 0) + cell.value;\n                    runningTotals[runningTotalKey] = runningTotal;\n                    cellsRunningTotals[colDomainKey][rowDomainKey] = runningTotal;\n                }\n                if (mode === \"%_running_total\") {\n                    for (const cell of cellsWithValue) {\n                        const rowDomain = cell.rowDomain;\n                        const rowDomainKey = domainToString(rowDomain);\n                        const runningTotalKey = getRunningTotalDomainKey(rowDomain, fieldNameWithGranularity);\n                        const cellRunningTotal = cellsRunningTotals[colDomainKey][rowDomainKey] || 0;\n                        const finalRunningTotal = runningTotals[runningTotalKey];\n                        cellsRunningTotals[colDomainKey][rowDomainKey] = !finalRunningTotal\n                            ? undefined\n                            : cellRunningTotal / finalRunningTotal;\n                    }\n                }\n            }\n            return mainDimension === \"row\" ? cellsRunningTotals : transpose2dPOJO(cellsRunningTotals);\n        }\n        getGrandTotal(measureId) {\n            const grandTotal = this._getPivotCellValueAndFormat(measureId, []);\n            return this.measureValueToNumber(grandTotal);\n        }\n        getRowTotal(measureId, domain) {\n            const totalDomain = domainToColRowDomain(this, domain).rowDomain;\n            const rowTotal = this._getPivotCellValueAndFormat(measureId, totalDomain);\n            return this.measureValueToNumber(rowTotal);\n        }\n        getColumnTotal(measureId, domain) {\n            const totalDomain = domainToColRowDomain(this, domain).colDomain;\n            const columnTotal = this._getPivotCellValueAndFormat(measureId, totalDomain);\n            return this.measureValueToNumber(columnTotal);\n        }\n        isFieldInPivot(nameWithGranularity) {\n            return (this.definition.columns.some((c) => c.nameWithGranularity === nameWithGranularity) ||\n                this.definition.rows.some((r) => r.nameWithGranularity === nameWithGranularity));\n        }\n        /**\n         * With the given measure, fetch the value of the cell in the pivot that has the given domain with\n         * the value of the field `fieldNameWithGranularity` replaced by `valueToCompare`.\n         *\n         * @param valueToCompare either a value to replace the field value with, or \"(previous)\" or \"(next)\"\n         * @returns the value of the cell in the pivot with the new domain, or \"sameValue\" if the domain is the same\n         */\n        getComparisonValue(measure, domain, fieldNameWithGranularity, valueToCompare) {\n            const comparedDomain = valueToCompare === PREVIOUS_VALUE || valueToCompare === NEXT_VALUE\n                ? getPreviousOrNextValueDomain(this, domain, fieldNameWithGranularity, valueToCompare)\n                : replaceFieldValueInDomain(domain, fieldNameWithGranularity, valueToCompare);\n            if (deepEquals(comparedDomain, domain)) {\n                return \"sameValue\";\n            }\n            if (!comparedDomain || !isDomainIsInPivot(this, comparedDomain)) {\n                throw new NotAvailableError();\n            }\n            const comparedValue = this._getPivotCellValueAndFormat(measure.id, comparedDomain);\n            const comparedValueNumber = this.strictMeasureValueToNumber(comparedValue);\n            return comparedValueNumber;\n        }\n        getPivotValueCells(measureId) {\n            return this.getTableStructure()\n                .getPivotCells()\n                .map((col) => col.filter((cell) => cell.type === \"VALUE\" && cell.measure === measureId))\n                .filter((col) => col.length > 0);\n        }\n        measureValueToNumber(result) {\n            if (typeof result.value === \"number\") {\n                return result.value;\n            }\n            if (!result.value) {\n                return 0;\n            }\n            // Should not happen, measures aggregates are always numbers or undefined\n            throw new Error(`Value ${result.value} is not a number`);\n        }\n        strictMeasureValueToNumber(result) {\n            if (typeof result.value === \"number\") {\n                return result.value;\n            }\n            if (!result.value) {\n                return undefined;\n            }\n            throw new Error(`Value ${result.value} is not a number`);\n        }\n    }\n    return PivotPresentationLayer;\n}\n\nconst UNDO_REDO_PIVOT_COMMANDS = [\"ADD_PIVOT\", \"UPDATE_PIVOT\"];\nfunction isPivotCommand(cmd) {\n    return UNDO_REDO_PIVOT_COMMANDS.includes(cmd.type);\n}\nclass PivotUIPlugin extends UIPlugin {\n    static getters = [\n        \"getPivot\",\n        \"getFirstPivotFunction\",\n        \"getPivotIdFromPosition\",\n        \"getPivotCellFromPosition\",\n        \"generateNewCalculatedMeasureName\",\n        \"isPivotUnused\",\n        \"isSpillPivotFormula\",\n    ];\n    pivots = {};\n    unusedPivots;\n    custom;\n    constructor(config) {\n        super(config);\n        this.custom = config.custom;\n    }\n    beforeHandle(cmd) {\n        switch (cmd.type) {\n            case \"START\":\n                for (const pivotId of this.getters.getPivotIds()) {\n                    this.setupPivot(pivotId);\n                }\n        }\n    }\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type)) {\n            for (const pivotId of this.getters.getPivotIds()) {\n                if (!pivotRegistry.get(this.getters.getPivotCoreDefinition(pivotId).type).externalData) {\n                    this.setupPivot(pivotId, { recreate: true });\n                }\n            }\n        }\n        switch (cmd.type) {\n            case \"REFRESH_PIVOT\":\n                this.refreshPivot(cmd.id);\n                break;\n            case \"ADD_PIVOT\": {\n                this.setupPivot(cmd.pivotId);\n                break;\n            }\n            case \"DUPLICATE_PIVOT\": {\n                this.setupPivot(cmd.newPivotId);\n                break;\n            }\n            case \"UPDATE_PIVOT\": {\n                this.setupPivot(cmd.pivotId, { recreate: true });\n                break;\n            }\n            case \"DELETE_SHEET\":\n            case \"UPDATE_CELL\": {\n                this.unusedPivots = undefined;\n                break;\n            }\n            case \"UNDO\":\n            case \"REDO\": {\n                this.unusedPivots = undefined;\n                const pivotCommands = cmd.commands.filter(isPivotCommand);\n                for (const cmd of pivotCommands) {\n                    const pivotId = cmd.pivotId;\n                    if (!this.getters.isExistingPivot(pivotId)) {\n                        continue;\n                    }\n                    this.setupPivot(pivotId, { recreate: true });\n                }\n                break;\n            }\n            case \"UPDATE_LOCALE\":\n                /**\n                 * Reset the cache of the date/datetime pivot values, as it depends on\n                 * the locale. (e.g. the first day of the week)\n                 */\n                resetMapValueDimensionDate();\n                break;\n        }\n    }\n    // ---------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------\n    /**\n     * Get the id of the pivot at the given position. Returns undefined if there\n     * is no pivot at this position\n     */\n    getPivotIdFromPosition(position) {\n        const cell = this.getters.getCorrespondingFormulaCell(position);\n        if (cell && cell.isFormula) {\n            const pivotFunction = this.getFirstPivotFunction(position.sheetId, cell.compiledFormula.tokens);\n            if (pivotFunction) {\n                const pivotId = pivotFunction.args[0]?.toString();\n                return pivotId && this.getters.getPivotId(pivotId);\n            }\n        }\n        return undefined;\n    }\n    isSpillPivotFormula(position) {\n        const cell = this.getters.getCorrespondingFormulaCell(position);\n        if (cell && cell.isFormula) {\n            const pivotFunction = this.getFirstPivotFunction(position.sheetId, cell.compiledFormula.tokens);\n            return pivotFunction?.functionName === \"PIVOT\";\n        }\n        return false;\n    }\n    getFirstPivotFunction(sheetId, tokens) {\n        const pivotFunction = getFirstPivotFunction(tokens);\n        if (!pivotFunction) {\n            return undefined;\n        }\n        const { functionName, args } = pivotFunction;\n        const evaluatedArgs = args.map((argAst) => {\n            if (argAst.type == \"EMPTY\") {\n                return undefined;\n            }\n            else if (argAst.type === \"STRING\" ||\n                argAst.type === \"BOOLEAN\" ||\n                argAst.type === \"NUMBER\") {\n                return argAst.value;\n            }\n            const argsString = astToFormula(argAst);\n            return this.getters.evaluateFormula(sheetId, argsString);\n        });\n        return { functionName, args: evaluatedArgs };\n    }\n    /**\n     * Returns the domain args of a pivot formula from a position.\n     * For all those formulas:\n     *\n     * =PIVOT.VALUE(1,\"expected_revenue\",\"stage_id\",2,\"city\",\"Brussels\")\n     * =PIVOT.HEADER(1,\"stage_id\",2,\"city\",\"Brussels\")\n     * =PIVOT.HEADER(1,\"stage_id\",2,\"city\",\"Brussels\",\"measure\",\"expected_revenue\")\n     *\n     * the result is the same: [\"stage_id\", 2, \"city\", \"Brussels\"]\n     *\n     * If the cell is the result of PIVOT, the result is the domain of the cell\n     * as if it was the individual pivot formula\n     */\n    getPivotCellFromPosition(position) {\n        const cell = this.getters.getCorrespondingFormulaCell(position);\n        if (!cell || !cell.isFormula || getNumberOfPivotFunctions(cell.compiledFormula.tokens) === 0) {\n            return EMPTY_PIVOT_CELL;\n        }\n        const mainPosition = this.getters.getCellPosition(cell.id);\n        const result = this.getters.getFirstPivotFunction(position.sheetId, cell.compiledFormula.tokens);\n        if (!result) {\n            return EMPTY_PIVOT_CELL;\n        }\n        const { functionName, args } = result;\n        const formulaId = args[0];\n        if (!formulaId) {\n            return EMPTY_PIVOT_CELL;\n        }\n        const pivotId = this.getters.getPivotId(formulaId.toString());\n        if (!pivotId) {\n            return EMPTY_PIVOT_CELL;\n        }\n        const pivot = this.getPivot(pivotId);\n        if (!pivot.isValid()) {\n            return EMPTY_PIVOT_CELL;\n        }\n        if (functionName === \"PIVOT\" &&\n            !cell.content.replaceAll(\" \", \"\").toUpperCase().startsWith(\"=PIVOT\")) {\n            return EMPTY_PIVOT_CELL;\n        }\n        if (functionName === \"PIVOT\") {\n            const includeTotal = toScalar(args[2]);\n            const shouldIncludeTotal = includeTotal === undefined ? true : toBoolean(includeTotal);\n            const includeColumnHeaders = toScalar(args[3]);\n            const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);\n            const pivotCells = pivot\n                .getTableStructure()\n                .getPivotCells(shouldIncludeTotal, shouldIncludeColumnHeaders);\n            const pivotCol = position.col - mainPosition.col;\n            const pivotRow = position.row - mainPosition.row;\n            return pivotCells[pivotCol][pivotRow];\n        }\n        try {\n            if (functionName === \"PIVOT.HEADER\" && args.at(-2) === \"measure\") {\n                const domain = pivot.parseArgsToPivotDomain(args.slice(1, -2).map((value) => ({ value })));\n                return {\n                    type: \"MEASURE_HEADER\",\n                    domain,\n                    measure: args.at(-1)?.toString() || \"\",\n                };\n            }\n            else if (functionName === \"PIVOT.HEADER\") {\n                const domain = pivot.parseArgsToPivotDomain(args.slice(1).map((value) => ({ value })));\n                return {\n                    type: \"HEADER\",\n                    domain,\n                };\n            }\n            const [measure, ...domainArgs] = args.slice(1);\n            const domain = pivot.parseArgsToPivotDomain(domainArgs.map((value) => ({ value })));\n            return {\n                type: \"VALUE\",\n                domain,\n                measure: measure?.toString() || \"\",\n            };\n        }\n        catch (_) {\n            return EMPTY_PIVOT_CELL;\n        }\n    }\n    generateNewCalculatedMeasureName(measures) {\n        const existingMeasures = measures.map((m) => m.fieldName);\n        let i = 1;\n        let name = _t(\"Calculated measure %s\", i);\n        while (existingMeasures.includes(name)) {\n            i++;\n            name = _t(\"Calculated measure %s\", i);\n        }\n        return name;\n    }\n    getPivot(pivotId) {\n        if (!this.getters.isExistingPivot(pivotId)) {\n            throw new Error(`pivot ${pivotId} not found`);\n        }\n        return this.pivots[pivotId];\n    }\n    isPivotUnused(pivotId) {\n        return this._getUnusedPivots().includes(pivotId);\n    }\n    // ---------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------\n    /**\n     * Refresh the cache of a pivot\n     */\n    refreshPivot(pivotId) {\n        const pivot = this.getters.getPivot(pivotId);\n        pivot.init({ reload: true });\n    }\n    setupPivot(pivotId, { recreate } = { recreate: false }) {\n        const definition = this.getters.getPivotCoreDefinition(pivotId);\n        if (!(pivotId in this.pivots)) {\n            const Pivot = withPivotPresentationLayer(pivotRegistry.get(definition.type).ui);\n            this.pivots[pivotId] = new Pivot(this.custom, { definition, getters: this.getters });\n        }\n        else if (recreate) {\n            this.pivots[pivotId].onDefinitionChange(definition);\n        }\n    }\n    _getUnusedPivots() {\n        if (this.unusedPivots !== undefined) {\n            return this.unusedPivots;\n        }\n        const unusedPivots = new Set(this.getters.getPivotIds());\n        for (const sheetId of this.getters.getSheetIds()) {\n            for (const cellId in this.getters.getCells(sheetId)) {\n                const position = this.getters.getCellPosition(cellId);\n                const pivotId = this.getPivotIdFromPosition(position);\n                if (pivotId) {\n                    unusedPivots.delete(pivotId);\n                    if (!unusedPivots.size) {\n                        this.unusedPivots = [];\n                        return [];\n                    }\n                }\n            }\n        }\n        this.unusedPivots = [...unusedPivots];\n        return this.unusedPivots;\n    }\n}\n\n/**\n * This plugin manage the autofill.\n *\n * The way it works is the next one:\n * For each line (row if the direction is left/right, col otherwise), we create\n * a \"AutofillGenerator\" object which is used to compute the cells to\n * autofill.\n *\n * When we need to autofill a cell, we compute the origin cell in the source.\n *  EX: from A1:A2, autofill A3->A6.\n *      Target | Origin cell\n *        A3   |   A1\n *        A4   |   A2\n *        A5   |   A1\n *        A6   |   A2\n * When we have the origin, we take the associated cell in the AutofillGenerator\n * and we apply the modifier (AutofillModifier) associated to the content of the\n * cell.\n */\n/**\n * This class is used to generate the next values to autofill.\n * It's done from a selection (the source) and describe how the next values\n * should be computed.\n */\nclass AutofillGenerator {\n    cells;\n    getters;\n    index = 0;\n    direction;\n    constructor(cells, getters, direction) {\n        this.cells = cells;\n        this.getters = getters;\n        this.direction = direction;\n    }\n    /**\n     * Get the next value to autofill\n     */\n    next() {\n        const genCell = this.cells[this.index++ % this.cells.length];\n        const rule = genCell.rule;\n        const { cellData, tooltip } = autofillModifiersRegistry\n            .get(rule.type)\n            .apply(rule, genCell.data, this.getters, this.direction);\n        return {\n            cellData,\n            tooltip,\n            origin: {\n                col: genCell.data.col,\n                row: genCell.data.row,\n            },\n        };\n    }\n}\n/**\n * Autofill Plugin\n *\n */\nclass AutofillPlugin extends UIPlugin {\n    static layers = [\"Autofill\"];\n    static getters = [\"getAutofillTooltip\"];\n    autofillZone;\n    steps;\n    lastCellSelected = {};\n    direction;\n    tooltip;\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"AUTOFILL_SELECT\":\n                const sheetId = this.getters.getActiveSheetId();\n                this.lastCellSelected.col =\n                    cmd.col === -1\n                        ? this.lastCellSelected.col\n                        : clip(cmd.col, 0, this.getters.getNumberCols(sheetId));\n                this.lastCellSelected.row =\n                    cmd.row === -1\n                        ? this.lastCellSelected.row\n                        : clip(cmd.row, 0, this.getters.getNumberRows(sheetId));\n                if (this.lastCellSelected.col !== undefined && this.lastCellSelected.row !== undefined) {\n                    return \"Success\" /* CommandResult.Success */;\n                }\n                return \"InvalidAutofillSelection\" /* CommandResult.InvalidAutofillSelection */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"AUTOFILL\":\n                this.autofill(true);\n                break;\n            case \"AUTOFILL_SELECT\":\n                this.select(cmd.col, cmd.row);\n                break;\n            case \"AUTOFILL_AUTO\":\n                this.autofillAuto();\n                break;\n            case \"AUTOFILL_CELL\":\n                this.autoFillMerge(cmd.originCol, cmd.originRow, cmd.col, cmd.row);\n                const sheetId = this.getters.getActiveSheetId();\n                this.dispatch(\"UPDATE_CELL\", {\n                    sheetId,\n                    col: cmd.col,\n                    row: cmd.row,\n                    style: cmd.style || null,\n                    content: cmd.content || \"\",\n                    format: cmd.format || \"\",\n                });\n                this.dispatch(\"SET_BORDER\", {\n                    sheetId,\n                    col: cmd.col,\n                    row: cmd.row,\n                    border: cmd.border,\n                });\n                this.autofillCF(cmd.originCol, cmd.originRow, cmd.col, cmd.row);\n                this.autofillDV(cmd.originCol, cmd.originRow, cmd.col, cmd.row);\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getAutofillTooltip() {\n        return this.tooltip;\n    }\n    // ---------------------------------------------------------------------------\n    // Private methods\n    // ---------------------------------------------------------------------------\n    /**\n     * Autofill the autofillZone from the current selection\n     * @param apply Flag set to true to apply the autofill in the model. It's\n     *              useful to set it to false when we need to fill the tooltip\n     */\n    autofill(apply) {\n        if (!this.autofillZone || !this.steps || this.direction === undefined) {\n            this.tooltip = undefined;\n            return;\n        }\n        const source = this.getters.getSelectedZone();\n        const target = this.autofillZone;\n        switch (this.direction) {\n            case \"down\" /* DIRECTION.DOWN */:\n                for (let col = source.left; col <= source.right; col++) {\n                    const xcs = [];\n                    for (let row = source.top; row <= source.bottom; row++) {\n                        xcs.push(toXC(col, row));\n                    }\n                    const generator = this.createGenerator(xcs);\n                    for (let row = target.top; row <= target.bottom; row++) {\n                        this.computeNewCell(generator, col, row, apply);\n                    }\n                }\n                break;\n            case \"up\" /* DIRECTION.UP */:\n                for (let col = source.left; col <= source.right; col++) {\n                    const xcs = [];\n                    for (let row = source.bottom; row >= source.top; row--) {\n                        xcs.push(toXC(col, row));\n                    }\n                    const generator = this.createGenerator(xcs);\n                    for (let row = target.bottom; row >= target.top; row--) {\n                        this.computeNewCell(generator, col, row, apply);\n                    }\n                }\n                break;\n            case \"left\" /* DIRECTION.LEFT */:\n                for (let row = source.top; row <= source.bottom; row++) {\n                    const xcs = [];\n                    for (let col = source.right; col >= source.left; col--) {\n                        xcs.push(toXC(col, row));\n                    }\n                    const generator = this.createGenerator(xcs);\n                    for (let col = target.right; col >= target.left; col--) {\n                        this.computeNewCell(generator, col, row, apply);\n                    }\n                }\n                break;\n            case \"right\" /* DIRECTION.RIGHT */:\n                for (let row = source.top; row <= source.bottom; row++) {\n                    const xcs = [];\n                    for (let col = source.left; col <= source.right; col++) {\n                        xcs.push(toXC(col, row));\n                    }\n                    const generator = this.createGenerator(xcs);\n                    for (let col = target.left; col <= target.right; col++) {\n                        this.computeNewCell(generator, col, row, apply);\n                    }\n                }\n                break;\n        }\n        if (apply) {\n            this.autofillZone = undefined;\n            this.selection.resizeAnchorZone(this.direction, this.steps);\n            this.lastCellSelected = {};\n            this.direction = undefined;\n            this.steps = 0;\n            this.tooltip = undefined;\n        }\n    }\n    /**\n     * Select a cell which becomes the last cell of the autofillZone\n     */\n    select(col, row) {\n        const source = this.getters.getSelectedZone();\n        if (isInside(col, row, source)) {\n            this.autofillZone = undefined;\n            return;\n        }\n        this.direction = this.getDirection(col, row);\n        switch (this.direction) {\n            case \"up\" /* DIRECTION.UP */:\n                this.saveZone(row, source.top - 1, source.left, source.right);\n                this.steps = source.top - row;\n                break;\n            case \"down\" /* DIRECTION.DOWN */:\n                this.saveZone(source.bottom + 1, row, source.left, source.right);\n                this.steps = row - source.bottom;\n                break;\n            case \"left\" /* DIRECTION.LEFT */:\n                this.saveZone(source.top, source.bottom, col, source.left - 1);\n                this.steps = source.left - col;\n                break;\n            case \"right\" /* DIRECTION.RIGHT */:\n                this.saveZone(source.top, source.bottom, source.right + 1, col);\n                this.steps = col - source.right;\n                break;\n        }\n        this.autofill(false);\n    }\n    /**\n     * Computes the autofillZone to autofill when the user double click on the\n     * autofiller\n     */\n    autofillAuto() {\n        const activePosition = this.getters.getActivePosition();\n        const table = this.getters.getTable(activePosition);\n        let autofillRow = table ? table.range.zone.bottom : this.getAutofillAutoLastRow();\n        // Stop autofill at the next non-empty cell\n        const selection = this.getters.getSelectedZone();\n        for (let row = selection.bottom + 1; row <= autofillRow; row++) {\n            if (this.getters.getEvaluatedCell({ ...activePosition, row }).type !== CellValueType.empty) {\n                autofillRow = row - 1;\n                break;\n            }\n        }\n        if (autofillRow > selection.bottom) {\n            this.select(activePosition.col, autofillRow);\n            this.autofill(true);\n        }\n    }\n    getAutofillAutoLastRow() {\n        const zone = this.getters.getSelectedZone();\n        const sheetId = this.getters.getActiveSheetId();\n        let col = zone.left;\n        let row = zone.bottom;\n        if (col > 0) {\n            let leftPosition = { sheetId, col: col - 1, row };\n            while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty) {\n                row += 1;\n                leftPosition = { sheetId, col: col - 1, row };\n            }\n        }\n        if (row === zone.bottom) {\n            col = zone.right;\n            if (col <= this.getters.getNumberCols(sheetId)) {\n                let rightPosition = { sheetId, col: col + 1, row };\n                while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty) {\n                    row += 1;\n                    rightPosition = { sheetId, col: col + 1, row };\n                }\n            }\n        }\n        return row - 1;\n    }\n    /**\n     * Generate the next cell\n     */\n    computeNewCell(generator, col, row, apply) {\n        const { cellData, tooltip, origin } = generator.next();\n        const { content, style, border, format } = cellData;\n        this.tooltip = tooltip;\n        if (apply) {\n            this.dispatch(\"AUTOFILL_CELL\", {\n                originCol: origin.col,\n                originRow: origin.row,\n                col,\n                row,\n                content,\n                style,\n                border,\n                format,\n            });\n        }\n    }\n    /**\n     * Get the rule associated to the current cell\n     */\n    getRule(cell, cells) {\n        const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);\n        const rule = rules.find((rule) => rule.condition(cell, cells));\n        return rule && rule.generateRule(cell, cells);\n    }\n    /**\n     * Create the generator to be able to autofill the next cells.\n     */\n    createGenerator(source) {\n        const nextCells = [];\n        const cellsData = [];\n        const sheetId = this.getters.getActiveSheetId();\n        for (let xc of source) {\n            const { col, row } = toCartesian(xc);\n            const cell = this.getters.getCell({ sheetId, col, row });\n            cellsData.push({\n                col,\n                row,\n                cell,\n                sheetId,\n            });\n        }\n        const cells = cellsData.map((cellData) => cellData.cell);\n        for (let cellData of cellsData) {\n            let rule = { type: \"COPY_MODIFIER\" };\n            if (cellData && cellData.cell) {\n                const newRule = this.getRule(cellData.cell, cells);\n                rule = newRule || rule;\n            }\n            const border = this.getters.getCellBorder(cellData) || undefined;\n            nextCells.push({\n                data: { ...cellData, border },\n                rule,\n            });\n        }\n        return new AutofillGenerator(nextCells, this.getters, this.direction);\n    }\n    saveZone(top, bottom, left, right) {\n        this.autofillZone = { top, bottom, left, right };\n    }\n    /**\n     * Compute the direction of the autofill from the last selected zone and\n     * a given cell (col, row)\n     */\n    getDirection(col, row) {\n        const source = this.getters.getSelectedZone();\n        const position = {\n            up: { number: source.top - row, value: \"up\" /* DIRECTION.UP */ },\n            down: { number: row - source.bottom, value: \"down\" /* DIRECTION.DOWN */ },\n            left: { number: source.left - col, value: \"left\" /* DIRECTION.LEFT */ },\n            right: { number: col - source.right, value: \"right\" /* DIRECTION.RIGHT */ },\n        };\n        if (Object.values(position)\n            .map((x) => (x.number > 0 ? 1 : 0))\n            .reduce((acc, value) => acc + value) === 1) {\n            return Object.values(position).find((x) => (x.number > 0 ? 1 : 0)).value;\n        }\n        const first = position.up.number > 0 ? \"up\" : \"down\";\n        const second = position.left.number > 0 ? \"left\" : \"right\";\n        return Math.abs(position[first].number) >= Math.abs(position[second].number)\n            ? position[first].value\n            : position[second].value;\n    }\n    autoFillMerge(originCol, originRow, col, row) {\n        const sheetId = this.getters.getActiveSheetId();\n        const position = { sheetId, col, row };\n        const originPosition = { sheetId, col: originCol, row: originRow };\n        if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {\n            const zone = this.getters.getMerge(position);\n            if (zone) {\n                this.dispatch(\"REMOVE_MERGE\", {\n                    sheetId,\n                    target: [zone],\n                });\n            }\n        }\n        const originMerge = this.getters.getMerge(originPosition);\n        if (originMerge?.left === originCol && originMerge?.top === originRow) {\n            this.dispatch(\"ADD_MERGE\", {\n                sheetId,\n                target: [\n                    {\n                        top: row,\n                        bottom: row + originMerge.bottom - originMerge.top,\n                        left: col,\n                        right: col + originMerge.right - originMerge.left,\n                    },\n                ],\n            });\n        }\n    }\n    autofillCF(originCol, originRow, col, row) {\n        const sheetId = this.getters.getActiveSheetId();\n        const cfOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);\n        for (const cf of cfOrigin) {\n            const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, [positionToZone({ col, row })], []);\n            if (newCfRanges) {\n                this.dispatch(\"ADD_CONDITIONAL_FORMAT\", {\n                    cf: deepCopy(cf),\n                    ranges: newCfRanges,\n                    sheetId,\n                });\n            }\n        }\n    }\n    autofillDV(originCol, originRow, col, row) {\n        const sheetId = this.getters.getActiveSheetId();\n        const cellPosition = { sheetId, col: originCol, row: originRow };\n        const dvOrigin = this.getters.getValidationRuleForCell(cellPosition);\n        if (!dvOrigin) {\n            return;\n        }\n        const dvRangesZones = dvOrigin.ranges.map((range) => range.zone);\n        const newDvRanges = recomputeZones(dvRangesZones.concat(positionToZone({ col, row })), []);\n        this.dispatch(\"ADD_DATA_VALIDATION_RULE\", {\n            rule: dvOrigin,\n            ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),\n            sheetId,\n        });\n    }\n    // ---------------------------------------------------------------------------\n    // Grid rendering\n    // ---------------------------------------------------------------------------\n    drawLayer(renderingContext) {\n        if (!this.autofillZone) {\n            return;\n        }\n        const { ctx, thinLineWidth } = renderingContext;\n        const { x, y, width, height } = this.getters.getVisibleRect(this.autofillZone);\n        if (width > 0 && height > 0) {\n            ctx.strokeStyle = \"black\";\n            ctx.lineWidth = thinLineWidth;\n            ctx.setLineDash([3]);\n            ctx.strokeRect(x, y, width, height);\n            ctx.setLineDash([]);\n        }\n    }\n}\n\nclass AutomaticSumPlugin extends UIPlugin {\n    static getters = [\"getAutomaticSums\"];\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SUM_SELECTION\":\n                const sheetId = this.getters.getActiveSheetId();\n                const { zones, anchor } = this.getters.getSelection();\n                for (const zone of zones) {\n                    const sums = this.getAutomaticSums(sheetId, zone, anchor.cell);\n                    this.dispatchCellUpdates(sheetId, sums);\n                }\n                break;\n        }\n    }\n    getAutomaticSums(sheetId, zone, anchor) {\n        return this.shouldFindData(sheetId, zone)\n            ? this.sumAdjacentData(sheetId, zone, anchor)\n            : this.sumData(sheetId, zone);\n    }\n    // ---------------------------------------------------------------------------\n    // Private methods\n    // ---------------------------------------------------------------------------\n    sumData(sheetId, zone) {\n        const dimensions = this.dimensionsToSum(sheetId, zone);\n        const sums = this.sumDimensions(sheetId, zone, dimensions).filter(({ zone }) => !this.getters.isEmpty(sheetId, zone));\n        if (dimensions.has(\"ROW\") && dimensions.has(\"COL\")) {\n            sums.push(this.sumTotal(zone));\n        }\n        return sums;\n    }\n    sumAdjacentData(sheetId, zone, anchor) {\n        const { col, row } = isInside(anchor.col, anchor.row, zone)\n            ? anchor\n            : { col: zone.left, row: zone.top };\n        const dataZone = this.findAdjacentData(sheetId, col, row);\n        if (!dataZone) {\n            return [];\n        }\n        if (this.getters.isSingleCellOrMerge(sheetId, zone) ||\n            isOneDimensional(union(dataZone, zone))) {\n            return [{ position: { col, row }, zone: dataZone }];\n        }\n        else {\n            return this.sumDimensions(sheetId, union(dataZone, zone), this.transpose(this.dimensionsToSum(sheetId, zone)));\n        }\n    }\n    /**\n     * Find a zone to automatically sum a column or row of numbers.\n     *\n     * We first decide which direction will be summed (column or row).\n     * Here is the strategy:\n     *  1. If the left cell is a number and the top cell is not: choose horizontal\n     *  2. Try to find a valid vertical zone. If it's valid: choose vertical\n     *  3. Try to find a valid horizontal zone. If it's valid: choose horizontal\n     *  4. Otherwise, no zone is returned\n     *\n     * Now, how to find a valid zone?\n     * The zone starts directly above or on the left of the starting point\n     * (depending on the direction).\n     * The zone ends where the first continuous sequence of numbers ends.\n     * Empty or text cells can be part of the zone while no number has been found.\n     * Other kind of cells (boolean, dates, etc.) are not valid in the zone and the\n     * search stops immediately if one is found.\n     *\n     *  -------                                       -------\n     * |   1   |                                     |   1   |\n     *  -------                                       -------\n     * |       |                                     |       |\n     *  -------  <= end of the sequence, stop here    -------\n     * |   2   |                                     |   2   |\n     *  -------                                       -------\n     * |   3   | <= start of the number sequence     |   3   |\n     *  -------                                       -------\n     * |       | <= ignored                          | FALSE | <= invalid, no zone is found\n     *  -------                                       -------\n     * |   A   | <= ignored                          |   A   | <= ignored\n     *  -------                                       -------\n     */\n    findAdjacentData(sheetId, col, row) {\n        const sheet = this.getters.getSheet(sheetId);\n        const mainCellPosition = this.getters.getMainCellPosition({ sheetId, col, row });\n        const zone = this.findSuitableZoneToSum(sheet, mainCellPosition.col, mainCellPosition.row);\n        if (zone) {\n            return this.getters.expandZone(sheetId, zone);\n        }\n        return undefined;\n    }\n    /**\n     * Return the zone to sum if a valid one is found.\n     * @see getAutomaticSumZone\n     */\n    findSuitableZoneToSum(sheet, col, row) {\n        const topCell = this.getters.getEvaluatedCell({ sheetId: sheet.id, col, row: row - 1 });\n        const leftCell = this.getters.getEvaluatedCell({ sheetId: sheet.id, col: col - 1, row });\n        if (this.isNumber(leftCell) && !this.isNumber(topCell)) {\n            return this.findHorizontalZone(sheet, col, row);\n        }\n        const verticalZone = this.findVerticalZone(sheet, col, row);\n        if (this.isZoneValid(verticalZone)) {\n            return verticalZone;\n        }\n        const horizontalZone = this.findHorizontalZone(sheet, col, row);\n        if (this.isZoneValid(horizontalZone)) {\n            return horizontalZone;\n        }\n        return undefined;\n    }\n    findVerticalZone(sheet, col, row) {\n        const zone = {\n            top: 0,\n            bottom: row - 1,\n            left: col,\n            right: col,\n        };\n        const top = this.reduceZoneStart(sheet, zone, zone.bottom);\n        return { ...zone, top };\n    }\n    findHorizontalZone(sheet, col, row) {\n        const zone = {\n            top: row,\n            bottom: row,\n            left: 0,\n            right: col - 1,\n        };\n        const left = this.reduceZoneStart(sheet, zone, zone.right);\n        return { ...zone, left };\n    }\n    /**\n     * Reduces a column or row zone to a valid zone for the automatic sum.\n     * @see getAutomaticSumZone\n     * @param sheet\n     * @param zone one dimensional zone (a single row or a single column). The zone is\n     *             assumed to start at the beginning of the column (top=0) or the row (left=0)\n     * @param end end index of the zone (`bottom` or `right` depending on the dimension)\n     * @returns the starting position of the valid zone or Infinity if the zone is not valid.\n     */\n    reduceZoneStart(sheet, zone, end) {\n        const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);\n        const cellPositions = range(end, -1, -1);\n        const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);\n        const maxValidPosition = largeMax(invalidCells);\n        const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));\n        const firstSequence = numberSequences[0] || [];\n        if (largeMax(firstSequence) < maxValidPosition) {\n            return Infinity;\n        }\n        return largeMin(firstSequence);\n    }\n    shouldFindData(sheetId, zone) {\n        return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);\n    }\n    isNumber(cell) {\n        return cell.type === CellValueType.number && !(cell.format && isDateTimeFormat(cell.format));\n    }\n    isZoneValid(zone) {\n        return zone.bottom >= zone.top && zone.right >= zone.left;\n    }\n    lastColIsEmpty(sheetId, zone) {\n        return this.getters.isEmpty(sheetId, { ...zone, left: zone.right });\n    }\n    lastRowIsEmpty(sheetId, zone) {\n        return this.getters.isEmpty(sheetId, { ...zone, top: zone.bottom });\n    }\n    /**\n     * Decides which dimensions (columns or rows) should be summed\n     * based on its shape and what's inside the zone.\n     */\n    dimensionsToSum(sheetId, zone) {\n        const dimensions = new Set();\n        if (isOneDimensional(zone)) {\n            dimensions.add(zoneToDimension(zone).numberOfCols === 1 ? \"COL\" : \"ROW\");\n            return dimensions;\n        }\n        if (this.lastColIsEmpty(sheetId, zone)) {\n            dimensions.add(\"ROW\");\n        }\n        if (this.lastRowIsEmpty(sheetId, zone)) {\n            dimensions.add(\"COL\");\n        }\n        if (dimensions.size === 0) {\n            dimensions.add(\"COL\");\n        }\n        return dimensions;\n    }\n    /**\n     * Sum each column and/or row in the zone in the appropriate cells,\n     * depending on the available space.\n     */\n    sumDimensions(sheetId, zone, dimensions) {\n        return [\n            ...(dimensions.has(\"COL\") ? this.sumColumns(zone, sheetId) : []),\n            ...(dimensions.has(\"ROW\") ? this.sumRows(zone, sheetId) : []),\n        ];\n    }\n    /**\n     * Sum the total of the zone in the bottom right cell, assuming\n     * the last row contains summed columns.\n     */\n    sumTotal(zone) {\n        const { bottom, right } = zone;\n        return {\n            position: { col: right, row: bottom },\n            zone: { ...zone, top: bottom, right: right - 1 },\n        };\n    }\n    sumColumns(zone, sheetId) {\n        const target = this.nextEmptyRow(sheetId, { ...zone, bottom: zone.bottom - 1 });\n        zone = { ...zone, bottom: Math.min(zone.bottom, target.bottom - 1) };\n        return positions(target).map((position) => ({\n            position,\n            zone: { ...zone, right: position.col, left: position.col },\n        }));\n    }\n    sumRows(zone, sheetId) {\n        const target = this.nextEmptyCol(sheetId, { ...zone, right: zone.right - 1 });\n        zone = { ...zone, right: Math.min(zone.right, target.right - 1) };\n        return positions(target).map((position) => ({\n            position,\n            zone: { ...zone, top: position.row, bottom: position.row },\n        }));\n    }\n    dispatchCellUpdates(sheetId, sums) {\n        for (const sum of sums) {\n            const { col, row } = sum.position;\n            this.dispatch(\"UPDATE_CELL\", {\n                sheetId,\n                col,\n                row,\n                content: `=SUM(${this.getters.zoneToXC(sheetId, sum.zone)})`,\n            });\n        }\n    }\n    /**\n     * Find the first row where all cells below the zone are empty.\n     */\n    nextEmptyRow(sheetId, zone) {\n        let start = zone.bottom + 1;\n        const { left, right } = zone;\n        while (!this.getters.isEmpty(sheetId, { bottom: start, top: start, left, right })) {\n            start++;\n        }\n        return {\n            ...zone,\n            top: start,\n            bottom: start,\n        };\n    }\n    /**\n     * Find the first column where all cells right of the zone are empty.\n     */\n    nextEmptyCol(sheetId, zone) {\n        let start = zone.right + 1;\n        const { top, bottom } = zone;\n        while (!this.getters.isEmpty(sheetId, { left: start, right: start, top, bottom })) {\n            start++;\n        }\n        return {\n            ...zone,\n            left: start,\n            right: start,\n        };\n    }\n    /**\n     * Transpose the given dimensions.\n     * COL becomes ROW\n     * ROW becomes COL\n     */\n    transpose(dimensions) {\n        return new Set([...dimensions.values()].map((dimension) => (dimension === \"COL\" ? \"ROW\" : \"COL\")));\n    }\n}\n\n/*\n * This file contains the specifics transformations\n */\notRegistry.addTransformation(\"ADD_COLUMNS_ROWS\", [\"ADD_COLUMNS_ROWS\"], addHeadersTransformation);\notRegistry.addTransformation(\"REMOVE_COLUMNS_ROWS\", [\"ADD_COLUMNS_ROWS\"], addHeadersTransformation);\notRegistry.addTransformation(\"ADD_COLUMNS_ROWS\", [\"CREATE_CHART\", \"UPDATE_CHART\"], updateChartRangesTransformation);\notRegistry.addTransformation(\"REMOVE_COLUMNS_ROWS\", [\"CREATE_CHART\", \"UPDATE_CHART\"], updateChartRangesTransformation);\notRegistry.addTransformation(\"DELETE_SHEET\", [\"MOVE_RANGES\"], transformTargetSheetId);\notRegistry.addTransformation(\"DELETE_FIGURE\", [\"UPDATE_FIGURE\", \"UPDATE_CHART\"], updateChartFigure);\notRegistry.addTransformation(\"CREATE_SHEET\", [\"CREATE_SHEET\"], createSheetTransformation);\notRegistry.addTransformation(\"ADD_MERGE\", [\"ADD_MERGE\", \"REMOVE_MERGE\"], mergeTransformation);\notRegistry.addTransformation(\"ADD_COLUMNS_ROWS\", [\"FREEZE_COLUMNS\", \"FREEZE_ROWS\"], freezeTransformation);\notRegistry.addTransformation(\"REMOVE_COLUMNS_ROWS\", [\"FREEZE_COLUMNS\", \"FREEZE_ROWS\"], freezeTransformation);\notRegistry.addTransformation(\"ADD_COLUMNS_ROWS\", [\"UPDATE_TABLE\"], updateTableTransformation);\notRegistry.addTransformation(\"REMOVE_COLUMNS_ROWS\", [\"UPDATE_TABLE\"], updateTableTransformation);\notRegistry.addTransformation(\"REMOVE_TABLE_STYLE\", [\"CREATE_TABLE\", \"UPDATE_TABLE\"], removeTableStyleTransform);\notRegistry.addTransformation(\"ADD_COLUMNS_ROWS\", [\"GROUP_HEADERS\", \"UNGROUP_HEADERS\", \"FOLD_HEADER_GROUP\", \"UNFOLD_HEADER_GROUP\"], groupHeadersTransformation);\notRegistry.addTransformation(\"REMOVE_COLUMNS_ROWS\", [\"GROUP_HEADERS\", \"UNGROUP_HEADERS\", \"FOLD_HEADER_GROUP\", \"UNFOLD_HEADER_GROUP\"], groupHeadersTransformation);\notRegistry.addTransformation(\"REMOVE_PIVOT\", [\"RENAME_PIVOT\", \"DUPLICATE_PIVOT\", \"INSERT_PIVOT\", \"UPDATE_PIVOT\"], pivotRemovedTransformation);\notRegistry.addTransformation(\"DELETE_SHEET\", [\"ADD_PIVOT\", \"UPDATE_PIVOT\"], pivotDeletedSheetTransformation);\notRegistry.addTransformation(\"ADD_COLUMNS_ROWS\", [\"ADD_PIVOT\", \"UPDATE_PIVOT\"], pivotZoneTransformation);\notRegistry.addTransformation(\"REMOVE_COLUMNS_ROWS\", [\"ADD_PIVOT\", \"UPDATE_PIVOT\"], pivotZoneTransformation);\nfunction pivotZoneTransformation(toTransform, executed) {\n    if (toTransform.pivot.type !== \"SPREADSHEET\") {\n        return toTransform;\n    }\n    if (toTransform.pivot.dataSet?.sheetId !== executed.sheetId) {\n        return toTransform;\n    }\n    const newZone = transformZone(toTransform.pivot.dataSet.zone, executed);\n    const dataSet = newZone ? { ...toTransform.pivot.dataSet, zone: newZone } : undefined;\n    return { ...toTransform, pivot: { ...toTransform.pivot, dataSet } };\n}\nfunction pivotDeletedSheetTransformation(toTransform, executed) {\n    if (toTransform.pivot.type !== \"SPREADSHEET\") {\n        return toTransform;\n    }\n    if (toTransform.pivot.dataSet?.sheetId === executed.sheetId) {\n        return { ...toTransform, pivot: { ...toTransform.pivot, dataSet: undefined } };\n    }\n    return toTransform;\n}\nfunction pivotRemovedTransformation(toTransform, executed) {\n    if (toTransform.pivotId === executed.pivotId) {\n        return undefined;\n    }\n    return toTransform;\n}\nfunction transformTargetSheetId(toTransform, executed) {\n    const deletedSheetId = executed.sheetId;\n    if (toTransform.targetSheetId === deletedSheetId || toTransform.sheetId === deletedSheetId) {\n        return undefined;\n    }\n    return toTransform;\n}\nfunction updateChartFigure(toTransform, executed) {\n    if (toTransform.id === executed.id) {\n        return undefined;\n    }\n    return toTransform;\n}\nfunction updateChartRangesTransformation(toTransform, executed) {\n    return {\n        ...toTransform,\n        definition: transformDefinition(toTransform.definition, executed),\n    };\n}\nfunction createSheetTransformation(toTransform, executed) {\n    if (toTransform.name === executed.name) {\n        return {\n            ...toTransform,\n            name: toTransform.name?.match(/\\d+/)\n                ? toTransform.name.replace(/\\d+/, (n) => (parseInt(n) + 1).toString())\n                : `${toTransform.name}~`,\n            position: toTransform.position + 1,\n        };\n    }\n    return toTransform;\n}\nfunction mergeTransformation(toTransform, executed) {\n    if (toTransform.sheetId !== executed.sheetId) {\n        return toTransform;\n    }\n    const target = [];\n    for (const zone1 of toTransform.target) {\n        for (const zone2 of executed.target) {\n            if (!overlap(zone1, zone2)) {\n                target.push({ ...zone1 });\n            }\n        }\n    }\n    if (target.length) {\n        return { ...toTransform, target };\n    }\n    return undefined;\n}\nfunction freezeTransformation(toTransform, executed) {\n    if (toTransform.sheetId !== executed.sheetId) {\n        return toTransform;\n    }\n    const dimension = toTransform.type === \"FREEZE_COLUMNS\" ? \"COL\" : \"ROW\";\n    if (dimension !== executed.dimension) {\n        return toTransform;\n    }\n    let quantity = toTransform[\"quantity\"];\n    if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n        const executedElements = [...executed.elements].sort((a, b) => b - a);\n        for (let removedElement of executedElements) {\n            if (quantity > removedElement) {\n                quantity--;\n            }\n        }\n    }\n    if (executed.type === \"ADD_COLUMNS_ROWS\") {\n        const executedBase = executed.position === \"before\" ? executed.base - 1 : executed.base;\n        quantity = quantity > executedBase ? quantity + executed.quantity : quantity;\n    }\n    return quantity > 0 ? { ...toTransform, quantity } : undefined;\n}\n/**\n * Update the zones of an UPDATE_TABLE command if some headers were added/removed\n */\nfunction updateTableTransformation(toTransform, executed) {\n    if (toTransform.sheetId !== executed.sheetId) {\n        return toTransform;\n    }\n    const newCmdZone = transformZone(toTransform.zone, executed);\n    if (!newCmdZone) {\n        return undefined;\n    }\n    const newTableRange = toTransform.newTableRange\n        ? transformRangeData(toTransform.newTableRange, executed)\n        : undefined;\n    return { ...toTransform, newTableRange, zone: newCmdZone };\n}\nfunction removeTableStyleTransform(toTransform, executed) {\n    if (toTransform.config?.styleId !== executed.tableStyleId) {\n        return toTransform;\n    }\n    return {\n        ...toTransform,\n        config: { ...toTransform.config, styleId: DEFAULT_TABLE_CONFIG.styleId },\n    };\n}\n/**\n * Transform ADD_COLUMNS_ROWS command if some headers were added/removed\n */\nfunction addHeadersTransformation(toTransform, executed) {\n    if (toTransform.sheetId !== executed.sheetId || toTransform.dimension !== executed.dimension) {\n        return toTransform;\n    }\n    let result = undefined;\n    if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n        result = moveHeaderIndexesOnHeaderDeletion(executed.elements, [toTransform.base])[0];\n    }\n    else if (executed.type === \"ADD_COLUMNS_ROWS\") {\n        const base = getAddHeaderStartIndex(executed.position, executed.base);\n        result = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, [toTransform.base])[0];\n    }\n    if (result === undefined) {\n        return undefined;\n    }\n    return { ...toTransform, base: result };\n}\n/**\n * Transform header group command if some headers were added/removed\n */\nfunction groupHeadersTransformation(toTransform, executed) {\n    if (toTransform.sheetId !== executed.sheetId || toTransform.dimension !== executed.dimension) {\n        return toTransform;\n    }\n    const elementsToTransform = range(toTransform.start, toTransform.end + 1);\n    let results = [];\n    if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n        results = moveHeaderIndexesOnHeaderDeletion(executed.elements, elementsToTransform);\n    }\n    else if (executed.type === \"ADD_COLUMNS_ROWS\") {\n        const base = getAddHeaderStartIndex(executed.position, executed.base);\n        results = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, elementsToTransform);\n    }\n    if (results.length === 0) {\n        return undefined;\n    }\n    return { ...toTransform, start: Math.min(...results), end: Math.max(...results) };\n}\n\nconst transformations = [\n    { match: isSheetDependent, fn: transformSheetId },\n    { match: isTargetDependent, fn: transformTarget },\n    { match: isZoneDependent, fn: transformZoneDependentCommand },\n    { match: isPositionDependent, fn: transformPosition },\n    { match: isHeadersDependant, fn: transformHeaders },\n    { match: isRangeDependant, fn: transformRangesDependentCommand },\n];\n/**\n * Get the result of applying the operation transformations on the given command\n * to transform based on the executed command.\n * Let's see a small example:\n * Given\n *  - command A: set the content of C1 to \"Hello\"\n *  - command B: add a column after A\n *\n * If command B has been executed locally and not transmitted (yet) to\n * other clients, and command A arrives from an other client to be executed locally.\n * Command A is no longer valid and no longer reflects the user intention.\n * It needs to be transformed knowing that command B is already executed.\n * transform(A, B) => set the content of D1 to \"Hello\"\n */\nfunction transform(toTransform, executed) {\n    const specificTransform = otRegistry.getTransformation(toTransform.type, executed.type);\n    return specificTransform\n        ? specificTransform(toTransform, executed)\n        : genericTransform(toTransform, executed);\n}\n/**\n * Get the result of applying the operation transformations on all the given\n * commands to transform for each executed commands.\n */\nfunction transformAll(toTransform, executed) {\n    let transformedCommands = [...toTransform];\n    for (const executedCommand of executed) {\n        transformedCommands = transformedCommands\n            .map((cmd) => transform(cmd, executedCommand))\n            .filter(isDefined);\n    }\n    return transformedCommands;\n}\n/**\n * Apply all generic transformation based on the characteristic of the given commands.\n */\nfunction genericTransform(cmd, executed) {\n    for (const { match, fn } of transformations) {\n        if (match(cmd)) {\n            const result = fn(cmd, executed);\n            if (result === \"SKIP_TRANSFORMATION\") {\n                continue;\n            }\n            if (result === \"IGNORE_COMMAND\") {\n                return undefined;\n            }\n            cmd = result;\n        }\n    }\n    return cmd;\n}\nfunction transformSheetId(toTransform, executed) {\n    if (!(\"sheetId\" in executed)) {\n        return toTransform;\n    }\n    const deleteSheet = executed.type === \"DELETE_SHEET\" && executed.sheetId;\n    if (toTransform.sheetId === deleteSheet) {\n        return \"IGNORE_COMMAND\";\n    }\n    else if (toTransform.type === \"CREATE_SHEET\" ||\n        executed.type === \"CREATE_SHEET\" ||\n        toTransform.sheetId !== executed.sheetId) {\n        return toTransform;\n    }\n    return \"SKIP_TRANSFORMATION\";\n}\nfunction transformTarget(cmd, executed) {\n    const transformSheetResult = transformSheetId(cmd, executed);\n    if (transformSheetResult !== \"SKIP_TRANSFORMATION\") {\n        return transformSheetResult === \"IGNORE_COMMAND\" ? \"IGNORE_COMMAND\" : cmd;\n    }\n    const target = [];\n    for (const zone of cmd.target) {\n        const newZone = transformZone(zone, executed);\n        if (newZone) {\n            target.push(newZone);\n        }\n    }\n    if (!target.length) {\n        return \"IGNORE_COMMAND\";\n    }\n    return { ...cmd, target };\n}\nfunction transformZoneDependentCommand(cmd, executed) {\n    const transformSheetResult = transformSheetId(cmd, executed);\n    if (transformSheetResult !== \"SKIP_TRANSFORMATION\") {\n        return transformSheetResult === \"IGNORE_COMMAND\" ? \"IGNORE_COMMAND\" : cmd;\n    }\n    const newZone = transformZone(cmd.zone, executed);\n    if (newZone) {\n        return { ...cmd, zone: newZone };\n    }\n    return \"IGNORE_COMMAND\";\n}\nfunction transformRangesDependentCommand(toTransform, executed) {\n    if (!(\"sheetId\" in executed)) {\n        return toTransform;\n    }\n    const ranges = toTransform.ranges\n        .map((range) => transformRangeData(range, executed))\n        .filter(isDefined);\n    if (!ranges.length) {\n        return \"IGNORE_COMMAND\";\n    }\n    return { ...toTransform, ranges };\n}\nfunction transformHeaders(toTransform, executed) {\n    const transformSheetResult = transformSheetId(toTransform, executed);\n    if (transformSheetResult !== \"SKIP_TRANSFORMATION\") {\n        return transformSheetResult === \"IGNORE_COMMAND\" ? \"IGNORE_COMMAND\" : toTransform;\n    }\n    if (executed.type !== \"ADD_COLUMNS_ROWS\" && executed.type !== \"REMOVE_COLUMNS_ROWS\") {\n        return \"SKIP_TRANSFORMATION\";\n    }\n    if (executed.dimension !== toTransform.dimension) {\n        return toTransform;\n    }\n    let result = [];\n    if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n        result = moveHeaderIndexesOnHeaderDeletion(executed.elements, toTransform.elements);\n    }\n    else if (executed.type === \"ADD_COLUMNS_ROWS\") {\n        const base = getAddHeaderStartIndex(executed.position, executed.base);\n        result = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, toTransform.elements);\n    }\n    if (result.length === 0) {\n        return \"IGNORE_COMMAND\";\n    }\n    return { ...toTransform, elements: result };\n}\n/**\n * Transform a PositionDependentCommand. It could be impacted by a grid command\n * (Add/remove cols/rows) and a merge\n */\nfunction transformPosition(toTransform, executed) {\n    const transformSheetResult = transformSheetId(toTransform, executed);\n    if (transformSheetResult !== \"SKIP_TRANSFORMATION\") {\n        return transformSheetResult === \"IGNORE_COMMAND\" ? \"IGNORE_COMMAND\" : toTransform;\n    }\n    if (executed.type === \"ADD_COLUMNS_ROWS\" || executed.type === \"REMOVE_COLUMNS_ROWS\") {\n        return transformPositionWithGrid(toTransform, executed);\n    }\n    if (executed.type === \"ADD_MERGE\") {\n        return transformPositionWithMerge(toTransform, executed);\n    }\n    return \"SKIP_TRANSFORMATION\";\n}\n/**\n * Transform a PositionDependentCommand after a grid shape modification. This\n * transformation consists of updating the position.\n */\nfunction transformPositionWithGrid(toTransform, executed) {\n    const field = executed.dimension === \"COL\" ? \"col\" : \"row\";\n    let base = toTransform[field];\n    if (executed.type === \"REMOVE_COLUMNS_ROWS\") {\n        const elements = [...executed.elements].sort((a, b) => b - a);\n        if (elements.includes(base)) {\n            return \"IGNORE_COMMAND\";\n        }\n        for (let removedElement of elements) {\n            if (base >= removedElement) {\n                base--;\n            }\n        }\n    }\n    if (executed.type === \"ADD_COLUMNS_ROWS\") {\n        if (base > executed.base || (base === executed.base && executed.position === \"before\")) {\n            base = base + executed.quantity;\n        }\n    }\n    return { ...toTransform, [field]: base };\n}\n/**\n * Transform a PositionDependentCommand after a merge. This transformation\n * consists of checking that the position is not inside the merged zones\n */\nfunction transformPositionWithMerge(toTransform, executed) {\n    for (const zone of executed.target) {\n        const sameTopLeft = toTransform.col === zone.left && toTransform.row === zone.top;\n        if (!sameTopLeft && isInside(toTransform.col, toTransform.row, zone)) {\n            return \"IGNORE_COMMAND\";\n        }\n    }\n    return toTransform;\n}\n\nclass Revision {\n    rootCommand;\n    timestamp;\n    id;\n    clientId;\n    _commands = [];\n    _changes = [];\n    /**\n     * A revision represents a whole client action (Create a sheet, merge a Zone, Undo, ...).\n     * A revision contains the following information:\n     *  - id: ID of the revision\n     *  - commands: CoreCommands that are linked to the action, and should be\n     *              dispatched in other clients\n     *  - clientId: Client who initiated the action\n     *  - changes: List of changes applied on the state.\n     */\n    constructor(id, clientId, commands, rootCommand, changes, timestamp) {\n        this.rootCommand = rootCommand;\n        this.timestamp = timestamp;\n        this.id = id;\n        this.clientId = clientId;\n        this._commands = [...commands];\n        this._changes = changes ? [...changes] : [];\n    }\n    setChanges(changes) {\n        this._changes = changes;\n    }\n    get commands() {\n        return this._commands;\n    }\n    get changes() {\n        return this._changes;\n    }\n}\n\nclass ClientDisconnectedError extends Error {\n}\nclass Session extends EventBus {\n    revisions;\n    transportService;\n    serverRevisionId;\n    /**\n     * Positions of the others client.\n     */\n    clients = {};\n    clientId = \"local\";\n    /**\n     * Id of the server revision\n     */\n    debouncedMove;\n    pendingMessages = [];\n    waitingAck = false;\n    /**\n     * Flag used to block all commands when an undo or redo is triggered, until\n     * it is accepted on the server\n     */\n    waitingUndoRedoAck = false;\n    isReplayingInitialRevisions = false;\n    processedRevisions = new Set();\n    uuidGenerator = new UuidGenerator();\n    lastLocalOperation;\n    /**\n     * Manages the collaboration between multiple users on the same spreadsheet.\n     * It can forward local state changes to other users to ensure they all eventually\n     * reach the same state.\n     * It also manages the positions of each clients in the spreadsheet to provide\n     * a visual indication of what other users are doing in the spreadsheet.\n     *\n     * @param revisions\n     * @param transportService communication channel used to send and receive messages\n     * between all connected clients\n     * @param client the client connected locally\n     * @param serverRevisionId\n     */\n    constructor(revisions, transportService, serverRevisionId = DEFAULT_REVISION_ID) {\n        super();\n        this.revisions = revisions;\n        this.transportService = transportService;\n        this.serverRevisionId = serverRevisionId;\n        this.debouncedMove = debounce(this._move.bind(this), DEBOUNCE_TIME);\n    }\n    canApplyOptimisticUpdate() {\n        return !this.waitingUndoRedoAck;\n    }\n    /**\n     * Add a new revision to the collaborative session.\n     * It will be transmitted to all other connected clients.\n     */\n    save(rootCommand, commands, changes) {\n        if (!commands.length || !changes.length || !this.canApplyOptimisticUpdate())\n            return;\n        const revision = new Revision(this.uuidGenerator.uuidv4(), this.clientId, commands, rootCommand, changes, Date.now());\n        this.revisions.append(revision.id, revision);\n        // REQUEST_REDO just repeats the last operation, the\n        // last operation is still the same and should not change.\n        if (rootCommand.type !== \"REQUEST_REDO\") {\n            this.lastLocalOperation = revision;\n        }\n        this.trigger(\"new-local-state-update\", { id: revision.id });\n        this.sendUpdateMessage({\n            type: \"REMOTE_REVISION\",\n            version: MESSAGE_VERSION,\n            serverRevisionId: this.serverRevisionId,\n            nextRevisionId: revision.id,\n            clientId: revision.clientId,\n            commands: revision.commands,\n        });\n    }\n    undo(revisionId) {\n        this.waitingUndoRedoAck = true;\n        this.sendUpdateMessage({\n            type: \"REVISION_UNDONE\",\n            version: MESSAGE_VERSION,\n            serverRevisionId: this.serverRevisionId,\n            nextRevisionId: this.uuidGenerator.uuidv4(),\n            undoneRevisionId: revisionId,\n        });\n    }\n    redo(revisionId) {\n        this.waitingUndoRedoAck = true;\n        this.sendUpdateMessage({\n            type: \"REVISION_REDONE\",\n            version: MESSAGE_VERSION,\n            serverRevisionId: this.serverRevisionId,\n            nextRevisionId: this.uuidGenerator.uuidv4(),\n            redoneRevisionId: revisionId,\n        });\n    }\n    /**\n     * Notify that the position of the client has changed\n     */\n    move(position) {\n        this.debouncedMove(position);\n    }\n    join(client) {\n        if (client) {\n            this.clients[client.id] = client;\n            this.clientId = client.id;\n        }\n        else {\n            this.clients[\"local\"] = { id: \"local\", name: \"local\" };\n            this.clientId = \"local\";\n        }\n        this.transportService.onNewMessage(this.clientId, this.onMessageReceived.bind(this));\n    }\n    loadInitialMessages(messages) {\n        const start = performance.now();\n        const numberOfCommands = messages.reduce((acc, message) => acc + (message.type === \"REMOTE_REVISION\" ? message.commands.length : 1), 0);\n        this.isReplayingInitialRevisions = true;\n        for (const message of messages) {\n            this.onMessageReceived(message);\n        }\n        this.isReplayingInitialRevisions = false;\n        console.debug(\"Replayed\", numberOfCommands, \"commands in\", performance.now() - start, \"ms\");\n    }\n    /**\n     * Notify the server that the user client left the collaborative session\n     */\n    async leave(data) {\n        if (data && Object.keys(this.clients).length === 1 && this.processedRevisions.size) {\n            await this.snapshot(data());\n        }\n        delete this.clients[this.clientId];\n        this.transportService.leave(this.clientId);\n        this.transportService.sendMessage({\n            type: \"CLIENT_LEFT\",\n            clientId: this.clientId,\n            version: MESSAGE_VERSION,\n        });\n    }\n    /**\n     * Send a snapshot of the spreadsheet to the collaboration server\n     */\n    async snapshot(data) {\n        if (this.pendingMessages.length !== 0) {\n            return;\n        }\n        const snapshotId = this.uuidGenerator.uuidv4();\n        await this.transportService.sendMessage({\n            type: \"SNAPSHOT\",\n            nextRevisionId: snapshotId,\n            serverRevisionId: this.serverRevisionId,\n            data: { ...data, revisionId: snapshotId },\n            version: MESSAGE_VERSION,\n        });\n    }\n    getClient() {\n        const client = this.clients[this.clientId];\n        if (!client) {\n            throw new ClientDisconnectedError(\"The client left the session\");\n        }\n        return client;\n    }\n    getConnectedClients() {\n        return new Set(Object.values(this.clients).filter(isDefined));\n    }\n    getRevisionId() {\n        return this.serverRevisionId;\n    }\n    isFullySynchronized() {\n        return this.pendingMessages.length === 0;\n    }\n    /**\n     * Get the last local revision\n     * */\n    getLastLocalNonEmptyRevision() {\n        return this.lastLocalOperation;\n    }\n    _move(position) {\n        // this method is debounced and might be called after the client\n        // left the session.\n        if (!this.clients[this.clientId])\n            return;\n        const currentPosition = this.clients[this.clientId]?.position;\n        if (currentPosition?.col === position.col &&\n            currentPosition.row === position.row &&\n            currentPosition.sheetId === position.sheetId) {\n            return;\n        }\n        const type = currentPosition ? \"CLIENT_MOVED\" : \"CLIENT_JOINED\";\n        const client = this.getClient();\n        this.clients[this.clientId] = { ...client, position };\n        this.transportService.sendMessage({\n            type,\n            version: MESSAGE_VERSION,\n            client: { ...client, position },\n        });\n    }\n    /**\n     * Handles messages received from other clients in the collaborative\n     * session.\n     */\n    onMessageReceived(message) {\n        if (this.isAlreadyProcessed(message))\n            return;\n        if (this.isWrongServerRevisionId(message)) {\n            this.trigger(\"unexpected-revision-id\");\n            return;\n        }\n        switch (message.type) {\n            case \"CLIENT_MOVED\":\n                this.onClientMoved(message);\n                break;\n            case \"CLIENT_JOINED\":\n                this.onClientJoined(message);\n                break;\n            case \"CLIENT_LEFT\":\n                this.onClientLeft(message);\n                break;\n            case \"REVISION_REDONE\": {\n                this.revisions.redo(message.redoneRevisionId, message.nextRevisionId, message.serverRevisionId);\n                this.trigger(\"revision-redone\", {\n                    revisionId: message.redoneRevisionId,\n                    commands: this.revisions.get(message.redoneRevisionId).commands,\n                });\n                break;\n            }\n            case \"REVISION_UNDONE\":\n                this.revisions.undo(message.undoneRevisionId, message.nextRevisionId, message.serverRevisionId);\n                this.trigger(\"revision-undone\", {\n                    revisionId: message.undoneRevisionId,\n                    commands: this.revisions.get(message.undoneRevisionId).commands,\n                });\n                break;\n            case \"REMOTE_REVISION\":\n                const { clientId, commands, timestamp } = message;\n                const revision = new Revision(message.nextRevisionId, clientId, commands, undefined, undefined, timestamp);\n                if (revision.clientId !== this.clientId) {\n                    this.revisions.insert(revision.id, revision, message.serverRevisionId);\n                    const pendingCommands = this.pendingMessages\n                        .filter((msg) => msg.type === \"REMOTE_REVISION\")\n                        .map((msg) => msg.commands)\n                        .flat();\n                    this.trigger(\"remote-revision-received\", {\n                        commands: transformAll(commands, pendingCommands),\n                    });\n                }\n                break;\n            case \"SNAPSHOT_CREATED\": {\n                const revision = new Revision(message.nextRevisionId, \"server\", [], undefined, undefined, Date.now());\n                this.revisions.insert(revision.id, revision, message.serverRevisionId);\n                this.dropPendingHistoryMessages();\n                this.trigger(\"snapshot\");\n                this.lastLocalOperation = undefined;\n                break;\n            }\n        }\n        this.acknowledge(message);\n        this.trigger(\"collaborative-event-received\");\n    }\n    onClientMoved(message) {\n        if (message.client.id !== this.clientId) {\n            this.clients[message.client.id] = message.client;\n        }\n    }\n    /**\n     * Register the new client and send your\n     * own position back.\n     */\n    onClientJoined(message) {\n        if (message.client.id !== this.clientId) {\n            this.clients[message.client.id] = message.client;\n            const client = this.clients[this.clientId];\n            if (client) {\n                const { position } = client;\n                if (position) {\n                    this.transportService.sendMessage({\n                        type: \"CLIENT_MOVED\",\n                        version: MESSAGE_VERSION,\n                        client: { ...client, position },\n                    });\n                }\n            }\n        }\n    }\n    onClientLeft(message) {\n        if (message.clientId !== this.clientId) {\n            delete this.clients[message.clientId];\n        }\n    }\n    sendUpdateMessage(message) {\n        this.pendingMessages.push(message);\n        if (this.waitingAck) {\n            return;\n        }\n        this.waitingAck = true;\n        this.sendPendingMessage();\n    }\n    /**\n     * Send the next pending message\n     */\n    sendPendingMessage() {\n        let message = this.pendingMessages[0];\n        if (!message)\n            return;\n        if (message.type === \"REMOTE_REVISION\") {\n            const revision = this.revisions.get(message.nextRevisionId);\n            if (revision.commands.length === 0) {\n                /**\n                 * The command is empty, we have to drop all the next local revisions\n                 * to avoid issues with undo/redo\n                 */\n                this.revisions.drop(revision.id);\n                const revisionIds = this.pendingMessages\n                    .filter((message) => message.type === \"REMOTE_REVISION\")\n                    .map((message) => message.nextRevisionId);\n                this.trigger(\"pending-revisions-dropped\", { revisionIds });\n                this.waitingAck = false;\n                this.waitingUndoRedoAck = false;\n                this.pendingMessages = [];\n                return;\n            }\n            message = {\n                ...message,\n                clientId: revision.clientId,\n                commands: revision.commands,\n            };\n        }\n        if (this.isReplayingInitialRevisions) {\n            throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.\n      ${JSON.stringify(message)}`);\n        }\n        this.transportService.sendMessage({\n            ...message,\n            serverRevisionId: this.serverRevisionId,\n        });\n    }\n    acknowledge(message) {\n        if (message.type === \"REVISION_UNDONE\" || message.type === \"REVISION_REDONE\") {\n            this.waitingUndoRedoAck = false;\n        }\n        switch (message.type) {\n            case \"REMOTE_REVISION\":\n            case \"REVISION_REDONE\":\n            case \"REVISION_UNDONE\":\n            case \"SNAPSHOT_CREATED\":\n                this.waitingAck = false;\n                this.pendingMessages = this.pendingMessages.filter((msg) => msg.nextRevisionId !== message.nextRevisionId);\n                this.serverRevisionId = message.nextRevisionId;\n                this.processedRevisions.add(message.nextRevisionId);\n                this.sendPendingMessage();\n                break;\n        }\n    }\n    isAlreadyProcessed(message) {\n        if (message.type === \"CLIENT_MOVED\" && message.client.id === this.clientId) {\n            return true;\n        }\n        switch (message.type) {\n            case \"REMOTE_REVISION\":\n            case \"REVISION_REDONE\":\n            case \"REVISION_UNDONE\":\n            case \"SNAPSHOT_CREATED\":\n                return this.processedRevisions.has(message.nextRevisionId);\n            default:\n                return false;\n        }\n    }\n    isWrongServerRevisionId(message) {\n        switch (message.type) {\n            case \"REMOTE_REVISION\":\n            case \"REVISION_REDONE\":\n            case \"REVISION_UNDONE\":\n            case \"SNAPSHOT_CREATED\":\n                return message.serverRevisionId !== this.serverRevisionId;\n            default:\n                return false;\n        }\n    }\n    dropPendingHistoryMessages() {\n        this.waitingUndoRedoAck = false;\n        this.pendingMessages = this.pendingMessages.filter(({ type }) => type !== \"REVISION_REDONE\" && type !== \"REVISION_UNDONE\");\n    }\n}\n\nfunction randomChoice(arr) {\n    return arr[Math.floor(Math.random() * arr.length)];\n}\nconst colors = [\n    \"#ff851b\",\n    \"#0074d9\",\n    \"#7fdbff\",\n    \"#b10dc9\",\n    \"#39cccc\",\n    \"#f012be\",\n    \"#3d9970\",\n    \"#111111\",\n    \"#ff4136\",\n    \"#aaaaaa\",\n    \"#85144b\",\n    \"#001f3f\",\n];\nclass CollaborativePlugin extends UIPlugin {\n    static getters = [\n        \"getClientsToDisplay\",\n        \"getClient\",\n        \"getConnectedClients\",\n        \"isFullySynchronized\",\n    ];\n    static layers = [\"Selection\"];\n    availableColors = new Set(colors);\n    colors = {};\n    session;\n    constructor(config) {\n        super(config);\n        this.session = config.session;\n    }\n    isPositionValid(position) {\n        return (position.row < this.getters.getNumberRows(position.sheetId) &&\n            position.col < this.getters.getNumberCols(position.sheetId));\n    }\n    chooseNewColor() {\n        if (this.availableColors.size === 0) {\n            this.availableColors = new Set(colors);\n        }\n        const color = randomChoice([...this.availableColors.values()]);\n        this.availableColors.delete(color);\n        return color;\n    }\n    getClient() {\n        return this.session.getClient();\n    }\n    getConnectedClients() {\n        return this.session.getConnectedClients();\n    }\n    isFullySynchronized() {\n        return this.session.isFullySynchronized();\n    }\n    /**\n     * Get the list of others connected clients which are present in the same sheet\n     * and with a valid position\n     */\n    getClientsToDisplay() {\n        try {\n            this.getters.getClient();\n        }\n        catch (e) {\n            if (e instanceof ClientDisconnectedError) {\n                return [];\n            }\n            else {\n                throw e;\n            }\n        }\n        const sheetId = this.getters.getActiveSheetId();\n        const clients = [];\n        for (const client of this.getters.getConnectedClients()) {\n            if (client.id !== this.getters.getClient().id &&\n                client.position &&\n                client.position.sheetId === sheetId &&\n                this.isPositionValid(client.position)) {\n                const position = client.position;\n                if (!this.colors[client.id]) {\n                    this.colors[client.id] = this.chooseNewColor();\n                }\n                const color = this.colors[client.id];\n                clients.push({ ...client, position, color });\n            }\n        }\n        return clients;\n    }\n    drawLayer(renderingContext) {\n        if (this.getters.isDashboard()) {\n            return;\n        }\n        const { ctx, thinLineWidth } = renderingContext;\n        const activeSheetId = this.getters.getActiveSheetId();\n        for (const client of this.getClientsToDisplay()) {\n            const { row, col } = client.position;\n            const zone = this.getters.expandZone(activeSheetId, {\n                top: row,\n                bottom: row,\n                left: col,\n                right: col,\n            });\n            const { x, y, width, height } = this.getters.getVisibleRect(zone);\n            if (width <= 0 || height <= 0) {\n                continue;\n            }\n            const color = client.color;\n            /* Cell background */\n            const cellBackgroundColor = `${color}10`;\n            ctx.fillStyle = cellBackgroundColor;\n            ctx.lineWidth = 4 * thinLineWidth;\n            ctx.strokeStyle = color;\n            ctx.globalCompositeOperation = \"multiply\";\n            ctx.fillRect(x, y, width, height);\n            /* Cell border */\n            ctx.globalCompositeOperation = \"source-over\";\n            ctx.strokeRect(x, y, width, height);\n            /* client name background */\n            ctx.font = `bold ${DEFAULT_FONT_SIZE + 1}px ${DEFAULT_FONT}`;\n        }\n    }\n}\n\nclass DataCleanupPlugin extends UIPlugin {\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"REMOVE_DUPLICATES\":\n                return this.checkValidations(cmd, this.chainValidations(this.checkSingleRangeSelected, this.checkNoMergeInZone, this.checkRangeContainsValues, this.checkColumnsIncludedInZone), this.chainValidations(this.checkNoColumnProvided, this.checkColumnsAreUnique));\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"REMOVE_DUPLICATES\":\n                this.removeDuplicates(cmd.columns, cmd.hasHeader);\n                break;\n            case \"TRIM_WHITESPACE\":\n                this.trimWhitespace();\n                break;\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    removeDuplicates(columnsToAnalyze, hasHeader) {\n        const sheetId = this.getters.getActiveSheetId();\n        const zone = this.getters.getSelectedZone();\n        if (hasHeader) {\n            zone.top += 1;\n        }\n        const uniqueRowsIndexes = this.getUniqueRowsIndexes(sheetId, zone.top, zone.bottom, columnsToAnalyze);\n        const numberOfUniqueRows = uniqueRowsIndexes.length;\n        if (numberOfUniqueRows === zoneToDimension(zone).numberOfRows) {\n            this.notifyRowsRemovedAndRemaining(0, numberOfUniqueRows);\n            return;\n        }\n        const rowsToKeep = uniqueRowsIndexes.map((rowIndex) => ({\n            left: zone.left,\n            top: rowIndex,\n            right: zone.right,\n            bottom: rowIndex,\n        }));\n        const handler = new CellClipboardHandler(this.getters, this.dispatch);\n        const data = handler.copy(getClipboardDataPositions(sheetId, rowsToKeep));\n        if (!data) {\n            return;\n        }\n        this.dispatch(\"CLEAR_CELLS\", { target: [zone], sheetId });\n        const zonePasted = {\n            left: zone.left,\n            top: zone.top,\n            right: zone.left,\n            bottom: zone.top,\n        };\n        handler.paste({ zones: [zonePasted], sheetId }, data, { isCutOperation: false });\n        const remainingZone = {\n            left: zone.left,\n            top: zone.top - (hasHeader ? 1 : 0),\n            right: zone.right,\n            bottom: zone.top + numberOfUniqueRows - 1,\n        };\n        this.selection.selectZone({\n            cell: { col: remainingZone.left, row: remainingZone.top },\n            zone: remainingZone,\n        });\n        const removedRows = zone.bottom - zone.top + 1 - numberOfUniqueRows;\n        this.notifyRowsRemovedAndRemaining(removedRows, numberOfUniqueRows);\n    }\n    getUniqueRowsIndexes(sheetId, top, bottom, columns) {\n        const uniqueRows = new Map();\n        for (const row of range(top, bottom + 1)) {\n            const cellsValuesInRow = columns.map((col) => {\n                return this.getters.getEvaluatedCell({\n                    sheetId,\n                    col,\n                    row,\n                }).value;\n            });\n            const isRowUnique = !Object.values(uniqueRows).some((uniqueRow) => deepEquals(uniqueRow, cellsValuesInRow));\n            if (isRowUnique) {\n                uniqueRows[row] = cellsValuesInRow;\n            }\n        }\n        // transform key object in number\n        return Object.keys(uniqueRows).map((key) => parseInt(key));\n    }\n    notifyRowsRemovedAndRemaining(removedRows, remainingRows) {\n        this.ui.notifyUI({\n            type: \"info\",\n            text: _t(\"%s duplicate rows found and removed.\\n%s unique rows remain.\", removedRows.toString(), remainingRows.toString()),\n            sticky: false,\n        });\n    }\n    checkSingleRangeSelected() {\n        const zones = this.getters.getSelectedZones();\n        if (zones.length !== 1) {\n            return \"MoreThanOneRangeSelected\" /* CommandResult.MoreThanOneRangeSelected */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkNoMergeInZone() {\n        const sheetId = this.getters.getActiveSheetId();\n        const zone = this.getters.getSelectedZone();\n        const mergesInZone = this.getters.getMergesInZone(sheetId, zone);\n        if (mergesInZone.length > 0) {\n            return \"WillRemoveExistingMerge\" /* CommandResult.WillRemoveExistingMerge */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkRangeContainsValues(cmd) {\n        const sheetId = this.getters.getActiveSheetId();\n        const zone = this.getters.getSelectedZone();\n        if (cmd.hasHeader) {\n            zone.top += 1;\n        }\n        const evaluatedCells = this.getters.getEvaluatedCellsInZone(sheetId, zone);\n        if (evaluatedCells.every((evaluatedCel) => evaluatedCel.type === \"empty\")) {\n            return \"EmptyTarget\" /* CommandResult.EmptyTarget */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkNoColumnProvided(cmd) {\n        if (cmd.columns.length === 0) {\n            return \"NoColumnsProvided\" /* CommandResult.NoColumnsProvided */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkColumnsIncludedInZone(cmd) {\n        const zone = this.getters.getSelectedZone();\n        const columnsToAnalyze = cmd.columns;\n        if (columnsToAnalyze.some((colIndex) => colIndex < zone.left || colIndex > zone.right)) {\n            return \"ColumnsNotIncludedInZone\" /* CommandResult.ColumnsNotIncludedInZone */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkColumnsAreUnique(cmd) {\n        if (cmd.columns.length !== new Set(cmd.columns).size) {\n            return \"DuplicatesColumnsSelected\" /* CommandResult.DuplicatesColumnsSelected */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    trimWhitespace() {\n        const zones = recomputeZones(this.getters.getSelectedZones());\n        const sheetId = this.getters.getActiveSheetId();\n        let count = 0;\n        for (const { col, row } of zones.map(positions).flat()) {\n            const cell = this.getters.getCell({ col, row, sheetId });\n            if (!cell) {\n                continue;\n            }\n            const trimmedContent = trimContent(cell.content);\n            if (trimmedContent !== cell.content) {\n                count += 1;\n                this.dispatch(\"UPDATE_CELL\", {\n                    sheetId,\n                    col,\n                    row,\n                    content: trimmedContent,\n                });\n            }\n        }\n        const text = count\n            ? _t(\"Trimmed whitespace from %s cells.\", count)\n            : _t(\"No selected cells had whitespace trimmed.\");\n        this.ui.notifyUI({\n            type: \"info\",\n            text: text,\n            sticky: false,\n        });\n    }\n}\n\nclass FormatPlugin extends UIPlugin {\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SET_DECIMAL\":\n                this.setDecimal(cmd.sheetId, cmd.target, cmd.step);\n                break;\n            case \"SET_FORMATTING_WITH_PIVOT\": {\n                this.setContextualFormat(cmd.sheetId, cmd.target, cmd.format);\n                break;\n            }\n        }\n    }\n    setContextualFormat(sheetId, zones, format) {\n        const measurePositions = [];\n        const measuresByPivotId = {};\n        for (const zone of recomputeZones(zones)) {\n            for (let col = zone.left; col <= zone.right; col++) {\n                for (let row = zone.top; row <= zone.bottom; row++) {\n                    const position = { sheetId, col, row };\n                    const pivotCell = this.getters.getPivotCellFromPosition(position);\n                    if (this.isSpilledPivotValueFormula(position, pivotCell)) {\n                        measurePositions.push(position);\n                        const pivotId = this.getters.getPivotIdFromPosition(position) || \"\";\n                        measuresByPivotId[pivotId] ??= new Set();\n                        measuresByPivotId[pivotId].add(pivotCell.measure);\n                    }\n                }\n            }\n        }\n        const measureZones = recomputeZones(measurePositions.map(positionToZone));\n        for (const pivotId in measuresByPivotId) {\n            const measures = measuresByPivotId[pivotId];\n            const pivotDefinition = this.getters.getPivotCoreDefinition(pivotId);\n            this.dispatch(\"UPDATE_PIVOT\", {\n                pivotId,\n                pivot: {\n                    ...pivotDefinition,\n                    measures: pivotDefinition.measures.map((measure) => {\n                        if (measures.has(measure.id)) {\n                            return { ...measure, format };\n                        }\n                        return measure;\n                    }),\n                },\n            });\n        }\n        this.dispatch(\"SET_FORMATTING\", {\n            sheetId,\n            target: measureZones,\n            format: \"\",\n        });\n        this.dispatch(\"SET_FORMATTING\", {\n            sheetId,\n            target: recomputeZones(zones, measureZones),\n            format,\n        });\n    }\n    isSpilledPivotValueFormula(position, pivotCell) {\n        const cell = this.getters.getCell(position);\n        return pivotCell.type === \"VALUE\" && !cell?.isFormula;\n    }\n    /**\n     * This function allows to adjust the quantity of decimal places after a decimal\n     * point on cells containing number value. It does this by changing the cells\n     * format. Values aren't modified.\n     *\n     * The change of the decimal quantity is done one by one, the sign of the step\n     * variable indicates whether we are increasing or decreasing.\n     *\n     * If several cells are in the zone, each cell's format will be individually\n     * evaluated and updated with the number type.\n     */\n    setDecimal(sheetId, zones, step) {\n        const positionsByFormat = {};\n        // Find the each cell with a number value and get the format\n        for (const zone of recomputeZones(zones)) {\n            for (const position of positions(zone)) {\n                const numberFormat = this.getCellNumberFormat({ sheetId, ...position });\n                if (numberFormat !== undefined) {\n                    // Depending on the step sign, increase or decrease the decimal representation\n                    // of the format\n                    const newFormat = changeDecimalPlaces(numberFormat, step);\n                    positionsByFormat[newFormat] = positionsByFormat[newFormat] || [];\n                    positionsByFormat[newFormat].push(position);\n                }\n            }\n        }\n        // consolidate all positions with the same format in bigger zones\n        for (const newFormat in positionsByFormat) {\n            const zones = recomputeZones(positionsByFormat[newFormat].map((position) => positionToZone(position)));\n            this.setContextualFormat(sheetId, zones, newFormat);\n        }\n    }\n    /**\n     * Take a range of cells and return the format of the first cell containing a\n     * number value. Returns a default format if the cell hasn't format. Returns\n     * undefined if no number value in the range.\n     */\n    getCellNumberFormat(position) {\n        for (const pos of [position]) {\n            const cell = this.getters.getEvaluatedCell(pos);\n            if (cell.type === CellValueType.number &&\n                !(cell.format && isDateTimeFormat(cell.format)) // reject dates\n            ) {\n                return cell.format || createDefaultFormat(cell.value);\n            }\n        }\n        return undefined;\n    }\n}\n\nclass HeaderVisibilityUIPlugin extends UIPlugin {\n    static getters = [\n        \"getNextVisibleCellPosition\",\n        \"findVisibleHeader\",\n        \"findLastVisibleColRowIndex\",\n        \"findFirstVisibleColRowIndex\",\n        \"isRowHidden\",\n        \"isColHidden\",\n        \"isHeaderHidden\",\n    ];\n    isRowHidden(sheetId, index) {\n        return (this.getters.isRowHiddenByUser(sheetId, index) || this.getters.isRowFiltered(sheetId, index));\n    }\n    isColHidden(sheetId, index) {\n        return this.getters.isColHiddenByUser(sheetId, index);\n    }\n    isHeaderHidden(sheetId, dimension, index) {\n        return dimension === \"COL\"\n            ? this.isColHidden(sheetId, index)\n            : this.isRowHidden(sheetId, index);\n    }\n    getNextVisibleCellPosition({ sheetId, col, row }) {\n        return {\n            sheetId,\n            col: this.findVisibleHeader(sheetId, \"COL\", col, this.getters.getNumberCols(sheetId) - 1),\n            row: this.findVisibleHeader(sheetId, \"ROW\", row, this.getters.getNumberRows(sheetId) - 1),\n        };\n    }\n    /**\n     * Find the first visible header in the range [`from` => `to`].\n     *\n     * Both `from` and `to` are inclusive.\n     */\n    findVisibleHeader(sheetId, dimension, from, to) {\n        if (from <= to) {\n            for (let i = from; i <= to; i++) {\n                if (this.getters.doesHeaderExist(sheetId, dimension, i) &&\n                    !this.isHeaderHidden(sheetId, dimension, i)) {\n                    return i;\n                }\n            }\n        }\n        if (from > to) {\n            for (let i = from; i >= to; i--) {\n                if (this.getters.doesHeaderExist(sheetId, dimension, i) &&\n                    !this.isHeaderHidden(sheetId, dimension, i)) {\n                    return i;\n                }\n            }\n        }\n        return undefined;\n    }\n    findLastVisibleColRowIndex(sheetId, dimension, { last, first }) {\n        const lastVisibleIndex = range(last, first, -1).find((index) => !this.isHeaderHidden(sheetId, dimension, index));\n        return lastVisibleIndex || first;\n    }\n    findFirstVisibleColRowIndex(sheetId, dimension) {\n        const numberOfHeaders = this.getters.getNumberHeaders(sheetId, dimension);\n        for (let i = 0; i < numberOfHeaders; i++) {\n            if (dimension === \"COL\" && !this.isColHidden(sheetId, i)) {\n                return i;\n            }\n            if (dimension === \"ROW\" && !this.isRowHidden(sheetId, i)) {\n                return i;\n            }\n        }\n        return undefined;\n    }\n    exportForExcel(data) {\n        for (const sheetData of data.sheets) {\n            for (const [row, rowData] of Object.entries(sheetData.rows)) {\n                const isHidden = this.isRowHidden(sheetData.id, Number(row));\n                rowData.isHidden = isHidden;\n            }\n        }\n    }\n}\n\nclass InsertPivotPlugin extends UIPlugin {\n    static getters = [];\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"INSERT_NEW_PIVOT\":\n                this.insertNewPivot(cmd.pivotId, cmd.newSheetId);\n                break;\n            case \"DUPLICATE_PIVOT_IN_NEW_SHEET\":\n                this.duplicatePivotInNewSheet(cmd.pivotId, cmd.newPivotId, cmd.newSheetId);\n                break;\n            case \"INSERT_PIVOT_WITH_TABLE\":\n                this.insertPivotWithTable(cmd.sheetId, cmd.col, cmd.row, cmd.pivotId, cmd.table, cmd.pivotMode);\n                break;\n            case \"SPLIT_PIVOT_FORMULA\":\n                this.splitPivotFormula(cmd.sheetId, cmd.col, cmd.row, cmd.pivotId);\n        }\n    }\n    insertNewPivot(pivotId, sheetId) {\n        if (getZoneArea(this.getters.getSelectedZone()) === 1) {\n            this.selection.selectTableAroundSelection();\n        }\n        const currentSheetId = this.getters.getActiveSheetId();\n        this.dispatch(\"ADD_PIVOT\", {\n            pivotId,\n            pivot: {\n                dataSet: {\n                    zone: this.getters.getSelectedZone(),\n                    sheetId: currentSheetId,\n                },\n                columns: [],\n                rows: [],\n                measures: [],\n                name: _t(\"New pivot\"),\n                type: \"SPREADSHEET\",\n            },\n        });\n        const position = this.getters.getSheetIds().findIndex((sheetId) => sheetId === currentSheetId) + 1;\n        const formulaId = this.getters.getPivotFormulaId(pivotId);\n        this.dispatch(\"CREATE_SHEET\", {\n            sheetId,\n            name: _t(\"Pivot #%(formulaId)s\", { formulaId }),\n            position,\n        });\n        this.dispatch(\"ACTIVATE_SHEET\", {\n            sheetIdFrom: currentSheetId,\n            sheetIdTo: sheetId,\n        });\n        const pivot = this.getters.getPivot(pivotId);\n        this.insertPivotWithTable(sheetId, 0, 0, pivotId, pivot.getTableStructure().export(), \"dynamic\");\n    }\n    duplicatePivotInNewSheet(pivotId, newPivotId, newSheetId) {\n        this.dispatch(\"DUPLICATE_PIVOT\", {\n            pivotId,\n            newPivotId,\n            duplicatedPivotName: _t(\"%s (copy)\", this.getters.getPivotCoreDefinition(pivotId).name),\n        });\n        const activeSheetId = this.getters.getActiveSheetId();\n        const position = this.getters.getSheetIds().indexOf(activeSheetId) + 1;\n        const formulaId = this.getters.getPivotFormulaId(newPivotId);\n        const newPivotName = this.getters.getPivotName(newPivotId);\n        const result = this.dispatch(\"CREATE_SHEET\", {\n            sheetId: newSheetId,\n            name: this.getPivotDuplicateSheetName(_t(\"%(newPivotName)s (Pivot #%(formulaId)s)\", {\n                newPivotName,\n                formulaId,\n            })),\n            position,\n        });\n        if (result.isSuccessful) {\n            this.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });\n            const pivot = this.getters.getPivot(pivotId);\n            this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), \"dynamic\");\n        }\n    }\n    getPivotDuplicateSheetName(pivotName) {\n        let i = 1;\n        const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));\n        const sanitizedName = sanitizeSheetName(pivotName);\n        let name = sanitizedName;\n        while (names.includes(name)) {\n            name = `${sanitizedName} (${i})`;\n            i++;\n        }\n        return name;\n    }\n    insertPivotWithTable(sheetId, col, row, pivotId, table, mode) {\n        const { cols, rows, measures, fieldsType } = table;\n        const pivotTable = new SpreadsheetPivotTable(cols, rows, measures, fieldsType || {});\n        const numberOfHeaders = pivotTable.columns.length - 1;\n        this.resizeSheet(sheetId, col, row, pivotTable);\n        const pivotFormulaId = this.getters.getPivotFormulaId(pivotId);\n        let zone;\n        if (mode === \"dynamic\") {\n            this.dispatch(\"UPDATE_CELL\", {\n                sheetId,\n                col,\n                row,\n                content: `=PIVOT(${pivotFormulaId})`,\n            });\n            zone = {\n                left: col,\n                right: col,\n                top: row,\n                bottom: row,\n            };\n        }\n        else {\n            this.dispatch(\"INSERT_PIVOT\", {\n                sheetId,\n                col,\n                row,\n                pivotId,\n                table: pivotTable.export(),\n            });\n            zone = {\n                left: col,\n                right: col + pivotTable.getNumberOfDataColumns(),\n                top: row,\n                bottom: row + numberOfHeaders + pivotTable.rows.length,\n            };\n        }\n        this.dispatch(\"CREATE_TABLE\", {\n            tableType: mode,\n            sheetId,\n            ranges: [this.getters.getRangeDataFromZone(sheetId, zone)],\n            config: { ...PIVOT_TABLE_CONFIG, numberOfHeaders },\n        });\n    }\n    resizeSheet(sheetId, col, row, table) {\n        const colLimit = table.getNumberOfDataColumns() + 1; // +1 for the Top-Left\n        const numberCols = this.getters.getNumberCols(sheetId);\n        const deltaCol = numberCols - col;\n        if (deltaCol < colLimit) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"COL\",\n                base: numberCols - 1,\n                sheetId: sheetId,\n                quantity: colLimit - deltaCol,\n                position: \"after\",\n            });\n        }\n        const rowLimit = table.columns.length + table.rows.length;\n        const numberRows = this.getters.getNumberRows(sheetId);\n        const deltaRow = numberRows - row;\n        if (deltaRow < rowLimit) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"ROW\",\n                base: numberRows - 1,\n                sheetId: sheetId,\n                quantity: rowLimit - deltaRow,\n                position: \"after\",\n            });\n        }\n    }\n    splitPivotFormula(sheetId, col, row, pivotId) {\n        const spreadZone = this.getters.getSpreadZone({ sheetId, col, row });\n        if (!spreadZone) {\n            return;\n        }\n        const formulaId = this.getters.getPivotFormulaId(pivotId);\n        const pivotCells = new Map();\n        for (let col = spreadZone.left; col <= spreadZone.right; col++) {\n            for (let row = spreadZone.top; row <= spreadZone.bottom; row++) {\n                const position = { sheetId, col, row };\n                pivotCells.set(position, this.getters.getPivotCellFromPosition(position));\n            }\n        }\n        for (const [position, pivotCell] of pivotCells) {\n            this.dispatch(\"UPDATE_CELL\", {\n                ...position,\n                content: createPivotFormula(formulaId, pivotCell),\n            });\n        }\n        const table = this.getters.getCoreTable({ sheetId, col, row });\n        if (table?.type === \"dynamic\") {\n            const zone = positionToZone({ col, row });\n            const rangeData = this.getters.getRangeDataFromZone(sheetId, spreadZone);\n            this.dispatch(\"UPDATE_TABLE\", {\n                sheetId,\n                zone,\n                newTableRange: rangeData,\n                tableType: \"static\",\n            });\n        }\n    }\n}\n\nclass SortPlugin extends UIPlugin {\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"SORT_CELLS\":\n                if (!isInside(cmd.col, cmd.row, cmd.zone)) {\n                    throw new Error(_t(\"The anchor must be part of the provided zone\"));\n                }\n                return this.checkValidations(cmd, this.checkMerge, this.checkMergeSizes);\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SORT_CELLS\":\n                this.sortZone(cmd.sheetId, cmd, cmd.zone, cmd.sortDirection, cmd.sortOptions || {});\n                break;\n        }\n    }\n    checkMerge({ sheetId, zone }) {\n        if (!this.getters.doesIntersectMerge(sheetId, zone)) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        /*Test the presence of single cells*/\n        const singleCells = positions(zone).some(({ col, row }) => !this.getters.isInMerge({ sheetId, col, row }));\n        if (singleCells) {\n            return \"InvalidSortZone\" /* CommandResult.InvalidSortZone */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkMergeSizes({ sheetId, zone }) {\n        if (!this.getters.doesIntersectMerge(sheetId, zone)) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        const merges = this.getters.getMerges(sheetId).filter((merge) => overlap(merge, zone));\n        /*Test the presence of merges of different sizes*/\n        const mergeDimension = zoneToDimension(merges[0]);\n        let [widthFirst, heightFirst] = [mergeDimension.numberOfCols, mergeDimension.numberOfRows];\n        if (!merges.every((merge) => {\n            let [widthCurrent, heightCurrent] = [\n                merge.right - merge.left + 1,\n                merge.bottom - merge.top + 1,\n            ];\n            return widthCurrent === widthFirst && heightCurrent === heightFirst;\n        })) {\n            return \"InvalidSortZone\" /* CommandResult.InvalidSortZone */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    /**\n     * This function evaluates if the top row of a provided zone can be considered as a `header`\n     * by checking the following criteria:\n     * * If the left-most column top row value (topLeft) is empty, we ignore it while evaluating the criteria.\n     * 1 - Apart from the left-most column, every element of the top row must be non-empty, i.e. a cell should be present in the sheet.\n     * 2 - There should be at least one column in which the type (CellValueType) of the rop row cell differs from the type of the cell below.\n     *  For the second criteria, we ignore columns on which the cell below is empty.\n     *\n     */\n    hasHeader(sheetId, items) {\n        if (items[0].length === 1)\n            return false;\n        let cells = items.map((col) => col.map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }).type));\n        // ignore left-most column when topLeft cell is empty\n        const topLeft = cells[0][0];\n        if (topLeft === CellValueType.empty) {\n            cells = cells.slice(1);\n        }\n        if (cells.some((item) => item[0] === CellValueType.empty)) {\n            return false;\n        }\n        else if (cells.some((item) => item[1] !== CellValueType.empty && item[0] !== item[1])) {\n            return true;\n        }\n        else {\n            return false;\n        }\n    }\n    sortZone(sheetId, anchor, zone, sortDirection, options) {\n        const [stepX, stepY] = this.mainCellsSteps(sheetId, zone);\n        let sortingCol = this.getters.getMainCellPosition({\n            sheetId,\n            col: anchor.col,\n            row: anchor.row,\n        }).col; // fetch anchor\n        let sortZone = Object.assign({}, zone);\n        // Update in case of merges in the zone\n        let cellPositions = this.mainCells(sheetId, zone);\n        if (!options.sortHeaders && this.hasHeader(sheetId, cellPositions)) {\n            sortZone.top += stepY;\n        }\n        cellPositions = this.mainCells(sheetId, sortZone);\n        const sortingCells = cellPositions[sortingCol - sortZone.left];\n        const sortedIndexOfSortTypeCells = sortCells(sortingCells.map((position) => this.getters.getEvaluatedCell(position)), sortDirection, Boolean(options.emptyCellAsZero));\n        const sortedIndex = sortedIndexOfSortTypeCells.map((x) => x.index);\n        const [width, height] = [cellPositions.length, cellPositions[0].length];\n        const updateCellCommands = [];\n        for (let c = 0; c < width; c++) {\n            for (let r = 0; r < height; r++) {\n                let { col, row, sheetId } = cellPositions[c][sortedIndex[r]];\n                const cell = this.getters.getCell({ sheetId, col, row });\n                let newCol = sortZone.left + c * stepX;\n                let newRow = sortZone.top + r * stepY;\n                let newCellValues = {\n                    sheetId: sheetId,\n                    col: newCol,\n                    row: newRow,\n                    content: \"\",\n                };\n                if (cell) {\n                    let content = cell.content;\n                    if (cell.isFormula) {\n                        const position = this.getters.getCellPosition(cell.id);\n                        // we only have a vertical offset\n                        content = this.getters.getTranslatedCellFormula(sheetId, 0, newRow - position.row, cell.compiledFormula.tokens);\n                    }\n                    newCellValues.style = cell.style;\n                    newCellValues.content = content;\n                    newCellValues.format = cell.format;\n                }\n                updateCellCommands.push(newCellValues);\n            }\n        }\n        updateCellCommands.forEach((cmdPayload) => this.dispatch(\"UPDATE_CELL\", cmdPayload));\n    }\n    /**\n     * Return the distances between main merge cells in the zone.\n     * (1 if there are no merges).\n     * Note: it is assumed all merges are the same in the zone.\n     */\n    mainCellsSteps(sheetId, zone) {\n        const merge = this.getters.getMerge({ sheetId, col: zone.left, row: zone.top });\n        const stepX = merge ? merge.right - merge.left + 1 : 1;\n        const stepY = merge ? merge.bottom - merge.top + 1 : 1;\n        return [stepX, stepY];\n    }\n    /**\n     * Return a 2D array of cells in the zone (main merge cells if there are merges)\n     */\n    mainCells(sheetId, zone) {\n        const [stepX, stepY] = this.mainCellsSteps(sheetId, zone);\n        const cells = [];\n        const cols = range(zone.left, zone.right + 1, stepX);\n        const rows = range(zone.top, zone.bottom + 1, stepY);\n        for (const col of cols) {\n            const colCells = [];\n            cells.push(colCells);\n            for (const row of rows) {\n                colCells.push({ sheetId, col, row });\n            }\n        }\n        return cells;\n    }\n}\n\nclass UIOptionsPlugin extends UIPlugin {\n    static getters = [\"shouldShowFormulas\"];\n    showFormulas = false;\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SET_FORMULA_VISIBILITY\":\n                this.showFormulas = cmd.show;\n                break;\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    shouldShowFormulas() {\n        return this.showFormulas;\n    }\n}\n\nclass SheetUIPlugin extends UIPlugin {\n    static getters = [\n        \"doesCellHaveGridIcon\",\n        \"getCellWidth\",\n        \"getCellIconSrc\",\n        \"getTextWidth\",\n        \"getCellText\",\n        \"getCellMultiLineText\",\n        \"getContiguousZone\",\n    ];\n    ctx = document.createElement(\"canvas\").getContext(\"2d\");\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        return this.chainValidations(this.checkSheetExists, this.checkZonesAreInSheet)(cmd);\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"AUTORESIZE_COLUMNS\":\n                for (let col of cmd.cols) {\n                    const size = this.getColMaxWidth(cmd.sheetId, col);\n                    if (size !== 0) {\n                        this.dispatch(\"RESIZE_COLUMNS_ROWS\", {\n                            elements: [col],\n                            dimension: \"COL\",\n                            size,\n                            sheetId: cmd.sheetId,\n                        });\n                    }\n                }\n                break;\n            case \"AUTORESIZE_ROWS\":\n                for (let row of cmd.rows) {\n                    this.dispatch(\"RESIZE_COLUMNS_ROWS\", {\n                        elements: [row],\n                        dimension: \"ROW\",\n                        size: null,\n                        sheetId: cmd.sheetId,\n                    });\n                }\n                break;\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    getCellWidth(position) {\n        const style = this.getters.getCellComputedStyle(position);\n        let contentWidth = 0;\n        const content = this.getters.getEvaluatedCell(position).formattedValue;\n        if (content) {\n            const multiLineText = splitTextToWidth(this.ctx, content, style, undefined);\n            contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style)));\n        }\n        const icon = this.getters.getCellIconSrc(position);\n        if (icon) {\n            contentWidth += computeIconWidth(style);\n        }\n        if (this.getters.doesCellHaveGridIcon(position)) {\n            contentWidth += ICON_EDGE_LENGTH + GRID_ICON_MARGIN;\n        }\n        if (contentWidth === 0) {\n            return 0;\n        }\n        contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;\n        if (style.wrapping === \"wrap\") {\n            const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col);\n            return Math.min(colWidth, contentWidth);\n        }\n        return contentWidth;\n    }\n    getCellIconSrc(position) {\n        const callbacks = iconsOnCellRegistry.getAll();\n        for (const callback of callbacks) {\n            const imageSrc = callback(this.getters, position);\n            if (imageSrc) {\n                return imageSrc;\n            }\n        }\n        return undefined;\n    }\n    getTextWidth(text, style) {\n        return computeTextWidth(this.ctx, text, style);\n    }\n    getCellText(position, args) {\n        const cell = this.getters.getCell(position);\n        const locale = this.getters.getLocale();\n        if (args?.showFormula && cell?.isFormula) {\n            return localizeFormula(cell.content, locale);\n        }\n        else if (args?.showFormula && !cell?.content) {\n            return \"\";\n        }\n        else {\n            const evaluatedCell = this.getters.getEvaluatedCell(position);\n            const formatWidth = args?.availableWidth\n                ? {\n                    availableWidth: args.availableWidth,\n                    measureText: (text) => computeTextWidth(this.ctx, text, cell?.style || {}),\n                }\n                : undefined;\n            return formatValue(evaluatedCell.value, {\n                format: evaluatedCell.format,\n                locale,\n                formatWidth,\n            });\n        }\n    }\n    /**\n     * Return the text of a cell, split in multiple lines if needed. The text will be split in multiple\n     * line if it contains NEWLINE characters, or if it's longer than the given width.\n     */\n    getCellMultiLineText(position, args) {\n        const style = this.getters.getCellStyle(position);\n        const text = this.getters.getCellText(position, {\n            showFormula: this.getters.shouldShowFormulas(),\n            availableWidth: args.maxWidth,\n        });\n        return splitTextToWidth(this.ctx, text, style, args.wrapText ? args.maxWidth : undefined);\n    }\n    doesCellHaveGridIcon(position) {\n        const isFilterHeader = this.getters.isFilterHeader(position);\n        const hasListIcon = !this.getters.isReadonly() && this.getters.cellHasListDataValidationIcon(position);\n        return isFilterHeader || hasListIcon;\n    }\n    /**\n     * Expands the given zone until bordered by empty cells or reached the sheet boundaries.\n     */\n    getContiguousZone(sheetId, zoneToExpand) {\n        /** Try to expand the zone by one col/row in any direction to include a new non-empty cell */\n        const expandZone = (zone) => {\n            for (const col of range(zone.left, zone.right + 1)) {\n                if (!this.isCellEmpty({ sheetId, col, row: zone.top - 1 })) {\n                    return { ...zone, top: zone.top - 1 };\n                }\n                if (!this.isCellEmpty({ sheetId, col, row: zone.bottom + 1 })) {\n                    return { ...zone, bottom: zone.bottom + 1 };\n                }\n            }\n            for (const row of range(zone.top, zone.bottom + 1)) {\n                if (!this.isCellEmpty({ sheetId, col: zone.left - 1, row })) {\n                    return { ...zone, left: zone.left - 1 };\n                }\n                if (!this.isCellEmpty({ sheetId, col: zone.right + 1, row })) {\n                    return { ...zone, right: zone.right + 1 };\n                }\n            }\n            return zone;\n        };\n        let hasExpanded = false;\n        let zone = zoneToExpand;\n        do {\n            hasExpanded = false;\n            const newZone = expandZone(zone);\n            if (!isEqual(zone, newZone)) {\n                hasExpanded = true;\n                zone = newZone;\n                continue;\n            }\n        } while (hasExpanded);\n        return zone;\n    }\n    /**\n     * Checks if a cell is empty (i.e. does not have a content or a formula does not spread over it).\n     * If the cell is part of a merge, the check applies to the main cell of the merge.\n     */\n    isCellEmpty(position) {\n        const mainPosition = this.getters.getMainCellPosition(position);\n        return this.getters.getEvaluatedCell(mainPosition).type === CellValueType.empty;\n    }\n    getColMaxWidth(sheetId, index) {\n        const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));\n        const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));\n        return Math.max(0, largeMax(sizes));\n    }\n    /**\n     * Check that any \"sheetId\" in the command matches an existing\n     * sheet.\n     */\n    checkSheetExists(cmd) {\n        if (\"sheetId\" in cmd && this.getters.tryGetSheet(cmd.sheetId) === undefined) {\n            return \"InvalidSheetId\" /* CommandResult.InvalidSheetId */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    /**\n     * Check if zones in the command are well formed and\n     * not outside the sheet.\n     */\n    checkZonesAreInSheet(cmd) {\n        const sheetId = \"sheetId\" in cmd ? cmd.sheetId : this.getters.tryGetActiveSheetId();\n        const zones = this.getters.getCommandZones(cmd);\n        if (!sheetId && zones.length > 0) {\n            return \"NoActiveSheet\" /* CommandResult.NoActiveSheet */;\n        }\n        if (sheetId && zones.length > 0) {\n            return this.getters.checkZonesExistInSheet(sheetId, zones);\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n}\n\nclass TableComputedStylePlugin extends UIPlugin {\n    static getters = [\"getCellTableStyle\", \"getCellTableBorder\"];\n    tableStyles = {};\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            (cmd.type === \"UPDATE_CELL\" && \"content\" in cmd) ||\n            cmd.type === \"EVALUATE_CELLS\") {\n            this.tableStyles = {};\n            return;\n        }\n        if (doesCommandInvalidatesTableStyle(cmd)) {\n            if (\"sheetId\" in cmd) {\n                delete this.tableStyles[cmd.sheetId];\n            }\n            else {\n                this.tableStyles = {};\n            }\n            return;\n        }\n    }\n    finalize() {\n        for (const sheetId of this.getters.getSheetIds()) {\n            if (!this.tableStyles[sheetId]) {\n                this.tableStyles[sheetId] = {};\n            }\n            for (const table of this.getters.getTables(sheetId)) {\n                if (!this.tableStyles[sheetId][table.id]) {\n                    this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);\n                }\n            }\n        }\n    }\n    getCellTableStyle(position) {\n        const table = this.getters.getTable(position);\n        if (!table) {\n            return undefined;\n        }\n        return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];\n    }\n    getCellTableBorder(position) {\n        const table = this.getters.getTable(position);\n        if (!table) {\n            return undefined;\n        }\n        return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];\n    }\n    computeTableStyle(sheetId, table) {\n        return lazy(() => {\n            const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);\n            const style = this.getters.getTableStyle(table.config.styleId);\n            const relativeTableStyle = getComputedTableStyle(config, style, numberOfCols, numberOfRows);\n            // Return the style with sheet coordinates instead of tables coordinates\n            const mapping = this.getTableMapping(sheetId, table);\n            const absoluteTableStyle = { borders: {}, styles: {} };\n            for (let col = 0; col < numberOfCols; col++) {\n                const colInSheet = mapping.colMapping[col];\n                absoluteTableStyle.borders[colInSheet] = {};\n                absoluteTableStyle.styles[colInSheet] = {};\n                for (let row = 0; row < numberOfRows; row++) {\n                    const rowInSheet = mapping.rowMapping[row];\n                    absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];\n                    absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];\n                }\n            }\n            return absoluteTableStyle;\n        });\n    }\n    /**\n     * Get the actual table config that will be used to compute the table style. It is different from\n     * the config of the table because of hidden rows and columns in the sheet. For example remove the\n     * hidden rows from config.numberOfHeaders.\n     */\n    getTableRuntimeConfig(sheetId, table) {\n        const tableZone = table.range.zone;\n        const config = { ...table.config };\n        let numberOfCols = tableZone.right - tableZone.left + 1;\n        let numberOfRows = tableZone.bottom - tableZone.top + 1;\n        for (let row = tableZone.top; row <= tableZone.bottom; row++) {\n            if (!this.getters.isRowHidden(sheetId, row)) {\n                continue;\n            }\n            numberOfRows--;\n            if (row - tableZone.top < table.config.numberOfHeaders) {\n                config.numberOfHeaders--;\n                if (config.numberOfHeaders < 0) {\n                    config.numberOfHeaders = 0;\n                }\n            }\n            if (row === tableZone.bottom) {\n                config.totalRow = false;\n            }\n        }\n        for (let col = tableZone.left; col <= tableZone.right; col++) {\n            if (!this.getters.isColHidden(sheetId, col)) {\n                continue;\n            }\n            numberOfCols--;\n            if (col === tableZone.left) {\n                config.firstColumn = false;\n            }\n            if (col === tableZone.right) {\n                config.lastColumn = false;\n            }\n        }\n        return {\n            config,\n            numberOfCols,\n            numberOfRows,\n        };\n    }\n    /**\n     * Get a mapping: relative col/row position in the table <=> col/row in the sheet\n     */\n    getTableMapping(sheetId, table) {\n        const colMapping = {};\n        const rowMapping = {};\n        let colOffset = 0;\n        let rowOffset = 0;\n        const tableZone = table.range.zone;\n        for (let col = tableZone.left; col <= tableZone.right; col++) {\n            if (this.getters.isColHidden(sheetId, col)) {\n                continue;\n            }\n            colMapping[colOffset] = col;\n            colOffset++;\n            for (let row = tableZone.top; row <= tableZone.bottom; row++) {\n                if (this.getters.isRowHidden(sheetId, row)) {\n                    continue;\n                }\n                rowMapping[rowOffset] = row;\n                rowOffset++;\n            }\n        }\n        return {\n            colMapping,\n            rowMapping,\n        };\n    }\n}\nconst invalidateTableStyleCommands = [\n    \"HIDE_COLUMNS_ROWS\",\n    \"UNHIDE_COLUMNS_ROWS\",\n    \"UNFOLD_HEADER_GROUP\",\n    \"UNGROUP_HEADERS\",\n    \"FOLD_HEADER_GROUP\",\n    \"FOLD_ALL_HEADER_GROUPS\",\n    \"UNFOLD_ALL_HEADER_GROUPS\",\n    \"FOLD_HEADER_GROUPS_IN_ZONE\",\n    \"UNFOLD_HEADER_GROUPS_IN_ZONE\",\n    \"CREATE_TABLE\",\n    \"UPDATE_TABLE\",\n    \"UPDATE_FILTER\",\n    \"REMOVE_TABLE\",\n    \"RESIZE_TABLE\",\n    \"CREATE_TABLE_STYLE\",\n    \"REMOVE_TABLE_STYLE\",\n];\nconst invalidateTableStyleCommandsSet = new Set(invalidateTableStyleCommands);\nfunction doesCommandInvalidatesTableStyle(cmd) {\n    return invalidateTableStyleCommandsSet.has(cmd.type);\n}\n\nclass CellComputedStylePlugin extends UIPlugin {\n    static getters = [\"getCellComputedBorder\", \"getCellComputedStyle\"];\n    styles = {};\n    borders = {};\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            cmd.type === \"UPDATE_CELL\" ||\n            cmd.type === \"SET_FORMATTING\" ||\n            cmd.type === \"EVALUATE_CELLS\") {\n            this.styles = {};\n            this.borders = {};\n            return;\n        }\n        if (doesCommandInvalidatesTableStyle(cmd)) {\n            if (\"sheetId\" in cmd) {\n                delete this.styles[cmd.sheetId];\n                delete this.borders[cmd.sheetId];\n            }\n            else {\n                this.styles = {};\n                this.borders = {};\n            }\n            return;\n        }\n        if (invalidateCFEvaluationCommands.has(cmd.type)) {\n            this.styles = {};\n            return;\n        }\n        if (invalidateBordersCommands.has(cmd.type)) {\n            this.borders = {};\n            return;\n        }\n    }\n    getCellComputedBorder(position) {\n        const { sheetId, row, col } = position;\n        if (this.borders[sheetId]?.[row]?.[col] !== undefined) {\n            return this.borders[sheetId][row][col];\n        }\n        if (!this.borders[sheetId]) {\n            this.borders[sheetId] = {};\n        }\n        if (!this.borders[sheetId][row]) {\n            this.borders[sheetId][row] = {};\n        }\n        if (!this.borders[sheetId][row][col]) {\n            this.borders[sheetId][row][col] = this.computeCellBorder(position);\n        }\n        return this.borders[sheetId][row][col];\n    }\n    getCellComputedStyle(position) {\n        const { sheetId, row, col } = position;\n        if (this.styles[sheetId]?.[row]?.[col] !== undefined) {\n            return this.styles[sheetId][row][col];\n        }\n        if (!this.styles[sheetId]) {\n            this.styles[sheetId] = {};\n        }\n        if (!this.styles[sheetId][row]) {\n            this.styles[sheetId][row] = {};\n        }\n        if (!this.styles[sheetId][row][col]) {\n            this.styles[sheetId][row][col] = this.computeCellStyle(position);\n        }\n        return this.styles[sheetId][row][col];\n    }\n    computeCellBorder(position) {\n        const cellBorder = this.getters.getCellBorder(position) || {};\n        const cellTableBorder = this.getters.getCellTableBorder(position) || {};\n        // Use removeFalsyAttributes to avoid overwriting borders with undefined values\n        const border = {\n            ...removeFalsyAttributes(cellTableBorder),\n            ...removeFalsyAttributes(cellBorder),\n        };\n        return isObjectEmptyRecursive(border) ? null : border;\n    }\n    computeCellStyle(position) {\n        const cell = this.getters.getCell(position);\n        const cfStyle = this.getters.getCellConditionalFormatStyle(position);\n        const tableStyle = this.getters.getCellTableStyle(position);\n        const computedStyle = {\n            ...removeFalsyAttributes(tableStyle),\n            ...removeFalsyAttributes(cell?.style),\n            ...removeFalsyAttributes(cfStyle),\n        };\n        const evaluatedCell = this.getters.getEvaluatedCell(position);\n        if (evaluatedCell.link && !computedStyle.textColor) {\n            computedStyle.textColor = LINK_COLOR;\n        }\n        return computedStyle;\n    }\n}\n\nclass DataValidationInsertionPlugin extends UIPlugin {\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_DATA_VALIDATION_RULE\":\n                if (cmd.rule.criterion.type === \"isBoolean\") {\n                    const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));\n                    for (const position of getCellPositionsInRanges(ranges)) {\n                        const cell = this.getters.getCell(position);\n                        const evaluatedCell = this.getters.getEvaluatedCell(position);\n                        if (!cell?.content) {\n                            this.dispatch(\"UPDATE_CELL\", { ...position, content: \"FALSE\" });\n                            // In this case, a cell has been updated in the core plugin but\n                            // not yet evaluated. This can occur after a paste operation.\n                        }\n                        else if (cell?.content && evaluatedCell.type === CellValueType.empty) {\n                            let value;\n                            if (cell.content.startsWith(\"=\")) {\n                                const result = this.getters.evaluateFormula(position.sheetId, cell.content);\n                                value = (isMatrix(result) ? result[0][0] : result)?.toString();\n                            }\n                            else {\n                                value = cell.content;\n                            }\n                            if (!value || !isBoolean(value)) {\n                                this.dispatch(\"UPDATE_CELL\", { ...position, content: \"FALSE\" });\n                            }\n                        }\n                        else if (evaluatedCell.type !== CellValueType.boolean) {\n                            this.dispatch(\"UPDATE_CELL\", { ...position, content: \"FALSE\" });\n                        }\n                    }\n                }\n        }\n    }\n}\n\nconst genericRepeatsTransforms = [\n    repeatSheetDependantCommand,\n    repeatTargetDependantCommand,\n    repeatPositionDependantCommand,\n    repeatRangeDependantCommand,\n];\nfunction repeatSheetDependantCommand(getters, command) {\n    if (!(\"sheetId\" in command))\n        return command;\n    return { ...deepCopy(command), sheetId: getters.getActiveSheetId() };\n}\nfunction repeatTargetDependantCommand(getters, command) {\n    if (!(\"target\" in command) || !Array.isArray(command.target))\n        return command;\n    return {\n        ...deepCopy(command),\n        target: getters.getSelectedZones(),\n    };\n}\nfunction repeatZoneDependantCommand(getters, command) {\n    if (!(\"zone\" in command))\n        return command;\n    return {\n        ...deepCopy(command),\n        zone: getters.getSelectedZone(),\n    };\n}\nfunction repeatPositionDependantCommand(getters, command) {\n    if (!(\"row\" in command) || !(\"col\" in command))\n        return command;\n    const { col, row } = getters.getActivePosition();\n    return { ...deepCopy(command), col, row };\n}\nfunction repeatRangeDependantCommand(getters, command) {\n    if (!(\"ranges\" in command))\n        return command;\n    return {\n        ...deepCopy(command),\n        ranges: getters\n            .getSelectedZones()\n            .map((zone) => getters.getRangeDataFromZone(getters.getActiveSheetId(), zone)),\n    };\n}\n\nconst uuidGenerator = new UuidGenerator();\nfunction repeatCreateChartCommand(getters, cmd) {\n    return {\n        ...repeatSheetDependantCommand(getters, cmd),\n        id: uuidGenerator.uuidv4(),\n    };\n}\nfunction repeatCreateImageCommand(getters, cmd) {\n    return {\n        ...repeatSheetDependantCommand(getters, cmd),\n        figureId: uuidGenerator.uuidv4(),\n    };\n}\nfunction repeatCreateFigureCommand(getters, cmd) {\n    const newCmd = repeatSheetDependantCommand(getters, cmd);\n    newCmd.figure.id = uuidGenerator.uuidv4();\n    return newCmd;\n}\nfunction repeatCreateSheetCommand(getters, cmd) {\n    const newCmd = deepCopy(cmd);\n    newCmd.sheetId = uuidGenerator.uuidv4();\n    const sheetName = cmd.name || getters.getSheet(getters.getActiveSheetId()).name;\n    // Extract the prefix of the sheet name (everything before the number at the end of the name)\n    const namePrefix = sheetName.match(/(.+?)\\d*$/)?.[1] || sheetName;\n    newCmd.name = getters.getNextSheetName(namePrefix);\n    return newCmd;\n}\nfunction repeatAddColumnsRowsCommand(getters, cmd) {\n    const currentPosition = getters.getActivePosition();\n    return {\n        ...repeatSheetDependantCommand(getters, cmd),\n        base: cmd.dimension === \"COL\" ? currentPosition.col : currentPosition.row,\n    };\n}\nfunction repeatHeaderElementCommand(getters, cmd) {\n    const currentSelection = getters.getSelectedZone();\n    return {\n        ...repeatSheetDependantCommand(getters, cmd),\n        elements: cmd.dimension === \"COL\"\n            ? range(currentSelection.left, currentSelection.right + 1)\n            : range(currentSelection.top, currentSelection.bottom + 1),\n    };\n}\nfunction repeatInsertOrDeleteCellCommand(getters, cmd) {\n    const currentSelection = getters.getSelectedZone();\n    return {\n        ...deepCopy(cmd),\n        zone: currentSelection,\n    };\n}\nfunction repeatAutoResizeCommand(getters, cmd) {\n    const newCmd = deepCopy(cmd);\n    const currentSelection = getters.getSelectedZone();\n    const { top, bottom, left, right } = currentSelection;\n    if (\"cols\" in newCmd) {\n        newCmd.cols = range(left, right + 1);\n    }\n    else if (\"rows\" in newCmd) {\n        newCmd.rows = range(top, bottom + 1);\n    }\n    return newCmd;\n}\nfunction repeatSortCellsCommand(getters, cmd) {\n    const currentSelection = getters.getSelectedZone();\n    return {\n        ...repeatSheetDependantCommand(getters, cmd),\n        col: currentSelection.left,\n        row: currentSelection.top,\n        zone: currentSelection,\n    };\n}\nfunction repeatPasteCommand(getters, cmd) {\n    /**\n     * Note : we have to store the state of the clipboard in the clipboard plugin, and introduce a\n     * new command REPEAT_PASTE to be able to repeat the paste command.\n     *\n     * We cannot re-dispatch a paste, because the content of the clipboard may have changed in between.\n     *\n     * And we cannot adapt the sub-commands of the paste command, because they are dependant on the state of the sheet,\n     * and may change based on the paste location. A simple example is that paste create new col/rows for the clipboard\n     * content to fit the sheet. So there will be ADD_COL_ROW_COMMANDS in the sub-commands in the history, but repeating\n     * paste might not need them. Or they could only needed for the repeated paste, not for the original.\n     */\n    return {\n        type: \"REPEAT_PASTE\",\n        pasteOption: deepCopy(cmd.pasteOption),\n        target: getters.getSelectedZones(),\n    };\n}\nfunction repeatGroupHeadersCommand(getters, cmd) {\n    const currentSelection = getters.getSelectedZone();\n    return {\n        ...repeatSheetDependantCommand(getters, cmd),\n        start: cmd.dimension === \"COL\" ? currentSelection.left : currentSelection.top,\n        end: cmd.dimension === \"COL\" ? currentSelection.right : currentSelection.bottom,\n    };\n}\n\n/**\n *  Registry containing all the command that can be repeated on redo, and function to transform them\n *  to the current state of the model.\n *\n * If the transform function is undefined, the command will be transformed using generic transformations.\n * (change the sheetId, the row, the col, the target, the ranges, to the current active sheet & selection)\n *\n */\nconst repeatCommandTransformRegistry = new Registry();\nrepeatCommandTransformRegistry.add(\"UPDATE_CELL\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"CLEAR_CELL\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"CLEAR_CELLS\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"DELETE_CONTENT\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"ADD_MERGE\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"REMOVE_MERGE\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"SET_FORMATTING\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"CLEAR_FORMATTING\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"SET_BORDER\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"CREATE_TABLE\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"REMOVE_TABLE\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"HIDE_SHEET\", genericRepeat);\nrepeatCommandTransformRegistry.add(\"ADD_COLUMNS_ROWS\", repeatAddColumnsRowsCommand);\nrepeatCommandTransformRegistry.add(\"REMOVE_COLUMNS_ROWS\", repeatHeaderElementCommand);\nrepeatCommandTransformRegistry.add(\"HIDE_COLUMNS_ROWS\", repeatHeaderElementCommand);\nrepeatCommandTransformRegistry.add(\"RESIZE_COLUMNS_ROWS\", repeatHeaderElementCommand);\nrepeatCommandTransformRegistry.add(\"CREATE_SHEET\", repeatCreateSheetCommand);\nrepeatCommandTransformRegistry.add(\"CREATE_FIGURE\", repeatCreateFigureCommand);\nrepeatCommandTransformRegistry.add(\"CREATE_CHART\", repeatCreateChartCommand);\nrepeatCommandTransformRegistry.add(\"CREATE_IMAGE\", repeatCreateImageCommand);\nrepeatCommandTransformRegistry.add(\"GROUP_HEADERS\", repeatGroupHeadersCommand);\nrepeatCommandTransformRegistry.add(\"UNGROUP_HEADERS\", repeatGroupHeadersCommand);\nrepeatCommandTransformRegistry.add(\"UNGROUP_HEADERS\", repeatGroupHeadersCommand);\nrepeatCommandTransformRegistry.add(\"UNFOLD_HEADER_GROUPS_IN_ZONE\", repeatZoneDependantCommand);\nrepeatCommandTransformRegistry.add(\"FOLD_HEADER_GROUPS_IN_ZONE\", repeatZoneDependantCommand);\nconst repeatLocalCommandTransformRegistry = new Registry();\nrepeatLocalCommandTransformRegistry.add(\"PASTE\", repeatPasteCommand);\nrepeatLocalCommandTransformRegistry.add(\"INSERT_CELL\", repeatInsertOrDeleteCellCommand);\nrepeatLocalCommandTransformRegistry.add(\"DELETE_CELL\", repeatInsertOrDeleteCellCommand);\nrepeatLocalCommandTransformRegistry.add(\"AUTORESIZE_COLUMNS\", repeatAutoResizeCommand);\nrepeatLocalCommandTransformRegistry.add(\"AUTORESIZE_ROWS\", repeatAutoResizeCommand);\nrepeatLocalCommandTransformRegistry.add(\"SORT_CELLS\", repeatSortCellsCommand);\nrepeatLocalCommandTransformRegistry.add(\"SUM_SELECTION\", genericRepeat);\nrepeatLocalCommandTransformRegistry.add(\"SET_DECIMAL\", genericRepeat);\nfunction genericRepeat(getters, command) {\n    let transformedCommand = deepCopy(command);\n    for (const repeatTransform of genericRepeatsTransforms) {\n        transformedCommand = repeatTransform(getters, transformedCommand);\n    }\n    return transformedCommand;\n}\nfunction repeatCoreCommand(getters, command) {\n    if (!command) {\n        return undefined;\n    }\n    const isRepeatable = repeatCommandTransformRegistry.contains(command.type);\n    if (!isRepeatable) {\n        return undefined;\n    }\n    const transform = repeatCommandTransformRegistry.get(command.type);\n    return transform(getters, command);\n}\nfunction repeatLocalCommand(getters, command, childCommands) {\n    const isRepeatable = repeatLocalCommandTransformRegistry.contains(command.type);\n    if (!isRepeatable) {\n        return undefined;\n    }\n    const repeatTransform = repeatLocalCommandTransformRegistry.get(command.type);\n    return repeatTransform(getters, command, childCommands);\n}\n\nfunction canRepeatRevision(revision) {\n    if (!revision || !revision.rootCommand || typeof revision.rootCommand !== \"object\") {\n        return false;\n    }\n    if (isCoreCommand(revision.rootCommand)) {\n        return repeatCommandTransformRegistry.contains(revision.rootCommand.type);\n    }\n    return repeatLocalCommandTransformRegistry.contains(revision.rootCommand.type);\n}\nfunction repeatRevision(revision, getters) {\n    if (!revision.rootCommand || typeof revision.rootCommand !== \"object\") {\n        return undefined;\n    }\n    if (isCoreCommand(revision.rootCommand)) {\n        return repeatCoreCommand(getters, revision.rootCommand);\n    }\n    return repeatLocalCommand(getters, revision.rootCommand, revision.commands);\n}\n\n/**\n * Local History\n *\n * The local history is responsible of tracking the locally state updates\n * It maintains the local undo and redo stack to allow to undo/redo only local\n * changes\n */\nclass HistoryPlugin extends UIPlugin {\n    static getters = [\"canUndo\", \"canRedo\"];\n    /**\n     * Ids of the revisions which can be undone\n     */\n    undoStack = [];\n    /**\n     * Ids of the revisions which can be redone\n     */\n    redoStack = [];\n    session;\n    constructor(config) {\n        super(config);\n        this.session = config.session;\n        this.session.on(\"new-local-state-update\", this, this.onNewLocalStateUpdate);\n        this.session.on(\"pending-revisions-dropped\", this, ({ revisionIds }) => this.drop(revisionIds));\n        this.session.on(\"snapshot\", this, () => {\n            this.undoStack = [];\n            this.redoStack = [];\n        });\n    }\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"REQUEST_UNDO\":\n                if (!this.canUndo()) {\n                    return \"EmptyUndoStack\" /* CommandResult.EmptyUndoStack */;\n                }\n                break;\n            case \"REQUEST_REDO\":\n                if (!this.canRedo()) {\n                    return \"EmptyRedoStack\" /* CommandResult.EmptyRedoStack */;\n                }\n                break;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"REQUEST_UNDO\":\n            case \"REQUEST_REDO\":\n                // History changes (undo & redo) are *not* applied optimistically on the local state.\n                // We wait a global confirmation from the server. The goal is to avoid handling concurrent\n                // history changes on multiple clients which are very hard to manage correctly.\n                this.requestHistoryChange(cmd.type === \"REQUEST_UNDO\" ? \"UNDO\" : \"REDO\");\n        }\n    }\n    finalize() { }\n    requestHistoryChange(type) {\n        const id = type === \"UNDO\" ? this.undoStack.pop() : this.redoStack.pop();\n        if (!id) {\n            const lastNonRedoRevision = this.getPossibleRevisionToRepeat();\n            if (!lastNonRedoRevision) {\n                return;\n            }\n            const repeatedCommands = repeatRevision(lastNonRedoRevision, this.getters);\n            if (!repeatedCommands) {\n                return;\n            }\n            if (!Array.isArray(repeatedCommands)) {\n                this.dispatch(repeatedCommands.type, repeatedCommands);\n                return;\n            }\n            for (const command of repeatedCommands) {\n                this.dispatch(command.type, command);\n            }\n            return;\n        }\n        if (type === \"UNDO\") {\n            this.session.undo(id);\n            this.redoStack.push(id);\n        }\n        else {\n            this.session.redo(id);\n            this.undoStack.push(id);\n        }\n    }\n    canUndo() {\n        return this.undoStack.length > 0;\n    }\n    canRedo() {\n        if (this.redoStack.length > 0)\n            return true;\n        const lastNonRedoRevision = this.getPossibleRevisionToRepeat();\n        return canRepeatRevision(lastNonRedoRevision);\n    }\n    drop(revisionIds) {\n        this.undoStack = this.undoStack.filter((id) => !revisionIds.includes(id));\n        this.redoStack = [];\n    }\n    onNewLocalStateUpdate({ id }) {\n        this.undoStack.push(id);\n        this.redoStack = [];\n        if (this.undoStack.length > MAX_HISTORY_STEPS) {\n            this.undoStack.shift();\n        }\n    }\n    /**\n     * Fetch the last revision which is not empty and not a repeated command\n     *\n     * Ignore repeated commands (REQUEST_REDO command as root command)\n     * Ignore standard undo/redo revisions (that are empty)\n     */\n    getPossibleRevisionToRepeat() {\n        return this.session.getLastLocalNonEmptyRevision();\n    }\n}\n\nclass SplitToColumnsPlugin extends UIPlugin {\n    static getters = [\"getAutomaticSeparator\"];\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"SPLIT_TEXT_INTO_COLUMNS\":\n                return this.chainValidations(this.batchValidations(this.checkSingleColSelected, this.checkNonEmptySelector), this.batchValidations(this.checkNotOverwritingContent, this.checkSeparatorInSelection))(cmd);\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SPLIT_TEXT_INTO_COLUMNS\":\n                this.splitIntoColumns(cmd);\n                break;\n        }\n    }\n    getAutomaticSeparator() {\n        const cells = this.getters.getSelectedCells();\n        for (const cell of cells) {\n            if (cell.value && cell.type === CellValueType.text) {\n                const separator = this.getAutoSeparatorForString(cell.value);\n                if (separator) {\n                    return separator;\n                }\n            }\n        }\n        return \" \";\n    }\n    getAutoSeparatorForString(str) {\n        const separators = [NEWLINE, \";\", \",\", \" \", \".\"];\n        for (const separator of separators) {\n            if (str.includes(separator)) {\n                return separator;\n            }\n        }\n        return;\n    }\n    splitIntoColumns({ separator, addNewColumns }) {\n        const selection = this.getters.getSelectedZone();\n        const sheetId = this.getters.getActiveSheetId();\n        const splitted = this.getSplittedCols(selection, separator);\n        if (addNewColumns) {\n            this.addColsToAvoidCollisions(selection, splitted);\n        }\n        this.removeMergesInSplitZone(selection, splitted);\n        this.addColumnsToNotOverflowSheet(selection, splitted);\n        for (let i = 0; i < splitted.length; i++) {\n            const row = selection.top + i;\n            const splittedContent = splitted[i];\n            const col = selection.left;\n            const mainCell = this.getters.getCell({ sheetId, col, row });\n            if (splittedContent.length === 1 && splittedContent[0] === mainCell?.content) {\n                continue;\n            }\n            for (const [index, content] of splittedContent.entries()) {\n                this.dispatch(\"UPDATE_CELL\", {\n                    sheetId,\n                    col: col + index,\n                    row,\n                    content: canonicalizeNumberContent(content, this.getters.getLocale()),\n                    format: \"\",\n                    style: mainCell?.style || null,\n                });\n            }\n        }\n    }\n    getSplittedCols(selection, separator) {\n        if (!separator) {\n            throw new Error(\"Separator cannot be empty\");\n        }\n        const sheetId = this.getters.getActiveSheetId();\n        const splitted = [];\n        for (const row of range(selection.top, selection.bottom + 1)) {\n            const text = this.getters.getEvaluatedCell({\n                sheetId,\n                col: selection.left,\n                row,\n            }).formattedValue;\n            splitted.push(this.splitAndRemoveTrailingEmpty(text, separator));\n        }\n        return splitted;\n    }\n    splitAndRemoveTrailingEmpty(string, separator) {\n        const splitted = string.split(separator);\n        while (splitted.length > 1 && splitted[splitted.length - 1] === \"\") {\n            splitted.pop();\n        }\n        return splitted;\n    }\n    willSplittedColsOverwriteContent(selection, splittedCols) {\n        const sheetId = this.getters.getActiveSheetId();\n        for (const row of range(selection.top, selection.bottom + 1)) {\n            const splittedText = splittedCols[row - selection.top];\n            for (let i = 1; i < splittedText.length; i++) {\n                const cell = this.getters.getCell({ sheetId, col: selection.left + i, row });\n                if (cell && cell.content) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n    removeMergesInSplitZone(selection, splittedCols) {\n        const sheetId = this.getters.getActiveSheetId();\n        const colsInSplitZone = Math.max(...splittedCols.map((s) => s.length));\n        const splitZone = { ...selection, right: selection.left + colsInSplitZone - 1 };\n        const merges = this.getters.getMergesInZone(sheetId, splitZone);\n        this.dispatch(\"REMOVE_MERGE\", { sheetId, target: merges });\n    }\n    addColsToAvoidCollisions(selection, splittedCols) {\n        const sheetId = this.getters.getActiveSheetId();\n        let colsToAdd = 0;\n        for (const row of range(selection.top, selection.bottom + 1)) {\n            const cellPosition = { sheetId, col: selection.left, row };\n            const splittedText = splittedCols[row - selection.top];\n            const colsToAddInRow = this.getColsToAddToAvoidCollision(cellPosition, splittedText);\n            colsToAdd = Math.max(colsToAdd, colsToAddInRow);\n        }\n        if (colsToAdd) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"COL\",\n                base: selection.left,\n                sheetId,\n                quantity: colsToAdd,\n                position: \"after\",\n            });\n        }\n    }\n    getColsToAddToAvoidCollision(cellPosition, splittedText) {\n        const maxColumnsToSpread = splittedText.length;\n        for (let i = 1; i < maxColumnsToSpread; i++) {\n            const col = cellPosition.col + i;\n            const cell = this.getters.getCell({ ...cellPosition, col });\n            if (cell && cell.content) {\n                return maxColumnsToSpread - i;\n            }\n        }\n        return 0;\n    }\n    addColumnsToNotOverflowSheet(selection, splittedCols) {\n        const sheetId = this.getters.getActiveSheetId();\n        const maxColumnsToSpread = Math.max(...splittedCols.map((s) => s.length - 1));\n        const maxColIndex = this.getters.getNumberCols(sheetId) - 1;\n        if (selection.left + maxColumnsToSpread > maxColIndex) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"COL\",\n                base: maxColIndex,\n                sheetId,\n                quantity: selection.left + maxColumnsToSpread - maxColIndex,\n                position: \"after\",\n            });\n        }\n    }\n    checkSingleColSelected() {\n        if (!this.getters.isSingleColSelected()) {\n            return \"MoreThanOneColumnSelected\" /* CommandResult.MoreThanOneColumnSelected */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkNonEmptySelector(cmd) {\n        if (cmd.separator === \"\") {\n            return \"EmptySplitSeparator\" /* CommandResult.EmptySplitSeparator */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkNotOverwritingContent(cmd) {\n        if (cmd.addNewColumns || cmd.force) {\n            return \"Success\" /* CommandResult.Success */;\n        }\n        const selection = this.getters.getSelectedZones()[0];\n        const splitted = this.getSplittedCols(selection, cmd.separator);\n        if (this.willSplittedColsOverwriteContent(selection, splitted)) {\n            return \"SplitWillOverwriteContent\" /* CommandResult.SplitWillOverwriteContent */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkSeparatorInSelection({ separator }) {\n        const cells = this.getters.getSelectedCells();\n        for (const cell of cells) {\n            if (cell.formattedValue.includes(separator)) {\n                return \"Success\" /* CommandResult.Success */;\n            }\n        }\n        return \"NoSplitSeparatorInSelection\" /* CommandResult.NoSplitSeparatorInSelection */;\n    }\n}\n\nclass TableAutofillPlugin extends UIPlugin {\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"AUTOFILL_TABLE_COLUMN\":\n                const table = this.getters.getCoreTable(cmd);\n                const cell = this.getters.getCell(cmd);\n                if (!table?.config.automaticAutofill || table.type === \"dynamic\" || !cell?.isFormula) {\n                    return;\n                }\n                const { col, row } = cmd;\n                const tableContentZone = getTableContentZone(table.range.zone, table.config);\n                if (tableContentZone && isInside(col, row, tableContentZone)) {\n                    const top = cmd.autofillRowStart ?? tableContentZone.top;\n                    const bottom = cmd.autofillRowEnd ?? tableContentZone.bottom;\n                    const autofillZone = { ...tableContentZone, top, bottom };\n                    this.autofillTableZone(cmd, autofillZone);\n                }\n                break;\n        }\n    }\n    autofillTableZone(autofillSource, zone) {\n        if (zone.top === zone.bottom) {\n            return;\n        }\n        const { col, row, sheetId } = autofillSource;\n        for (let r = zone.top; r <= zone.bottom; r++) {\n            if (r === row) {\n                continue;\n            }\n            if (this.getters.getEvaluatedCell({ col, row: r, sheetId }).type !== CellValueType.empty) {\n                return;\n            }\n        }\n        // TODO: seems odd that autofill a table column have side effects on the selection. Autofill commands should be\n        // refactored to take the selection as a parameter\n        const oldSelection = {\n            zone: this.getters.getSelectedZone(),\n            cell: this.getters.getActivePosition(),\n        };\n        this.selection.selectCell(col, row);\n        this.dispatch(\"AUTOFILL_SELECT\", { col, row: zone.bottom });\n        this.dispatch(\"AUTOFILL\");\n        this.selection.selectCell(col, row);\n        this.dispatch(\"AUTOFILL_SELECT\", { col, row: zone.top });\n        this.dispatch(\"AUTOFILL\");\n        this.selection.selectZone(oldSelection);\n    }\n}\n\nclass TableResizeUI extends UIPlugin {\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"RESIZE_TABLE\":\n                const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);\n                if (!table) {\n                    return \"TableNotFound\" /* CommandResult.TableNotFound */;\n                }\n                const oldTableZone = table.range.zone;\n                const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;\n                if (newTableZone.top !== oldTableZone.top || newTableZone.left !== oldTableZone.left) {\n                    return \"InvalidTableResize\" /* CommandResult.InvalidTableResize */;\n                }\n                return this.canDispatch(\"UPDATE_TABLE\", { ...cmd }).reasons;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"RESIZE_TABLE\": {\n                const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);\n                this.dispatch(\"UPDATE_TABLE\", { ...cmd });\n                if (!table || !table.config.automaticAutofill)\n                    return;\n                const oldTableZone = table.range.zone;\n                const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;\n                if (newTableZone.bottom >= oldTableZone.bottom) {\n                    for (let col = newTableZone.left; col <= newTableZone.right; col++) {\n                        const autofillSource = { col, row: oldTableZone.bottom, sheetId: cmd.sheetId };\n                        if (this.getters.getCell(autofillSource)?.content.startsWith(\"=\")) {\n                            this.dispatch(\"AUTOFILL_TABLE_COLUMN\", {\n                                ...autofillSource,\n                                autofillRowStart: oldTableZone.bottom,\n                                autofillRowEnd: newTableZone.bottom,\n                            });\n                        }\n                    }\n                    break;\n                }\n            }\n        }\n    }\n}\n\n/**\n * Clipboard Plugin\n *\n * This clipboard manages all cut/copy/paste interactions internal to the\n * application, and with the OS clipboard as well.\n */\nclass ClipboardPlugin extends UIPlugin {\n    static layers = [\"Clipboard\"];\n    static getters = [\n        \"getClipboardContent\",\n        \"getClipboardId\",\n        \"getClipboardTextContent\",\n        \"isCutOperation\",\n    ];\n    status = \"invisible\";\n    originSheetId;\n    copiedData;\n    _isCutOperation = false;\n    clipboardId = new UuidGenerator().uuidv4();\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"CUT\":\n                const zones = this.getters.getSelectedZones();\n                return this.isCutAllowedOn(zones);\n            case \"PASTE_FROM_OS_CLIPBOARD\": {\n                const copiedData = this.convertTextToClipboardData(cmd.clipboardContent.text ?? \"\");\n                const pasteOption = cmd.pasteOption;\n                return this.isPasteAllowed(cmd.target, copiedData, { pasteOption, isCutOperation: false });\n            }\n            case \"PASTE\": {\n                if (!this.copiedData) {\n                    return \"EmptyClipboard\" /* CommandResult.EmptyClipboard */;\n                }\n                const pasteOption = cmd.pasteOption;\n                return this.isPasteAllowed(cmd.target, this.copiedData, {\n                    pasteOption: pasteOption,\n                    isCutOperation: this._isCutOperation,\n                });\n            }\n            case \"COPY_PASTE_CELLS_ABOVE\": {\n                const zones = this.getters.getSelectedZones();\n                if (zones.length > 1 || (zones[0].top === 0 && zones[0].bottom === 0)) {\n                    return \"InvalidCopyPasteSelection\" /* CommandResult.InvalidCopyPasteSelection */;\n                }\n                break;\n            }\n            case \"COPY_PASTE_CELLS_ON_LEFT\": {\n                const zones = this.getters.getSelectedZones();\n                if (zones.length > 1 || (zones[0].left === 0 && zones[0].right === 0)) {\n                    return \"InvalidCopyPasteSelection\" /* CommandResult.InvalidCopyPasteSelection */;\n                }\n                break;\n            }\n            case \"INSERT_CELL\": {\n                const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);\n                const copiedData = this.copy(cut);\n                return this.isPasteAllowed(paste, copiedData, { isCutOperation: true });\n            }\n            case \"DELETE_CELL\": {\n                const { cut, paste } = this.getDeleteCellsTargets(cmd.zone, cmd.shiftDimension);\n                const copiedData = this.copy(cut);\n                return this.isPasteAllowed(paste, copiedData, { isCutOperation: true });\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"COPY\":\n            case \"CUT\":\n                const zones = this.getters.getSelectedZones();\n                this.status = \"visible\";\n                this.originSheetId = this.getters.getActiveSheetId();\n                this.copiedData = this.copy(zones);\n                this._isCutOperation = cmd.type === \"CUT\";\n                break;\n            case \"PASTE_FROM_OS_CLIPBOARD\": {\n                this._isCutOperation = false;\n                this.copiedData =\n                    cmd.clipboardContent.data ||\n                        this.convertTextToClipboardData(cmd.clipboardContent.text ?? \"\");\n                const pasteOption = cmd.pasteOption;\n                this.paste(cmd.target, this.copiedData, {\n                    pasteOption,\n                    selectTarget: true,\n                    isCutOperation: false,\n                });\n                this.status = \"invisible\";\n                break;\n            }\n            case \"PASTE\": {\n                const pasteOption = cmd.pasteOption;\n                this.paste(cmd.target, this.copiedData, {\n                    pasteOption,\n                    selectTarget: true,\n                    isCutOperation: this._isCutOperation,\n                });\n                this.status = \"invisible\";\n                if (this._isCutOperation) {\n                    this.copiedData = undefined;\n                    this._isCutOperation = false;\n                }\n                break;\n            }\n            case \"COPY_PASTE_CELLS_ABOVE\":\n                {\n                    const zone = this.getters.getSelectedZone();\n                    const multipleRowsInSelection = zone.top !== zone.bottom;\n                    const copyTarget = {\n                        ...zone,\n                        bottom: multipleRowsInSelection ? zone.top : zone.top - 1,\n                        top: multipleRowsInSelection ? zone.top : zone.top - 1,\n                    };\n                    this.originSheetId = this.getters.getActiveSheetId();\n                    const copiedData = this.copy([copyTarget]);\n                    this.paste([zone], copiedData, {\n                        isCutOperation: false,\n                        selectTarget: true,\n                    });\n                }\n                break;\n            case \"COPY_PASTE_CELLS_ON_LEFT\":\n                {\n                    const zone = this.getters.getSelectedZone();\n                    const multipleColsInSelection = zone.left !== zone.right;\n                    const copyTarget = {\n                        ...zone,\n                        right: multipleColsInSelection ? zone.left : zone.left - 1,\n                        left: multipleColsInSelection ? zone.left : zone.left - 1,\n                    };\n                    this.originSheetId = this.getters.getActiveSheetId();\n                    const copiedData = this.copy([copyTarget]);\n                    this.paste([zone], copiedData, {\n                        isCutOperation: false,\n                        selectTarget: true,\n                    });\n                }\n                break;\n            case \"CLEAN_CLIPBOARD_HIGHLIGHT\":\n                this.status = \"invisible\";\n                break;\n            case \"DELETE_CELL\": {\n                const { cut, paste } = this.getDeleteCellsTargets(cmd.zone, cmd.shiftDimension);\n                if (!isZoneValid(cut[0])) {\n                    this.dispatch(\"CLEAR_CELLS\", {\n                        target: [cmd.zone],\n                        sheetId: this.getters.getActiveSheetId(),\n                    });\n                    break;\n                }\n                const copiedData = this.copy(cut);\n                this.paste(paste, copiedData, { isCutOperation: true });\n                break;\n            }\n            case \"INSERT_CELL\": {\n                const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);\n                const copiedData = this.copy(cut);\n                this.paste(paste, copiedData, { isCutOperation: true });\n                break;\n            }\n            case \"ADD_COLUMNS_ROWS\": {\n                this.status = \"invisible\";\n                // If we add a col/row inside or before the cut area, we invalidate the clipboard\n                if (this._isCutOperation !== true || cmd.sheetId !== this.copiedData?.sheetId) {\n                    return;\n                }\n                const isClipboardDirty = this.isColRowDirtyingClipboard(cmd.position === \"before\" ? cmd.base : cmd.base + 1, cmd.dimension);\n                if (isClipboardDirty) {\n                    this.copiedData = undefined;\n                }\n                break;\n            }\n            case \"REMOVE_COLUMNS_ROWS\": {\n                this.status = \"invisible\";\n                // If we remove a col/row inside or before the cut area, we invalidate the clipboard\n                if (this._isCutOperation !== true || cmd.sheetId !== this.copiedData?.sheetId) {\n                    return;\n                }\n                for (let el of cmd.elements) {\n                    const isClipboardDirty = this.isColRowDirtyingClipboard(el, cmd.dimension);\n                    if (isClipboardDirty) {\n                        this.copiedData = undefined;\n                        break;\n                    }\n                }\n                this.status = \"invisible\";\n                break;\n            }\n            case \"REPEAT_PASTE\": {\n                this.paste(cmd.target, this.copiedData, {\n                    isCutOperation: false,\n                    pasteOption: cmd.pasteOption,\n                    selectTarget: true,\n                });\n                break;\n            }\n            case \"DELETE_SHEET\":\n                if (this._isCutOperation !== true) {\n                    return;\n                }\n                if (this.originSheetId === cmd.sheetId) {\n                    this.copiedData = undefined;\n                    this.status = \"invisible\";\n                }\n                break;\n            default:\n                if (isCoreCommand(cmd)) {\n                    this.status = \"invisible\";\n                }\n        }\n    }\n    convertTextToClipboardData(clipboardData) {\n        const handlers = this.selectClipboardHandlers({ figureId: true }).concat(this.selectClipboardHandlers({}));\n        let copiedData = {};\n        for (const { handlerName, handler } of handlers) {\n            const data = handler.convertTextToClipboardData(clipboardData);\n            copiedData[handlerName] = data;\n            const minimalKeys = [\"sheetId\", \"cells\", \"zones\", \"figureId\"];\n            for (const key of minimalKeys) {\n                if (data && key in data) {\n                    copiedData[key] = data[key];\n                }\n            }\n        }\n        return copiedData;\n    }\n    selectClipboardHandlers(data) {\n        const handlersRegistry = \"figureId\" in data\n            ? clipboardHandlersRegistries.figureHandlers\n            : clipboardHandlersRegistries.cellHandlers;\n        return handlersRegistry.getKeys().map((handlerName) => {\n            const Handler = handlersRegistry.get(handlerName);\n            return { handlerName, handler: new Handler(this.getters, this.dispatch) };\n        });\n    }\n    isCutAllowedOn(zones) {\n        const clipboardData = this.getClipboardData(zones);\n        for (const { handler } of this.selectClipboardHandlers(clipboardData)) {\n            const result = handler.isCutAllowed(clipboardData);\n            if (result !== \"Success\" /* CommandResult.Success */) {\n                return result;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    isPasteAllowed(target, copiedData, options) {\n        for (const { handler } of this.selectClipboardHandlers(copiedData)) {\n            const result = handler.isPasteAllowed(this.getters.getActiveSheetId(), target, copiedData, {\n                ...options,\n            });\n            if (result !== \"Success\" /* CommandResult.Success */) {\n                return result;\n            }\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    isColRowDirtyingClipboard(position, dimension) {\n        if (!this.copiedData || !this.copiedData.zones) {\n            return false;\n        }\n        const { zones } = this.copiedData;\n        for (let zone of zones) {\n            if (dimension === \"COL\" && position <= zone.right) {\n                return true;\n            }\n            if (dimension === \"ROW\" && position <= zone.bottom) {\n                return true;\n            }\n        }\n        return false;\n    }\n    copy(zones) {\n        let copiedData = {};\n        const clipboardData = this.getClipboardData(zones);\n        for (const { handlerName, handler } of this.selectClipboardHandlers(clipboardData)) {\n            const data = handler.copy(clipboardData);\n            copiedData[handlerName] = data;\n            const minimalKeys = [\"sheetId\", \"cells\", \"zones\", \"figureId\"];\n            for (const key of minimalKeys) {\n                if (data && key in data) {\n                    copiedData[key] = data[key];\n                }\n            }\n        }\n        return copiedData;\n    }\n    paste(zones, copiedData, options) {\n        if (!copiedData) {\n            return;\n        }\n        let zone = undefined;\n        let selectedZones = [];\n        const sheetId = this.getters.getActiveSheetId();\n        let target = {\n            sheetId,\n            zones,\n        };\n        const handlers = this.selectClipboardHandlers(copiedData);\n        for (const { handlerName, handler } of handlers) {\n            const handlerData = copiedData[handlerName];\n            if (!handlerData) {\n                continue;\n            }\n            const currentTarget = handler.getPasteTarget(sheetId, zones, handlerData, options);\n            if (currentTarget.figureId) {\n                target.figureId = currentTarget.figureId;\n            }\n            for (const targetZone of currentTarget.zones) {\n                selectedZones.push(targetZone);\n                if (zone === undefined) {\n                    zone = targetZone;\n                    continue;\n                }\n                zone = union(zone, targetZone);\n            }\n        }\n        if (zone !== undefined) {\n            this.addMissingDimensions(this.getters.getActiveSheetId(), zone.right - zone.left + 1, zone.bottom - zone.top + 1, zone.left, zone.top);\n        }\n        handlers.forEach(({ handlerName, handler }) => {\n            const handlerData = copiedData[handlerName];\n            if (handlerData) {\n                handler.paste(target, handlerData, options);\n            }\n        });\n        if (!options?.selectTarget) {\n            return;\n        }\n        const selection = zones[0];\n        const col = selection.left;\n        const row = selection.top;\n        this.selection.getBackToDefault();\n        this.selection.selectZone({ cell: { col, row }, zone: union(...selectedZones) }, { scrollIntoView: false });\n    }\n    /**\n     * Add columns and/or rows to ensure that col + width and row + height are still\n     * in the sheet\n     */\n    addMissingDimensions(sheetId, width, height, col, row) {\n        const missingRows = height + row - this.getters.getNumberRows(sheetId);\n        if (missingRows > 0) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"ROW\",\n                base: this.getters.getNumberRows(sheetId) - 1,\n                sheetId,\n                quantity: missingRows,\n                position: \"after\",\n            });\n        }\n        const missingCols = width + col - this.getters.getNumberCols(sheetId);\n        if (missingCols > 0) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"COL\",\n                base: this.getters.getNumberCols(sheetId) - 1,\n                sheetId,\n                quantity: missingCols,\n                position: \"after\",\n            });\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    /**\n     * Format the current clipboard to a string suitable for being pasted in other\n     * programs.\n     *\n     * - add a tab character between each consecutive cells\n     * - add a newline character between each line\n     *\n     * Note that it returns \\t if the clipboard is empty. This is necessary for the\n     * clipboard copy event to add it as data, otherwise an empty string is not\n     * considered as a copy content.\n     */\n    getClipboardTextContent() {\n        return this.getPlainTextContent();\n    }\n    getClipboardId() {\n        return this.clipboardId;\n    }\n    getClipboardContent() {\n        return {\n            [ClipboardMIMEType.PlainText]: this.getPlainTextContent(),\n            [ClipboardMIMEType.Html]: this.getHTMLContent(),\n        };\n    }\n    getSheetData() {\n        const data = {\n            version: CURRENT_VERSION,\n            clipboardId: this.clipboardId,\n        };\n        if (this.copiedData && \"figureId\" in this.copiedData) {\n            return data;\n        }\n        return {\n            ...data,\n            ...this.copiedData,\n        };\n    }\n    getPlainTextContent() {\n        if (!this.copiedData?.cells) {\n            return \"\\t\";\n        }\n        return (this.copiedData.cells\n            .map((cells) => {\n            return cells\n                .map((c) => this.getters.shouldShowFormulas() && c?.tokens?.length\n                ? c?.content || \"\"\n                : c.evaluatedCell?.formattedValue || \"\")\n                .join(\"\\t\");\n        })\n            .join(\"\\n\") || \"\\t\");\n    }\n    getHTMLContent() {\n        let innerHTML = \"\";\n        const cells = this.copiedData?.cells;\n        if (!cells) {\n            innerHTML = \"\\t\";\n        }\n        else if (cells.length === 1 && cells[0].length === 1) {\n            innerHTML = `${this.getters.getCellText(cells[0][0].position)}`;\n        }\n        else if (!cells[0][0]) {\n            return \"\";\n        }\n        else {\n            let htmlTable = `<table border=\"1\" style=\"border-collapse:collapse\">`;\n            for (const row of cells) {\n                htmlTable += \"<tr>\";\n                for (const cell of row) {\n                    if (!cell) {\n                        continue;\n                    }\n                    const cssStyle = cssPropertiesToCss(cellStyleToCss(this.getters.getCellComputedStyle(cell.position)));\n                    const cellText = this.getters.getCellText(cell.position);\n                    htmlTable += `<td style=\"${cssStyle}\">` + xmlEscape(cellText) + \"</td>\";\n                }\n                htmlTable += \"</tr>\";\n            }\n            htmlTable += \"</table>\";\n            innerHTML = htmlTable;\n        }\n        const serializedData = JSON.stringify(this.getSheetData());\n        return `<div data-osheet-clipboard='${xmlEscape(serializedData)}'>${innerHTML}</div>`;\n    }\n    isCutOperation() {\n        return this._isCutOperation ?? false;\n    }\n    // ---------------------------------------------------------------------------\n    // Private methods\n    // ---------------------------------------------------------------------------\n    getDeleteCellsTargets(zone, dimension) {\n        const sheetId = this.getters.getActiveSheetId();\n        let cut;\n        if (dimension === \"COL\") {\n            cut = {\n                ...zone,\n                left: zone.right + 1,\n                right: this.getters.getNumberCols(sheetId) - 1,\n            };\n        }\n        else {\n            cut = {\n                ...zone,\n                top: zone.bottom + 1,\n                bottom: this.getters.getNumberRows(sheetId) - 1,\n            };\n        }\n        return { cut: [cut], paste: [zone] };\n    }\n    getInsertCellsTargets(zone, dimension) {\n        const sheetId = this.getters.getActiveSheetId();\n        let cut;\n        let paste;\n        if (dimension === \"COL\") {\n            cut = {\n                ...zone,\n                right: this.getters.getNumberCols(sheetId) - 1,\n            };\n            paste = {\n                ...zone,\n                left: zone.right + 1,\n                right: zone.right + 1,\n            };\n        }\n        else {\n            cut = {\n                ...zone,\n                bottom: this.getters.getNumberRows(sheetId) - 1,\n            };\n            paste = { ...zone, top: zone.bottom + 1, bottom: this.getters.getNumberRows(sheetId) - 1 };\n        }\n        return { cut: [cut], paste: [paste] };\n    }\n    getClipboardData(zones) {\n        const sheetId = this.getters.getActiveSheetId();\n        const selectedFigureId = this.getters.getSelectedFigureId();\n        if (selectedFigureId) {\n            return { figureId: selectedFigureId, sheetId };\n        }\n        return getClipboardDataPositions(sheetId, zones);\n    }\n    // ---------------------------------------------------------------------------\n    // Grid rendering\n    // ---------------------------------------------------------------------------\n    drawLayer(renderingContext) {\n        if (this.status !== \"visible\" || !this.copiedData) {\n            return;\n        }\n        const { sheetId, zones } = this.copiedData;\n        if (sheetId !== this.getters.getActiveSheetId() || !zones || !zones.length) {\n            return;\n        }\n        const { ctx, thinLineWidth } = renderingContext;\n        ctx.setLineDash([8, 5]);\n        ctx.strokeStyle = SELECTION_BORDER_COLOR;\n        ctx.lineWidth = 3.3 * thinLineWidth;\n        for (const zone of zones) {\n            const { x, y, width, height } = this.getters.getVisibleRect(zone);\n            if (width > 0 && height > 0) {\n                ctx.strokeRect(x, y, width, height);\n            }\n        }\n    }\n}\n\nclass FilterEvaluationPlugin extends UIPlugin {\n    static getters = [\n        \"getFilterHiddenValues\",\n        \"getFirstTableInSelection\",\n        \"isRowFiltered\",\n        \"isFilterActive\",\n    ];\n    filterValues = {};\n    hiddenRows = {};\n    isEvaluationDirty = false;\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"UPDATE_FILTER\":\n                if (!this.getters.getFilterId(cmd)) {\n                    return \"FilterNotFound\" /* CommandResult.FilterNotFound */;\n                }\n                break;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"UNDO\":\n            case \"REDO\":\n            case \"UPDATE_CELL\":\n            case \"EVALUATE_CELLS\":\n            case \"ACTIVATE_SHEET\":\n            case \"REMOVE_TABLE\":\n            case \"ADD_COLUMNS_ROWS\":\n            case \"REMOVE_COLUMNS_ROWS\":\n            case \"UPDATE_TABLE\":\n                this.isEvaluationDirty = true;\n                break;\n            case \"START\":\n                for (const sheetId of this.getters.getSheetIds()) {\n                    this.filterValues[sheetId] = {};\n                }\n                break;\n            case \"CREATE_SHEET\":\n                this.filterValues[cmd.sheetId] = {};\n                break;\n            case \"HIDE_COLUMNS_ROWS\":\n            case \"UNHIDE_COLUMNS_ROWS\":\n            case \"GROUP_HEADERS\":\n            case \"UNGROUP_HEADERS\":\n            case \"FOLD_HEADER_GROUP\":\n            case \"UNFOLD_HEADER_GROUP\":\n            case \"FOLD_ALL_HEADER_GROUPS\":\n            case \"UNFOLD_ALL_HEADER_GROUPS\":\n            case \"FOLD_HEADER_GROUPS_IN_ZONE\":\n            case \"UNFOLD_HEADER_GROUPS_IN_ZONE\":\n                this.updateHiddenRows(cmd.sheetId);\n                break;\n            case \"UPDATE_FILTER\":\n                this.updateFilter(cmd);\n                this.updateHiddenRows(cmd.sheetId);\n                break;\n            case \"DUPLICATE_SHEET\":\n                this.filterValues[cmd.sheetIdTo] = deepCopy(this.filterValues[cmd.sheetId]);\n                break;\n            // If we don't handle DELETE_SHEET, on one hand we will have some residual data, on the other hand we keep the data\n            // on DELETE_SHEET followed by undo\n        }\n    }\n    finalize() {\n        if (this.isEvaluationDirty) {\n            for (const sheetId of this.getters.getSheetIds()) {\n                this.updateHiddenRows(sheetId);\n            }\n            this.isEvaluationDirty = false;\n        }\n    }\n    isRowFiltered(sheetId, row) {\n        return !!this.hiddenRows[sheetId]?.has(row);\n    }\n    getFilterHiddenValues(position) {\n        const id = this.getters.getFilterId(position);\n        const sheetId = position.sheetId;\n        if (!id || !this.filterValues[sheetId])\n            return [];\n        return this.filterValues[sheetId][id] || [];\n    }\n    isFilterActive(position) {\n        const id = this.getters.getFilterId(position);\n        const sheetId = position.sheetId;\n        return Boolean(id && this.filterValues[sheetId]?.[id]?.length);\n    }\n    getFirstTableInSelection() {\n        const sheetId = this.getters.getActiveSheetId();\n        const selection = this.getters.getSelectedZones();\n        return this.getters.getTablesOverlappingZones(sheetId, selection)[0];\n    }\n    updateFilter({ col, row, hiddenValues, sheetId }) {\n        const id = this.getters.getFilterId({ sheetId, col, row });\n        if (!id)\n            return;\n        if (!this.filterValues[sheetId])\n            this.filterValues[sheetId] = {};\n        this.filterValues[sheetId][id] = hiddenValues;\n    }\n    updateHiddenRows(sheetId) {\n        const filters = this.getters\n            .getFilters(sheetId)\n            .sort((filter1, filter2) => filter1.rangeWithHeaders.zone.top - filter2.rangeWithHeaders.zone.top);\n        const hiddenRows = new Set();\n        for (let filter of filters) {\n            // Disable filters whose header are hidden\n            if (hiddenRows.has(filter.rangeWithHeaders.zone.top) ||\n                this.getters.isRowHiddenByUser(sheetId, filter.rangeWithHeaders.zone.top)) {\n                continue;\n            }\n            const filteredValues = this.filterValues[sheetId]?.[filter.id]?.map(toLowerCase);\n            const filteredZone = filter.filteredRange?.zone;\n            if (!filteredValues || !filteredZone)\n                continue;\n            for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {\n                const value = this.getCellValueAsString(sheetId, filter.col, row);\n                if (filteredValues.includes(value)) {\n                    hiddenRows.add(row);\n                }\n            }\n        }\n        this.hiddenRows[sheetId] = hiddenRows;\n    }\n    getCellValueAsString(sheetId, col, row) {\n        const value = this.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue;\n        return value.toLowerCase();\n    }\n    exportForExcel(data) {\n        for (const sheetData of data.sheets) {\n            const sheetId = sheetData.id;\n            for (const tableData of sheetData.tables) {\n                const tableZone = toZone(tableData.range);\n                const filters = [];\n                const headerNames = [];\n                for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {\n                    const position = {\n                        sheetId: sheetData.id,\n                        col: tableZone.left + i,\n                        row: tableZone.top,\n                    };\n                    const filteredValues = this.getFilterHiddenValues(position);\n                    const filter = this.getters.getFilter(position);\n                    const valuesInFilterZone = filter?.filteredRange\n                        ? positions(filter.filteredRange.zone).map((position) => this.getters.getEvaluatedCell({ sheetId, ...position }).formattedValue)\n                        : [];\n                    if (filteredValues.length) {\n                        const xlsxDisplayedValues = valuesInFilterZone\n                            .filter((val) => val)\n                            .filter((val) => !filteredValues.includes(val));\n                        filters.push({\n                            colId: i,\n                            displayedValues: [...new Set(xlsxDisplayedValues)],\n                            displayBlanks: !filteredValues.includes(\"\") && valuesInFilterZone.some((val) => !val),\n                        });\n                    }\n                    // In xlsx, column header should ALWAYS be a string and should be unique in the table\n                    const headerString = this.getters.getEvaluatedCell(position).formattedValue;\n                    const headerName = this.getUniqueColNameForExcel(i, headerString, headerNames);\n                    headerNames.push(headerName);\n                    sheetData.cells[toXC(position.col, position.row)] = {\n                        ...sheetData.cells[toXC(position.col, position.row)],\n                        content: headerName,\n                        value: headerName,\n                        isFormula: false,\n                    };\n                }\n                tableData.filters = filters;\n            }\n        }\n    }\n    /**\n     * Get an unique column name for the column at colIndex. If the column name is already in the array of used column names,\n     * concatenate a number to the name until we find a new unique name (eg. \"ColName\" => \"ColName1\" => \"ColName2\" ...)\n     */\n    getUniqueColNameForExcel(colIndex, colName, usedColNames) {\n        if (!colName) {\n            colName = `Column${colIndex}`;\n        }\n        let currentColName = colName;\n        let i = 2;\n        while (usedColNames.includes(currentColName)) {\n            currentColName = colName + String(i);\n            i++;\n        }\n        return currentColName;\n    }\n}\n\n/**\n * SelectionPlugin\n */\nclass GridSelectionPlugin extends UIPlugin {\n    static layers = [\"Selection\"];\n    static getters = [\n        \"getActiveSheet\",\n        \"getActiveSheetId\",\n        \"getActiveCell\",\n        \"getActiveCols\",\n        \"getActiveRows\",\n        \"getCurrentStyle\",\n        \"getSelectedZones\",\n        \"getSelectedZone\",\n        \"getSelectedCells\",\n        \"getSelectedFigureId\",\n        \"getSelection\",\n        \"getActivePosition\",\n        \"getSheetPosition\",\n        \"isSingleColSelected\",\n        \"getElementsFromSelection\",\n        \"tryGetActiveSheetId\",\n        \"isGridSelectionActive\",\n    ];\n    gridSelection = {\n        anchor: {\n            cell: { col: 0, row: 0 },\n            zone: { top: 0, left: 0, bottom: 0, right: 0 },\n        },\n        zones: [{ top: 0, left: 0, bottom: 0, right: 0 }],\n    };\n    selectedFigureId = null;\n    sheetsData = {};\n    moveClient;\n    // This flag is used to avoid to historize the ACTIVE_SHEET command when it's\n    // the main command.\n    activeSheet = null;\n    constructor(config) {\n        super(config);\n        this.moveClient = config.moveClient;\n    }\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"ACTIVATE_SHEET\":\n                try {\n                    const sheet = this.getters.getSheet(cmd.sheetIdTo);\n                    if (!sheet.isVisible) {\n                        return \"SheetIsHidden\" /* CommandResult.SheetIsHidden */;\n                    }\n                    break;\n                }\n                catch (error) {\n                    return \"InvalidSheetId\" /* CommandResult.InvalidSheetId */;\n                }\n            case \"MOVE_COLUMNS_ROWS\":\n                return this.isMoveElementAllowed(cmd);\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    handleEvent(event) {\n        const anchor = event.anchor;\n        let zones = [];\n        switch (event.mode) {\n            case \"overrideSelection\":\n                zones = [anchor.zone];\n                break;\n            case \"updateAnchor\":\n                zones = [...this.gridSelection.zones];\n                const index = zones.findIndex((z) => isEqual(z, event.previousAnchor.zone));\n                if (index >= 0) {\n                    zones[index] = anchor.zone;\n                }\n                break;\n            case \"newAnchor\":\n                zones = [...this.gridSelection.zones, anchor.zone];\n                break;\n        }\n        this.setSelectionMixin(event.anchor, zones);\n        /** Any change to the selection has to be reflected in the selection processor. */\n        this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));\n        const { col, row } = this.gridSelection.anchor.cell;\n        this.moveClient({\n            sheetId: this.getters.getActiveSheetId(),\n            col,\n            row,\n        });\n        this.selectedFigureId = null;\n    }\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ACTIVATE_SHEET\":\n                this.selectedFigureId = null;\n                break;\n            case \"DELETE_FIGURE\":\n                if (this.selectedFigureId === cmd.id) {\n                    this.selectedFigureId = null;\n                }\n                break;\n            case \"DELETE_SHEET\":\n                if (this.selectedFigureId && this.getters.getFigure(cmd.sheetId, this.selectedFigureId)) {\n                    this.selectedFigureId = null;\n                }\n                break;\n        }\n        switch (cmd.type) {\n            case \"START\":\n                const firstSheetId = this.getters.getVisibleSheetIds()[0];\n                this.activateSheet(firstSheetId, firstSheetId);\n                const { col, row } = this.getters.getNextVisibleCellPosition({\n                    sheetId: firstSheetId,\n                    col: 0,\n                    row: 0,\n                });\n                this.selectCell(col, row);\n                this.selection.registerAsDefault(this, this.gridSelection.anchor, {\n                    handleEvent: this.handleEvent.bind(this),\n                });\n                this.moveClient({ sheetId: firstSheetId, col: 0, row: 0 });\n                break;\n            case \"ACTIVATE_SHEET\": {\n                this.activateSheet(cmd.sheetIdFrom, cmd.sheetIdTo);\n                break;\n            }\n            case \"REMOVE_COLUMNS_ROWS\": {\n                const sheetId = this.getters.getActiveSheetId();\n                if (cmd.sheetId === sheetId) {\n                    if (cmd.dimension === \"COL\") {\n                        this.onColumnsRemoved(cmd);\n                    }\n                    else {\n                        this.onRowsRemoved(cmd);\n                    }\n                    const { col, row } = this.gridSelection.anchor.cell;\n                    this.moveClient({ sheetId, col, row });\n                }\n                break;\n            }\n            case \"ADD_COLUMNS_ROWS\": {\n                const sheetId = this.getters.getActiveSheetId();\n                if (cmd.sheetId === sheetId) {\n                    this.onAddElements(cmd);\n                    const { col, row } = this.gridSelection.anchor.cell;\n                    this.moveClient({ sheetId, col, row });\n                }\n                break;\n            }\n            case \"MOVE_COLUMNS_ROWS\":\n                if (cmd.sheetId === this.getActiveSheetId()) {\n                    this.onMoveElements(cmd);\n                }\n                break;\n            case \"SELECT_FIGURE\":\n                this.selectedFigureId = cmd.id;\n                break;\n            case \"ACTIVATE_NEXT_SHEET\":\n                this.activateNextSheet(\"right\");\n                break;\n            case \"ACTIVATE_PREVIOUS_SHEET\":\n                this.activateNextSheet(\"left\");\n                break;\n            case \"HIDE_SHEET\":\n                if (cmd.sheetId === this.getActiveSheetId()) {\n                    this.dispatch(\"ACTIVATE_SHEET\", {\n                        sheetIdFrom: cmd.sheetId,\n                        sheetIdTo: this.getters.getVisibleSheetIds()[0],\n                    });\n                }\n                break;\n            case \"UNDO\":\n            case \"REDO\":\n            case \"DELETE_SHEET\":\n                const deletedSheetIds = Object.keys(this.sheetsData).filter((sheetId) => !this.getters.tryGetSheet(sheetId));\n                for (const sheetId of deletedSheetIds) {\n                    delete this.sheetsData[sheetId];\n                }\n                for (const sheetId in this.sheetsData) {\n                    const gridSelection = this.clipSelection(sheetId, this.sheetsData[sheetId].gridSelection);\n                    this.sheetsData[sheetId] = {\n                        gridSelection: deepCopy(gridSelection),\n                    };\n                }\n                if (!this.getters.tryGetSheet(this.getters.getActiveSheetId())) {\n                    const currentSheetIds = this.getters.getVisibleSheetIds();\n                    this.activeSheet = this.getters.getSheet(currentSheetIds[0]);\n                    if (this.activeSheet.id in this.sheetsData) {\n                        const { anchor } = this.clipSelection(this.activeSheet.id, this.sheetsData[this.activeSheet.id].gridSelection);\n                        this.selectCell(anchor.cell.col, anchor.cell.row);\n                    }\n                    else {\n                        this.selectCell(0, 0);\n                    }\n                    const { col, row } = this.gridSelection.anchor.cell;\n                    this.moveClient({\n                        sheetId: this.getters.getActiveSheetId(),\n                        col,\n                        row,\n                    });\n                }\n                const sheetId = this.getters.getActiveSheetId();\n                this.gridSelection.zones = this.gridSelection.zones.map((z) => this.getters.expandZone(sheetId, z));\n                this.gridSelection.anchor.zone = this.getters.expandZone(sheetId, this.gridSelection.anchor.zone);\n                this.setSelectionMixin(this.gridSelection.anchor, this.gridSelection.zones);\n                this.selectedFigureId = null;\n                break;\n        }\n    }\n    finalize() {\n        /** Any change to the selection has to be  reflected in the selection processor. */\n        this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    isGridSelectionActive() {\n        return this.selection.isListening(this);\n    }\n    getActiveSheet() {\n        return this.activeSheet;\n    }\n    getActiveSheetId() {\n        return this.activeSheet.id;\n    }\n    tryGetActiveSheetId() {\n        return this.activeSheet?.id;\n    }\n    getActiveCell() {\n        return this.getters.getEvaluatedCell(this.getActivePosition());\n    }\n    getActiveCols() {\n        const activeCols = new Set();\n        for (let zone of this.gridSelection.zones) {\n            if (zone.top === 0 &&\n                zone.bottom === this.getters.getNumberRows(this.getters.getActiveSheetId()) - 1) {\n                for (let i = zone.left; i <= zone.right; i++) {\n                    activeCols.add(i);\n                }\n            }\n        }\n        return activeCols;\n    }\n    getActiveRows() {\n        const activeRows = new Set();\n        const sheetId = this.getters.getActiveSheetId();\n        for (let zone of this.gridSelection.zones) {\n            if (zone.left === 0 && zone.right === this.getters.getNumberCols(sheetId) - 1) {\n                for (let i = zone.top; i <= zone.bottom; i++) {\n                    activeRows.add(i);\n                }\n            }\n        }\n        return activeRows;\n    }\n    getCurrentStyle() {\n        const zone = this.getters.getSelectedZone();\n        const sheetId = this.getters.getActiveSheetId();\n        return this.getters.getCellStyle({ sheetId, col: zone.left, row: zone.top });\n    }\n    getSelectedZones() {\n        return deepCopy(this.gridSelection.zones);\n    }\n    getSelectedZone() {\n        return deepCopy(this.gridSelection.anchor.zone);\n    }\n    getSelection() {\n        return deepCopy(this.gridSelection);\n    }\n    getSelectedCells() {\n        const sheetId = this.getters.getActiveSheetId();\n        const cells = [];\n        for (const zone of this.gridSelection.zones) {\n            cells.push(...this.getters.getEvaluatedCellsInZone(sheetId, zone));\n        }\n        return cells;\n    }\n    getSelectedFigureId() {\n        return this.selectedFigureId;\n    }\n    getActivePosition() {\n        return this.getters.getMainCellPosition({\n            sheetId: this.getActiveSheetId(),\n            col: this.gridSelection.anchor.cell.col,\n            row: this.gridSelection.anchor.cell.row,\n        });\n    }\n    getSheetPosition(sheetId) {\n        if (sheetId === this.getters.getActiveSheetId()) {\n            return this.getActivePosition();\n        }\n        else {\n            const sheetData = this.sheetsData[sheetId];\n            return sheetData\n                ? {\n                    sheetId,\n                    col: sheetData.gridSelection.anchor.cell.col,\n                    row: sheetData.gridSelection.anchor.cell.row,\n                }\n                : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });\n        }\n    }\n    isSingleColSelected() {\n        const selection = this.getters.getSelectedZones();\n        if (selection.length !== 1 || selection[0].left !== selection[0].right) {\n            return false;\n        }\n        return true;\n    }\n    /**\n     * Returns a sorted array of indexes of all columns (respectively rows depending\n     * on the dimension parameter) intersected by the currently selected zones.\n     *\n     * example:\n     * assume selectedZones: [{left:0, right: 2, top :2, bottom: 4}, {left:5, right: 6, top :3, bottom: 5}]\n     *\n     * if dimension === \"COL\" => [0,1,2,5,6]\n     * if dimension === \"ROW\" => [2,3,4,5]\n     */\n    getElementsFromSelection(dimension) {\n        if (dimension === \"COL\" && this.getters.getActiveCols().size === 0) {\n            return [];\n        }\n        if (dimension === \"ROW\" && this.getters.getActiveRows().size === 0) {\n            return [];\n        }\n        const zones = this.getters.getSelectedZones();\n        let elements = [];\n        const start = dimension === \"COL\" ? \"left\" : \"top\";\n        const end = dimension === \"COL\" ? \"right\" : \"bottom\";\n        for (const zone of zones) {\n            const zoneRows = Array.from({ length: zone[end] - zone[start] + 1 }, (_, i) => zone[start] + i);\n            elements = elements.concat(zoneRows);\n        }\n        return [...new Set(elements)].sort();\n    }\n    // ---------------------------------------------------------------------------\n    // Other\n    // ---------------------------------------------------------------------------\n    activateSheet(sheetIdFrom, sheetIdTo) {\n        this.setActiveSheet(sheetIdTo);\n        this.sheetsData[sheetIdFrom] = {\n            gridSelection: deepCopy(this.gridSelection),\n        };\n        if (sheetIdTo in this.sheetsData) {\n            Object.assign(this, this.sheetsData[sheetIdTo]);\n            this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));\n        }\n        else {\n            const { col, row } = this.getters.getNextVisibleCellPosition({\n                sheetId: sheetIdTo,\n                col: 0,\n                row: 0,\n            });\n            this.selectCell(col, row);\n        }\n    }\n    /**\n     * Ensure selections are not outside sheet boundaries.\n     * They are clipped to fit inside the sheet if needed.\n     */\n    setSelectionMixin(anchor, zones) {\n        const { anchor: clippedAnchor, zones: clippedZones } = this.clipSelection(this.getters.getActiveSheetId(), { anchor, zones });\n        this.gridSelection.anchor = clippedAnchor;\n        this.gridSelection.zones = uniqueZones(clippedZones);\n    }\n    /**\n     * Change the anchor of the selection active cell to an absolute col and row index.\n     *\n     * This is a non trivial task. We need to stop the editing process and update\n     * properly the current selection.  Also, this method can optionally create a new\n     * range in the selection.\n     */\n    selectCell(col, row) {\n        const sheetId = this.getters.getActiveSheetId();\n        const zone = this.getters.expandZone(sheetId, { left: col, right: col, top: row, bottom: row });\n        this.setSelectionMixin({ zone, cell: { col, row } }, [zone]);\n    }\n    setActiveSheet(id) {\n        const sheet = this.getters.getSheet(id);\n        this.activeSheet = sheet;\n    }\n    activateNextSheet(direction) {\n        const sheetIds = this.getters.getSheetIds();\n        const oldSheetPosition = sheetIds.findIndex((id) => id === this.activeSheet.id);\n        const delta = direction === \"left\" ? sheetIds.length - 1 : 1;\n        const newPosition = (oldSheetPosition + delta) % sheetIds.length;\n        this.dispatch(\"ACTIVATE_SHEET\", {\n            sheetIdFrom: this.getActiveSheetId(),\n            sheetIdTo: sheetIds[newPosition],\n        });\n    }\n    onColumnsRemoved(cmd) {\n        const { cell, zone } = this.gridSelection.anchor;\n        const selectedZone = updateSelectionOnDeletion(zone, \"left\", [...cmd.elements]);\n        let anchorZone = { left: cell.col, right: cell.col, top: cell.row, bottom: cell.row };\n        anchorZone = updateSelectionOnDeletion(anchorZone, \"left\", [...cmd.elements]);\n        const anchor = {\n            cell: {\n                col: anchorZone.left,\n                row: anchorZone.top,\n            },\n            zone: selectedZone,\n        };\n        const selections = this.gridSelection.zones.map((zone) => updateSelectionOnDeletion(zone, \"left\", [...cmd.elements]));\n        this.setSelectionMixin(anchor, selections);\n    }\n    onRowsRemoved(cmd) {\n        const { cell, zone } = this.gridSelection.anchor;\n        const selectedZone = updateSelectionOnDeletion(zone, \"top\", [...cmd.elements]);\n        let anchorZone = { left: cell.col, right: cell.col, top: cell.row, bottom: cell.row };\n        anchorZone = updateSelectionOnDeletion(anchorZone, \"top\", [...cmd.elements]);\n        const anchor = {\n            cell: {\n                col: anchorZone.left,\n                row: anchorZone.top,\n            },\n            zone: selectedZone,\n        };\n        const selections = this.gridSelection.zones.map((zone) => updateSelectionOnDeletion(zone, \"top\", [...cmd.elements]));\n        this.setSelectionMixin(anchor, selections);\n    }\n    onAddElements(cmd) {\n        const start = cmd.dimension === \"COL\" ? \"left\" : \"top\";\n        const anchorZone = updateSelectionOnInsertion(this.gridSelection.anchor.zone, start, cmd.base, cmd.position, cmd.quantity);\n        const selection = this.gridSelection.zones.map((zone) => updateSelectionOnInsertion(zone, start, cmd.base, cmd.position, cmd.quantity));\n        const anchor = {\n            cell: { col: anchorZone.left, row: anchorZone.top },\n            zone: anchorZone,\n        };\n        this.setSelectionMixin(anchor, selection);\n    }\n    onMoveElements(cmd) {\n        const thickness = cmd.elements.length;\n        this.dispatch(\"ADD_COLUMNS_ROWS\", {\n            dimension: cmd.dimension,\n            sheetId: cmd.sheetId,\n            base: cmd.base,\n            quantity: thickness,\n            position: cmd.position,\n        });\n        const isCol = cmd.dimension === \"COL\";\n        const start = cmd.elements[0];\n        const end = cmd.elements[thickness - 1];\n        const isBasedBefore = cmd.base < start;\n        const deltaCol = isBasedBefore && isCol ? thickness : 0;\n        const deltaRow = isBasedBefore && !isCol ? thickness : 0;\n        const target = [\n            {\n                left: isCol ? start + deltaCol : 0,\n                right: isCol ? end + deltaCol : this.getters.getNumberCols(cmd.sheetId) - 1,\n                top: !isCol ? start + deltaRow : 0,\n                bottom: !isCol ? end + deltaRow : this.getters.getNumberRows(cmd.sheetId) - 1,\n            },\n        ];\n        const sheetId = this.getActiveSheetId();\n        const handler = new CellClipboardHandler(this.getters, this.dispatch);\n        const data = handler.copy(getClipboardDataPositions(sheetId, target));\n        if (!data) {\n            return;\n        }\n        const base = isBasedBefore ? cmd.base : cmd.base + 1;\n        const pasteTarget = [\n            {\n                left: isCol ? base : 0,\n                right: isCol ? base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,\n                top: !isCol ? base : 0,\n                bottom: !isCol ? base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,\n            },\n        ];\n        handler.paste({ zones: pasteTarget, sheetId }, data, { isCutOperation: true });\n        const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;\n        let currentIndex = cmd.base;\n        for (const element of toRemove) {\n            const size = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element);\n            this.dispatch(\"RESIZE_COLUMNS_ROWS\", {\n                dimension: cmd.dimension,\n                sheetId: cmd.sheetId,\n                size,\n                elements: [currentIndex],\n            });\n            currentIndex += 1;\n        }\n        this.dispatch(\"REMOVE_COLUMNS_ROWS\", {\n            dimension: cmd.dimension,\n            sheetId: cmd.sheetId,\n            elements: toRemove,\n        });\n    }\n    isMoveElementAllowed(cmd) {\n        const isCol = cmd.dimension === \"COL\";\n        const start = cmd.elements[0];\n        const end = cmd.elements[cmd.elements.length - 1];\n        const id = cmd.sheetId;\n        const doesElementsHaveCommonMerges = isCol\n            ? this.getters.doesColumnsHaveCommonMerges\n            : this.getters.doesRowsHaveCommonMerges;\n        if (doesElementsHaveCommonMerges(id, start - 1, start) ||\n            doesElementsHaveCommonMerges(id, end, end + 1) ||\n            doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {\n            return \"WillRemoveExistingMerge\" /* CommandResult.WillRemoveExistingMerge */;\n        }\n        const headers = [cmd.base, ...cmd.elements];\n        const maxHeaderValue = isCol ? this.getters.getNumberCols(id) : this.getters.getNumberRows(id);\n        if (headers.some((h) => h < 0 || h >= maxHeaderValue)) {\n            return \"InvalidHeaderIndex\" /* CommandResult.InvalidHeaderIndex */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    //-------------------------------------------\n    // Helpers for extensions\n    // ------------------------------------------\n    /**\n     * Clip the selection if it spans outside the sheet\n     */\n    clipSelection(sheetId, selection) {\n        const cols = this.getters.getNumberCols(sheetId) - 1;\n        const rows = this.getters.getNumberRows(sheetId) - 1;\n        const zones = selection.zones.map((z) => {\n            return {\n                left: clip(z.left, 0, cols),\n                right: clip(z.right, 0, cols),\n                top: clip(z.top, 0, rows),\n                bottom: clip(z.bottom, 0, rows),\n            };\n        });\n        const anchorCol = clip(selection.anchor.cell.col, 0, cols);\n        const anchorRow = clip(selection.anchor.cell.row, 0, rows);\n        const anchorZone = {\n            left: clip(selection.anchor.zone.left, 0, cols),\n            right: clip(selection.anchor.zone.right, 0, cols),\n            top: clip(selection.anchor.zone.top, 0, rows),\n            bottom: clip(selection.anchor.zone.bottom, 0, rows),\n        };\n        return {\n            zones,\n            anchor: {\n                cell: { col: anchorCol, row: anchorRow },\n                zone: anchorZone,\n            },\n        };\n    }\n    // ---------------------------------------------------------------------------\n    // Grid rendering\n    // ---------------------------------------------------------------------------\n    drawLayer(renderingContext) {\n        if (this.getters.isDashboard()) {\n            return;\n        }\n        const { ctx, thinLineWidth } = renderingContext;\n        // selection\n        const zones = this.getSelectedZones();\n        ctx.fillStyle = \"#f3f7fe\";\n        const onlyOneCell = zones.length === 1 && zones[0].left === zones[0].right && zones[0].top === zones[0].bottom;\n        ctx.fillStyle = onlyOneCell ? \"#f3f7fe\" : \"#e9f0ff\";\n        ctx.strokeStyle = SELECTION_BORDER_COLOR;\n        ctx.lineWidth = 1.5 * thinLineWidth;\n        for (const zone of zones) {\n            const { x, y, width, height } = this.getters.getVisibleRect(zone);\n            ctx.globalCompositeOperation = \"multiply\";\n            ctx.fillRect(x, y, width, height);\n            ctx.globalCompositeOperation = \"source-over\";\n            ctx.strokeRect(x, y, width, height);\n        }\n        ctx.globalCompositeOperation = \"source-over\";\n        // active zone\n        const position = this.getActivePosition();\n        ctx.strokeStyle = SELECTION_BORDER_COLOR;\n        ctx.lineWidth = 3 * thinLineWidth;\n        let zone;\n        if (this.getters.isInMerge(position)) {\n            zone = this.getters.getMerge(position);\n        }\n        else {\n            zone = positionToZone(position);\n        }\n        const { x, y, width, height } = this.getters.getVisibleRect(zone);\n        if (width > 0 && height > 0) {\n            ctx.strokeRect(x, y, width, height);\n        }\n    }\n}\n\nclass InternalViewport {\n    getters;\n    sheetId;\n    boundaries;\n    top;\n    bottom;\n    left;\n    right;\n    offsetX;\n    offsetY;\n    offsetScrollbarX;\n    offsetScrollbarY;\n    canScrollVertically;\n    canScrollHorizontally;\n    viewportWidth;\n    viewportHeight;\n    offsetCorrectionX;\n    offsetCorrectionY;\n    constructor(getters, sheetId, boundaries, sizeInGrid, options, offsets) {\n        this.getters = getters;\n        this.sheetId = sheetId;\n        this.boundaries = boundaries;\n        this.viewportWidth = sizeInGrid.width;\n        this.viewportHeight = sizeInGrid.height;\n        this.offsetScrollbarX = offsets.x;\n        this.offsetScrollbarY = offsets.y;\n        this.canScrollVertically = options.canScrollVertically;\n        this.canScrollHorizontally = options.canScrollHorizontally;\n        this.offsetCorrectionX = this.getters.getColDimensions(this.sheetId, this.boundaries.left).start;\n        this.offsetCorrectionY = this.getters.getRowDimensions(this.sheetId, this.boundaries.top).start;\n        this.adjustViewportOffsetX();\n        this.adjustViewportOffsetY();\n    }\n    // PUBLIC\n    /** Returns the maximum size (in Pixels) of the viewport relative to its allocated client size\n     * When the viewport grid size is smaller than its client width (resp. height), it will return\n     * the client width (resp. height).\n     */\n    getMaxSize() {\n        const lastCol = this.getters.findLastVisibleColRowIndex(this.sheetId, \"COL\", {\n            first: this.boundaries.left,\n            last: this.boundaries.right,\n        });\n        const lastRow = this.getters.findLastVisibleColRowIndex(this.sheetId, \"ROW\", {\n            first: this.boundaries.top,\n            last: this.boundaries.bottom,\n        });\n        const { end: lastColEnd, size: lastColSize } = this.getters.getColDimensions(this.sheetId, lastCol);\n        const { end: lastRowEnd, size: lastRowSize } = this.getters.getRowDimensions(this.sheetId, lastRow);\n        const leftColIndex = this.searchHeaderIndex(\"COL\", lastColEnd - this.viewportWidth, 0);\n        const leftColSize = this.getters.getColSize(this.sheetId, leftColIndex);\n        const leftRowIndex = this.searchHeaderIndex(\"ROW\", lastRowEnd - this.viewportHeight, 0);\n        const topRowSize = this.getters.getRowSize(this.sheetId, leftRowIndex);\n        let width = lastColEnd - this.offsetCorrectionX;\n        if (this.canScrollHorizontally) {\n            width += Math.max(DEFAULT_CELL_WIDTH, // leave some minimal space to let the user know they scrolled all the way\n            Math.min(leftColSize, this.viewportWidth - lastColSize) // Add pixels that allows the snapping at maximum horizontal scroll\n            );\n            width = Math.max(width, this.viewportWidth); // if the viewport grid size is smaller than its client width, return client width\n        }\n        let height = lastRowEnd - this.offsetCorrectionY;\n        if (this.canScrollVertically) {\n            height += Math.max(DEFAULT_CELL_HEIGHT + 5, // leave some space to let the user know they scrolled all the way\n            Math.min(topRowSize, this.viewportHeight - lastRowSize) // Add pixels that allows the snapping at maximum vertical scroll\n            );\n            height = Math.max(height, this.viewportHeight); // if the viewport grid size is smaller than its client height, return client height\n        }\n        if (lastRowEnd + FOOTER_HEIGHT > height && !this.getters.isReadonly()) {\n            height += FOOTER_HEIGHT;\n        }\n        return { width, height };\n    }\n    /**\n     * Return the index of a column given an offset x, based on the pane left\n     * visible cell.\n     * It returns -1 if no column is found.\n     */\n    getColIndex(x) {\n        if (x < this.offsetCorrectionX || x > this.offsetCorrectionX + this.viewportWidth) {\n            return -1;\n        }\n        return this.searchHeaderIndex(\"COL\", x - this.offsetCorrectionX, this.left);\n    }\n    /**\n     * Return the index of a row given an offset y, based on the pane top\n     * visible cell.\n     * It returns -1 if no row is found.\n     */\n    getRowIndex(y) {\n        if (y < this.offsetCorrectionY || y > this.offsetCorrectionY + this.viewportHeight) {\n            return -1;\n        }\n        return this.searchHeaderIndex(\"ROW\", y - this.offsetCorrectionY, this.top);\n    }\n    /**\n     * This function will make sure that the provided cell position (or current selected position) is part of\n     * the pane that is actually displayed on the client. We therefore adjust the offset of the pane\n     * until it contains the cell completely.\n     */\n    adjustPosition(position) {\n        const sheetId = this.sheetId;\n        const mainCellPosition = this.getters.getMainCellPosition({ sheetId, ...position });\n        const { col, row } = this.getters.getNextVisibleCellPosition(mainCellPosition);\n        if (isInside(col, this.boundaries.top, this.boundaries)) {\n            this.adjustPositionX(col);\n        }\n        if (isInside(this.boundaries.left, row, this.boundaries)) {\n            this.adjustPositionY(row);\n        }\n    }\n    adjustPositionX(targetCol) {\n        const sheetId = this.sheetId;\n        const { end } = this.getters.getColDimensions(sheetId, targetCol);\n        if (this.offsetX + this.offsetCorrectionX + this.viewportWidth < end) {\n            const maxCol = this.getters.getNumberCols(sheetId);\n            let finalTarget = targetCol;\n            while (this.getters.isColHidden(sheetId, finalTarget) && finalTarget < maxCol) {\n                finalTarget++;\n            }\n            const finalTargetEnd = this.getters.getColDimensions(sheetId, finalTarget).end;\n            const startIndex = this.searchHeaderIndex(\"COL\", finalTargetEnd - this.viewportWidth - this.offsetCorrectionX, this.boundaries.left);\n            this.offsetScrollbarX =\n                this.getters.getColDimensions(sheetId, startIndex).end - this.offsetCorrectionX;\n        }\n        else if (this.left > targetCol) {\n            let finalTarget = targetCol;\n            while (this.getters.isColHidden(sheetId, finalTarget) && finalTarget > 0) {\n                finalTarget--;\n            }\n            this.offsetScrollbarX =\n                this.getters.getColDimensions(sheetId, finalTarget).start - this.offsetCorrectionX;\n        }\n        this.adjustViewportZoneX();\n    }\n    adjustPositionY(targetRow) {\n        const sheetId = this.sheetId;\n        const { end } = this.getters.getRowDimensions(sheetId, targetRow);\n        if (this.offsetY + this.viewportHeight + this.offsetCorrectionY < end) {\n            const maxRow = this.getters.getNumberRows(sheetId);\n            let finalTarget = targetRow;\n            while (this.getters.isRowHidden(sheetId, finalTarget) && finalTarget < maxRow) {\n                finalTarget++;\n            }\n            const finalTargetEnd = this.getters.getRowDimensions(sheetId, finalTarget).end;\n            const startIndex = this.searchHeaderIndex(\"ROW\", finalTargetEnd - this.viewportHeight - this.offsetCorrectionY, this.boundaries.top);\n            this.offsetScrollbarY =\n                this.getters.getRowDimensions(sheetId, startIndex).end - this.offsetCorrectionY;\n        }\n        else if (this.top > targetRow) {\n            let finalTarget = targetRow;\n            while (this.getters.isRowHidden(sheetId, finalTarget) && finalTarget > 0) {\n                finalTarget--;\n            }\n            this.offsetScrollbarY =\n                this.getters.getRowDimensions(sheetId, finalTarget).start - this.offsetCorrectionY;\n        }\n        this.adjustViewportZoneY();\n    }\n    willNewOffsetScrollViewport(offsetX, offsetY) {\n        return ((this.canScrollHorizontally && this.offsetScrollbarX !== offsetX) ||\n            (this.canScrollVertically && this.offsetScrollbarY !== offsetY));\n    }\n    setViewportOffset(offsetX, offsetY) {\n        this.setViewportOffsetX(offsetX);\n        this.setViewportOffsetY(offsetY);\n    }\n    adjustViewportZone() {\n        this.adjustViewportZoneX();\n        this.adjustViewportZoneY();\n    }\n    /**\n     *\n     * @param zone\n     * @returns Computes the absolute coordinate of a given zone inside the viewport\n     */\n    getRect(zone) {\n        const targetZone = intersection(zone, this);\n        if (targetZone) {\n            const x = this.getters.getColRowOffset(\"COL\", this.left, targetZone.left) + this.offsetCorrectionX;\n            const y = this.getters.getColRowOffset(\"ROW\", this.top, targetZone.top) + this.offsetCorrectionY;\n            const width = Math.min(this.getters.getColRowOffset(\"COL\", targetZone.left, targetZone.right + 1), this.viewportWidth);\n            const height = Math.min(this.getters.getColRowOffset(\"ROW\", targetZone.top, targetZone.bottom + 1), this.viewportHeight);\n            return {\n                x,\n                y,\n                width,\n                height,\n            };\n        }\n        return undefined;\n    }\n    isVisible(col, row) {\n        const isInside = row <= this.bottom && row >= this.top && col >= this.left && col <= this.right;\n        return (isInside &&\n            !this.getters.isColHidden(this.sheetId, col) &&\n            !this.getters.isRowHidden(this.sheetId, row));\n    }\n    searchHeaderIndex(dimension, position, startIndex = 0) {\n        const sheetId = this.sheetId;\n        const headers = this.getters.getNumberHeaders(sheetId, dimension);\n        // using a binary search:\n        let start = startIndex;\n        let end = headers;\n        while (start <= end && start !== headers && end !== -1) {\n            const mid = Math.floor((start + end) / 2);\n            const offset = this.getters.getColRowOffset(dimension, startIndex, mid);\n            const size = this.getters.getHeaderSize(sheetId, dimension, mid);\n            if (position >= offset && position < offset + size) {\n                return mid;\n            }\n            else if (position >= offset + size) {\n                start = mid + 1;\n            }\n            else {\n                end = mid - 1;\n            }\n        }\n        return -1;\n    }\n    setViewportOffsetX(offsetX) {\n        if (!this.canScrollHorizontally) {\n            return;\n        }\n        this.offsetScrollbarX = offsetX;\n        this.adjustViewportZoneX();\n    }\n    setViewportOffsetY(offsetY) {\n        if (!this.canScrollVertically) {\n            return;\n        }\n        this.offsetScrollbarY = offsetY;\n        this.adjustViewportZoneY();\n    }\n    /** Corrects the viewport's horizontal offset based on the current structure\n     *  To make sure that at least on column is visible inside the viewport.\n     */\n    adjustViewportOffsetX() {\n        if (this.canScrollHorizontally) {\n            const { width: viewportWidth } = this.getMaxSize();\n            if (this.viewportWidth + this.offsetScrollbarX > viewportWidth) {\n                this.offsetScrollbarX = Math.max(0, viewportWidth - this.viewportWidth);\n            }\n        }\n        this.adjustViewportZoneX();\n    }\n    /** Corrects the viewport's vertical offset based on the current structure\n     *  To make sure that at least on row is visible inside the viewport.\n     */\n    adjustViewportOffsetY() {\n        if (this.canScrollVertically) {\n            const { height: paneHeight } = this.getMaxSize();\n            if (this.viewportHeight + this.offsetScrollbarY > paneHeight) {\n                this.offsetScrollbarY = Math.max(0, paneHeight - this.viewportHeight);\n            }\n        }\n        this.adjustViewportZoneY();\n    }\n    /** Updates the pane zone and snapped offset based on its horizontal\n     * offset (will find Left) and its width (will find Right) */\n    adjustViewportZoneX() {\n        const sheetId = this.sheetId;\n        this.left = this.searchHeaderIndex(\"COL\", this.offsetScrollbarX, this.boundaries.left);\n        this.right = Math.min(this.boundaries.right, this.searchHeaderIndex(\"COL\", this.viewportWidth, this.left));\n        if (this.left === -1) {\n            this.left = this.boundaries.left;\n        }\n        if (this.right === -1) {\n            this.right = this.getters.getNumberCols(sheetId) - 1;\n        }\n        this.offsetX =\n            this.getters.getColDimensions(sheetId, this.left).start -\n                this.getters.getColDimensions(sheetId, this.boundaries.left).start;\n    }\n    /** Updates the pane zone and snapped offset based on its vertical\n     * offset (will find Top) and its width (will find Bottom) */\n    adjustViewportZoneY() {\n        const sheetId = this.sheetId;\n        this.top = this.searchHeaderIndex(\"ROW\", this.offsetScrollbarY, this.boundaries.top);\n        this.bottom = Math.min(this.boundaries.bottom, this.searchHeaderIndex(\"ROW\", this.viewportHeight, this.top));\n        if (this.top === -1) {\n            this.top = this.boundaries.top;\n        }\n        if (this.bottom === -1) {\n            this.bottom = this.getters.getNumberRows(sheetId) - 1;\n        }\n        this.offsetY =\n            this.getters.getRowDimensions(sheetId, this.top).start -\n                this.getters.getRowDimensions(sheetId, this.boundaries.top).start;\n    }\n}\n\n/**\n *   EdgeScrollCases Schema\n *\n *  The dots/double dots represent a freeze (= a split of viewports)\n *  In this example, we froze vertically between columns D and E\n *  and horizontally between rows 4 and 5.\n *\n *  One can see that we scrolled horizontally from column E to G and\n *  vertically from row 5 to 7.\n *\n *     A  B  C  D   G  H  I  J  K  L  M  N  O  P  Q  R  S  T\n *     _______________________________________________________\n *  1 |           :                                           |\n *  2 |           :                                           |\n *  3 |           :        B   \u2191                 6            |\n *  4 |           :        |   |                 |            |\n *     \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7+\u00b7\u00b7\u00b7+\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7+\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\n *  7 |           :        |   |                 |            |\n *  8 |           :        \u2193   2                 |            |\n *  9 |           :                              |            |\n * 10 |       A --+--\u2192                           |            |\n * 11 |           :                              |            |\n * 12 |           :                              |            |\n * 13 |        \u2190--+-- 1                          |            |\n * 14 |           :                              |        3 --+--\u2192\n * 15 |           :                              |            |\n * 16 |           :                              |            |\n * 17 |       5 --+-------------------------------------------+--\u2192\n * 18 |           :                              |            |\n * 19 |           :                  4           |            |\n * 20 |           :                  |           |            |\n *     ______________________________+___________| ____________\n *                                   |           |\n *                                   \u2193           \u2193\n */\n/**\n * Viewport plugin.\n *\n * This plugin manages all things related to all viewport states.\n *\n */\nclass SheetViewPlugin extends UIPlugin {\n    static getters = [\n        \"getColIndex\",\n        \"getRowIndex\",\n        \"getActiveMainViewport\",\n        \"getSheetViewDimension\",\n        \"getSheetViewDimensionWithHeaders\",\n        \"getMainViewportRect\",\n        \"isVisibleInViewport\",\n        \"getEdgeScrollCol\",\n        \"getEdgeScrollRow\",\n        \"getVisibleFigures\",\n        \"getVisibleRect\",\n        \"getVisibleRectWithoutHeaders\",\n        \"getVisibleCellPositions\",\n        \"getColRowOffsetInViewport\",\n        \"getMainViewportCoordinates\",\n        \"getActiveSheetScrollInfo\",\n        \"getActiveSheetDOMScrollInfo\",\n        \"getSheetViewVisibleCols\",\n        \"getSheetViewVisibleRows\",\n        \"getFrozenSheetViewRatio\",\n        \"isPositionVisible\",\n        \"getColDimensionsInViewport\",\n        \"getRowDimensionsInViewport\",\n    ];\n    viewports = {};\n    /**\n     * The viewport dimensions are usually set by one of the components\n     * (i.e. when grid component is mounted) to properly reflect its state in the DOM.\n     * In the absence of a component (standalone model), is it mandatory to set reasonable default values\n     * to ensure the correct operation of this plugin.\n     */\n    sheetViewWidth = getDefaultSheetViewSize();\n    sheetViewHeight = getDefaultSheetViewSize();\n    gridOffsetX = 0;\n    gridOffsetY = 0;\n    sheetsWithDirtyViewports = new Set();\n    shouldAdjustViewports = false;\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"SET_VIEWPORT_OFFSET\":\n                return this.chainValidations(this.checkScrollingDirection, this.checkIfViewportsWillChange)(cmd);\n            case \"RESIZE_SHEETVIEW\":\n                return this.chainValidations(this.checkValuesAreDifferent, this.checkPositiveDimension)(cmd);\n            default:\n                return \"Success\" /* CommandResult.Success */;\n        }\n    }\n    handleEvent(event) {\n        const sheetId = this.getters.getActiveSheetId();\n        if (event.options.scrollIntoView) {\n            let { col, row } = findCellInNewZone(event.previousAnchor.zone, event.anchor.zone);\n            if (event.mode === \"updateAnchor\") {\n                const oldZone = event.previousAnchor.zone;\n                const newZone = event.anchor.zone;\n                // altering a zone should not move the viewport in a dimension that wasn't changed\n                const { top, bottom, left, right } = this.getMainInternalViewport(sheetId);\n                if (oldZone.left === newZone.left && oldZone.right === newZone.right) {\n                    col = left > col || col > right ? left : col;\n                }\n                if (oldZone.top === newZone.top && oldZone.bottom === newZone.bottom) {\n                    row = top > row || row > bottom ? top : row;\n                }\n            }\n            col = Math.min(col, this.getters.getNumberCols(sheetId) - 1);\n            row = Math.min(row, this.getters.getNumberRows(sheetId) - 1);\n            if (!this.sheetsWithDirtyViewports.has(sheetId)) {\n                this.refreshViewport(this.getters.getActiveSheetId(), { col, row });\n            }\n        }\n    }\n    handle(cmd) {\n        // changing the evaluation can hide/show rows because of data filters\n        if (invalidateEvaluationCommands.has(cmd.type)) {\n            for (const sheetId of this.getters.getSheetIds()) {\n                this.sheetsWithDirtyViewports.add(sheetId);\n            }\n        }\n        switch (cmd.type) {\n            case \"START\":\n                this.selection.observe(this, {\n                    handleEvent: this.handleEvent.bind(this),\n                });\n                this.resetViewports(this.getters.getActiveSheetId());\n                break;\n            case \"UNDO\":\n            case \"REDO\":\n                this.cleanViewports();\n                for (const sheetId of this.getters.getSheetIds()) {\n                    this.sheetsWithDirtyViewports.add(sheetId);\n                }\n                this.shouldAdjustViewports = true;\n                break;\n            case \"RESIZE_SHEETVIEW\":\n                this.resizeSheetView(cmd.height, cmd.width, cmd.gridOffsetX, cmd.gridOffsetY);\n                break;\n            case \"SET_VIEWPORT_OFFSET\":\n                this.setSheetViewOffset(cmd.offsetX, cmd.offsetY);\n                break;\n            case \"SHIFT_VIEWPORT_DOWN\":\n                const sheetId = this.getters.getActiveSheetId();\n                const { top, viewportHeight, offsetCorrectionY } = this.getMainInternalViewport(sheetId);\n                const topRowDims = this.getters.getRowDimensions(sheetId, top);\n                this.shiftVertically(topRowDims.start + viewportHeight - offsetCorrectionY);\n                break;\n            case \"SHIFT_VIEWPORT_UP\": {\n                const sheetId = this.getters.getActiveSheetId();\n                const { top, viewportHeight, offsetCorrectionY } = this.getMainInternalViewport(sheetId);\n                const topRowDims = this.getters.getRowDimensions(sheetId, top);\n                this.shiftVertically(topRowDims.end - offsetCorrectionY - viewportHeight);\n                break;\n            }\n            case \"REMOVE_TABLE\":\n            case \"UPDATE_TABLE\":\n            case \"UPDATE_FILTER\":\n                this.sheetsWithDirtyViewports.add(cmd.sheetId);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\":\n            case \"RESIZE_COLUMNS_ROWS\":\n            case \"HIDE_COLUMNS_ROWS\":\n            case \"ADD_COLUMNS_ROWS\":\n            case \"UNHIDE_COLUMNS_ROWS\":\n            case \"UNGROUP_HEADERS\":\n            case \"GROUP_HEADERS\":\n            case \"FOLD_HEADER_GROUP\":\n            case \"UNFOLD_HEADER_GROUP\":\n            case \"FOLD_HEADER_GROUPS_IN_ZONE\":\n            case \"UNFOLD_HEADER_GROUPS_IN_ZONE\":\n            case \"UNFOLD_ALL_HEADER_GROUPS\":\n            case \"FOLD_ALL_HEADER_GROUPS\": {\n                const sheetId = \"sheetId\" in cmd ? cmd.sheetId : this.getters.getActiveSheetId();\n                this.sheetsWithDirtyViewports.add(sheetId);\n                break;\n            }\n            case \"UPDATE_CELL\":\n                // update cell content or format can change hidden rows because of data filters\n                if (\"content\" in cmd || \"format\" in cmd || cmd.style?.fontSize !== undefined) {\n                    for (const sheetId of this.getters.getSheetIds()) {\n                        this.sheetsWithDirtyViewports.add(sheetId);\n                    }\n                }\n                break;\n            case \"DELETE_SHEET\":\n                this.cleanViewports();\n                this.sheetsWithDirtyViewports.delete(cmd.sheetId);\n                break;\n            case \"ACTIVATE_SHEET\":\n                this.sheetsWithDirtyViewports.add(cmd.sheetIdTo);\n                break;\n            case \"UNFREEZE_ROWS\":\n            case \"UNFREEZE_COLUMNS\":\n            case \"FREEZE_COLUMNS\":\n            case \"FREEZE_ROWS\":\n            case \"UNFREEZE_COLUMNS_ROWS\":\n                this.resetViewports(this.getters.getActiveSheetId());\n                break;\n            case \"DELETE_SHEET\":\n                this.sheetsWithDirtyViewports.delete(cmd.sheetId);\n                break;\n            case \"SCROLL_TO_CELL\":\n                this.refreshViewport(this.getters.getActiveSheetId(), { col: cmd.col, row: cmd.row });\n                break;\n        }\n    }\n    finalize() {\n        for (const sheetId of this.sheetsWithDirtyViewports) {\n            this.resetViewports(sheetId);\n            if (this.shouldAdjustViewports) {\n                const position = this.getters.getSheetPosition(sheetId);\n                this.getSubViewports(sheetId).forEach((viewport) => {\n                    viewport.adjustPosition(position);\n                });\n            }\n        }\n        this.sheetsWithDirtyViewports = new Set();\n        this.shouldAdjustViewports = false;\n        this.setViewports();\n    }\n    setViewports() {\n        const sheetIds = this.getters.getSheetIds();\n        for (const sheetId of sheetIds) {\n            if (!this.viewports[sheetId]?.bottomRight) {\n                this.resetViewports(sheetId);\n            }\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------------\n    /**\n     * Return the index of a column given an offset x, based on the viewport left\n     * visible cell.\n     * It returns -1 if no column is found.\n     */\n    getColIndex(x) {\n        const sheetId = this.getters.getActiveSheetId();\n        return Math.max(...this.getSubViewports(sheetId).map((viewport) => viewport.getColIndex(x)));\n    }\n    /**\n     * Return the index of a row given an offset y, based on the viewport top\n     * visible cell.\n     * It returns -1 if no row is found.\n     */\n    getRowIndex(y) {\n        const sheetId = this.getters.getActiveSheetId();\n        return Math.max(...this.getSubViewports(sheetId).map((viewport) => viewport.getRowIndex(y)));\n    }\n    getSheetViewDimensionWithHeaders() {\n        return {\n            width: this.sheetViewWidth + this.gridOffsetX,\n            height: this.sheetViewHeight + this.gridOffsetY,\n        };\n    }\n    getSheetViewDimension() {\n        return {\n            width: this.sheetViewWidth,\n            height: this.sheetViewHeight,\n        };\n    }\n    /** type as pane, not viewport but basically pane extends viewport */\n    getActiveMainViewport() {\n        const sheetId = this.getters.getActiveSheetId();\n        return this.getMainViewport(sheetId);\n    }\n    /**\n     * Return the scroll info of the active sheet, ie. the offset between the viewport left/top side and\n     * the grid left/top side, snapped to the columns/rows.\n     */\n    getActiveSheetScrollInfo() {\n        const sheetId = this.getters.getActiveSheetId();\n        const viewport = this.getMainInternalViewport(sheetId);\n        return {\n            scrollX: viewport.offsetX,\n            scrollY: viewport.offsetY,\n        };\n    }\n    /**\n     * Return the DOM scroll info of the active sheet, ie. the offset between the viewport left/top side and\n     * the grid left/top side, corresponding to the scroll of the scrollbars and not snapped to the grid.\n     */\n    getActiveSheetDOMScrollInfo() {\n        const sheetId = this.getters.getActiveSheetId();\n        const viewport = this.getMainInternalViewport(sheetId);\n        return {\n            scrollX: viewport.offsetScrollbarX,\n            scrollY: viewport.offsetScrollbarY,\n        };\n    }\n    getSheetViewVisibleCols() {\n        const sheetId = this.getters.getActiveSheetId();\n        const viewports = this.getSubViewports(sheetId);\n        //TODO ake another commit to eimprove this\n        return [...new Set(viewports.map((v) => range(v.left, v.right + 1)).flat())].filter((col) => !this.getters.isHeaderHidden(sheetId, \"COL\", col));\n    }\n    getSheetViewVisibleRows() {\n        const sheetId = this.getters.getActiveSheetId();\n        const viewports = this.getSubViewports(sheetId);\n        return [...new Set(viewports.map((v) => range(v.top, v.bottom + 1)).flat())].filter((row) => !this.getters.isHeaderHidden(sheetId, \"ROW\", row));\n    }\n    /**\n     * Get the positions of all the cells that are visible in the viewport, taking merges into account.\n     */\n    getVisibleCellPositions() {\n        const visibleCols = this.getSheetViewVisibleCols();\n        const visibleRows = this.getSheetViewVisibleRows();\n        const sheetId = this.getters.getActiveSheetId();\n        const positions = [];\n        for (const col of visibleCols) {\n            for (const row of visibleRows) {\n                const position = { sheetId, col, row };\n                const mainPosition = this.getters.getMainCellPosition(position);\n                if (mainPosition.row !== row || mainPosition.col !== col) {\n                    continue;\n                }\n                positions.push(position);\n            }\n        }\n        return positions;\n    }\n    /**\n     * Return the main viewport maximum size relative to the client size.\n     */\n    getMainViewportRect() {\n        const sheetId = this.getters.getActiveSheetId();\n        const viewport = this.getMainInternalViewport(sheetId);\n        const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n        let { width, height } = viewport.getMaxSize();\n        const x = this.getters.getColDimensions(sheetId, xSplit).start;\n        const y = this.getters.getRowDimensions(sheetId, ySplit).start;\n        return { x, y, width, height };\n    }\n    getMaximumSheetOffset() {\n        const sheetId = this.getters.getActiveSheetId();\n        const { width, height } = this.getMainViewportRect();\n        const viewport = this.getMainInternalViewport(sheetId);\n        return {\n            maxOffsetX: Math.max(0, width - viewport.viewportWidth + 1),\n            maxOffsetY: Math.max(0, height - viewport.viewportHeight + 1),\n        };\n    }\n    getColRowOffsetInViewport(dimension, referenceIndex, index) {\n        const sheetId = this.getters.getActiveSheetId();\n        const visibleCols = this.getters.getSheetViewVisibleCols();\n        const visibleRows = this.getters.getSheetViewVisibleRows();\n        if (index < referenceIndex) {\n            return -this.getColRowOffsetInViewport(dimension, index, referenceIndex);\n        }\n        let offset = 0;\n        const visibleIndexes = dimension === \"COL\" ? visibleCols : visibleRows;\n        for (let i = referenceIndex; i < index; i++) {\n            if (!visibleIndexes.includes(i)) {\n                continue;\n            }\n            offset += this.getters.getHeaderSize(sheetId, dimension, i);\n        }\n        return offset;\n    }\n    /**\n     * Check if a given position is visible in the viewport.\n     */\n    isVisibleInViewport({ sheetId, col, row }) {\n        return this.getSubViewports(sheetId).some((pane) => pane.isVisible(col, row));\n    }\n    // => returns the new offset\n    getEdgeScrollCol(x, previousX, startingX) {\n        let canEdgeScroll = false;\n        let direction = 0;\n        let delay = 0;\n        /** 4 cases : See EdgeScrollCases Schema at the top\n         * 1. previous in XRight > XLeft\n         * 3. previous in XRight > outside\n         * 5. previous in Left > outside\n         * A. previous in Left > right\n         * with X a position taken in the bottomRIght (aka scrollable) viewport\n         */\n        const { xSplit } = this.getters.getPaneDivisions(this.getters.getActiveSheetId());\n        const { width } = this.getSheetViewDimension();\n        const { x: offsetCorrectionX } = this.getMainViewportCoordinates();\n        const currentOffsetX = this.getActiveSheetScrollInfo().scrollX;\n        if (x > width) {\n            // 3 & 5\n            canEdgeScroll = true;\n            delay = scrollDelay(x - width);\n            direction = 1;\n        }\n        else if (x < offsetCorrectionX && startingX >= offsetCorrectionX && currentOffsetX > 0) {\n            // 1\n            canEdgeScroll = true;\n            delay = scrollDelay(offsetCorrectionX - x);\n            direction = -1;\n        }\n        else if (xSplit && previousX < offsetCorrectionX && x > offsetCorrectionX) {\n            // A\n            canEdgeScroll = true;\n            delay = scrollDelay(x);\n            direction = \"reset\";\n        }\n        return { canEdgeScroll, direction, delay };\n    }\n    getEdgeScrollRow(y, previousY, tartingY) {\n        let canEdgeScroll = false;\n        let direction = 0;\n        let delay = 0;\n        /** 4 cases : See EdgeScrollCases Schema at the top\n         * 2. previous in XBottom > XTop\n         * 4. previous in XRight > outside\n         * 6. previous in Left > outside\n         * B. previous in Left > right\n         * with X a position taken in the bottomRIght (aka scrollable) viewport\n         */\n        const { ySplit } = this.getters.getPaneDivisions(this.getters.getActiveSheetId());\n        const { height } = this.getSheetViewDimension();\n        const { y: offsetCorrectionY } = this.getMainViewportCoordinates();\n        const currentOffsetY = this.getActiveSheetScrollInfo().scrollY;\n        if (y > height) {\n            // 4 & 6\n            canEdgeScroll = true;\n            delay = scrollDelay(y - height);\n            direction = 1;\n        }\n        else if (y < offsetCorrectionY && tartingY >= offsetCorrectionY && currentOffsetY > 0) {\n            // 2\n            canEdgeScroll = true;\n            delay = scrollDelay(offsetCorrectionY - y);\n            direction = -1;\n        }\n        else if (ySplit && previousY < offsetCorrectionY && y > offsetCorrectionY) {\n            // B\n            canEdgeScroll = true;\n            delay = scrollDelay(y);\n            direction = \"reset\";\n        }\n        return { canEdgeScroll, direction, delay };\n    }\n    /**\n     * Computes the coordinates and size to draw the zone on the canvas\n     */\n    getVisibleRect(zone) {\n        const rect = this.getVisibleRectWithoutHeaders(zone);\n        return { ...rect, x: rect.x + this.gridOffsetX, y: rect.y + this.gridOffsetY };\n    }\n    /**\n     * Computes the coordinates and size to draw the zone without taking the grid offset into account\n     */\n    getVisibleRectWithoutHeaders(zone) {\n        const sheetId = this.getters.getActiveSheetId();\n        const viewportRects = this.getSubViewports(sheetId)\n            .map((viewport) => viewport.getRect(zone))\n            .filter(isDefined);\n        if (viewportRects.length === 0) {\n            return { x: 0, y: 0, width: 0, height: 0 };\n        }\n        const x = Math.min(...viewportRects.map((rect) => rect.x));\n        const y = Math.min(...viewportRects.map((rect) => rect.y));\n        const width = Math.max(...viewportRects.map((rect) => rect.x + rect.width)) - x;\n        const height = Math.max(...viewportRects.map((rect) => rect.y + rect.height)) - y;\n        return { x, y, width, height };\n    }\n    /**\n     * Returns the position of the MainViewport relatively to the start of the grid (without headers)\n     * It corresponds to the summed dimensions of the visible cols/rows (in x/y respectively)\n     * situated before the pane divisions.\n     */\n    getMainViewportCoordinates() {\n        const sheetId = this.getters.getActiveSheetId();\n        const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n        const x = this.getters.getColDimensions(sheetId, xSplit).start;\n        const y = this.getters.getRowDimensions(sheetId, ySplit).start;\n        return { x, y };\n    }\n    /**\n     * Returns the size, start and end coordinates of a column relative to the left\n     * column of the current viewport\n     */\n    getColDimensionsInViewport(sheetId, col) {\n        const left = largeMin(this.getters.getSheetViewVisibleCols());\n        const start = this.getters.getColRowOffsetInViewport(\"COL\", left, col);\n        const size = this.getters.getColSize(sheetId, col);\n        const isColHidden = this.getters.isColHidden(sheetId, col);\n        return {\n            start,\n            size: size,\n            end: start + (isColHidden ? 0 : size),\n        };\n    }\n    /**\n     * Returns the size, start and end coordinates of a row relative to the top row\n     * of the current viewport\n     */\n    getRowDimensionsInViewport(sheetId, row) {\n        const top = largeMin(this.getters.getSheetViewVisibleRows());\n        const start = this.getters.getColRowOffsetInViewport(\"ROW\", top, row);\n        const size = this.getters.getRowSize(sheetId, row);\n        const isRowHidden = this.getters.isRowHidden(sheetId, row);\n        return {\n            start,\n            size: size,\n            end: start + (isRowHidden ? 0 : size),\n        };\n    }\n    // ---------------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------------\n    ensureMainViewportExist(sheetId) {\n        if (!this.viewports[sheetId]) {\n            this.resetViewports(sheetId);\n        }\n    }\n    getSubViewports(sheetId) {\n        this.ensureMainViewportExist(sheetId);\n        return Object.values(this.viewports[sheetId]).filter(isDefined);\n    }\n    checkPositiveDimension(cmd) {\n        if (cmd.width < 0 || cmd.height < 0) {\n            return \"InvalidViewportSize\" /* CommandResult.InvalidViewportSize */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkValuesAreDifferent(cmd) {\n        const { height, width } = this.getSheetViewDimension();\n        if (cmd.gridOffsetX === this.gridOffsetX &&\n            cmd.gridOffsetY === this.gridOffsetY &&\n            cmd.width === width &&\n            cmd.height === height) {\n            return \"ValuesNotChanged\" /* CommandResult.ValuesNotChanged */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkScrollingDirection({ offsetX, offsetY, }) {\n        const pane = this.getMainInternalViewport(this.getters.getActiveSheetId());\n        if ((!pane.canScrollHorizontally && offsetX > 0) ||\n            (!pane.canScrollVertically && offsetY > 0)) {\n            return \"InvalidScrollingDirection\" /* CommandResult.InvalidScrollingDirection */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkIfViewportsWillChange({ offsetX, offsetY }) {\n        const sheetId = this.getters.getActiveSheetId();\n        const { maxOffsetX, maxOffsetY } = this.getMaximumSheetOffset();\n        const willScroll = this.getSubViewports(sheetId).some((viewport) => viewport.willNewOffsetScrollViewport(clip(offsetX, 0, maxOffsetX), clip(offsetY, 0, maxOffsetY)));\n        return willScroll ? \"Success\" /* CommandResult.Success */ : \"ViewportScrollLimitsReached\" /* CommandResult.ViewportScrollLimitsReached */;\n    }\n    getMainViewport(sheetId) {\n        const viewport = this.getMainInternalViewport(sheetId);\n        return {\n            top: viewport.top,\n            left: viewport.left,\n            bottom: viewport.bottom,\n            right: viewport.right,\n        };\n    }\n    getMainInternalViewport(sheetId) {\n        this.ensureMainViewportExist(sheetId);\n        return this.viewports[sheetId].bottomRight;\n    }\n    /** gets rid of deprecated sheetIds */\n    cleanViewports() {\n        const sheetIds = this.getters.getSheetIds();\n        for (let sheetId of Object.keys(this.viewports)) {\n            if (!sheetIds.includes(sheetId)) {\n                delete this.viewports[sheetId];\n            }\n        }\n    }\n    resizeSheetView(height, width, gridOffsetX = 0, gridOffsetY = 0) {\n        this.sheetViewHeight = height;\n        this.sheetViewWidth = width;\n        this.gridOffsetX = gridOffsetX;\n        this.gridOffsetY = gridOffsetY;\n        this.recomputeViewports();\n    }\n    recomputeViewports() {\n        for (let sheetId of Object.keys(this.viewports)) {\n            this.resetViewports(sheetId);\n        }\n    }\n    setSheetViewOffset(offsetX, offsetY) {\n        const sheetId = this.getters.getActiveSheetId();\n        const { maxOffsetX, maxOffsetY } = this.getMaximumSheetOffset();\n        this.getSubViewports(sheetId).forEach((viewport) => viewport.setViewportOffset(clip(offsetX, 0, maxOffsetX), clip(offsetY, 0, maxOffsetY)));\n    }\n    getViewportOffset(sheetId) {\n        return {\n            x: this.viewports[sheetId]?.bottomRight.offsetScrollbarX || 0,\n            y: this.viewports[sheetId]?.bottomRight.offsetScrollbarY || 0,\n        };\n    }\n    resetViewports(sheetId) {\n        if (!this.getters.tryGetSheet(sheetId)) {\n            return;\n        }\n        const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n        const nCols = this.getters.getNumberCols(sheetId);\n        const nRows = this.getters.getNumberRows(sheetId);\n        const colOffset = this.getters.getColRowOffset(\"COL\", 0, xSplit, sheetId);\n        const rowOffset = this.getters.getColRowOffset(\"ROW\", 0, ySplit, sheetId);\n        const { xRatio, yRatio } = this.getFrozenSheetViewRatio(sheetId);\n        const canScrollHorizontally = xRatio < 1.0;\n        const canScrollVertically = yRatio < 1.0;\n        const previousOffset = this.getViewportOffset(sheetId);\n        const sheetViewports = {\n            topLeft: (ySplit &&\n                xSplit &&\n                new InternalViewport(this.getters, sheetId, { left: 0, right: xSplit - 1, top: 0, bottom: ySplit - 1 }, { width: colOffset, height: rowOffset }, { canScrollHorizontally: false, canScrollVertically: false }, { x: 0, y: 0 })) ||\n                undefined,\n            topRight: (ySplit &&\n                new InternalViewport(this.getters, sheetId, { left: xSplit, right: nCols - 1, top: 0, bottom: ySplit - 1 }, { width: this.sheetViewWidth - colOffset, height: rowOffset }, { canScrollHorizontally, canScrollVertically: false }, { x: canScrollHorizontally ? previousOffset.x : 0, y: 0 })) ||\n                undefined,\n            bottomLeft: (xSplit &&\n                new InternalViewport(this.getters, sheetId, { left: 0, right: xSplit - 1, top: ySplit, bottom: nRows - 1 }, { width: colOffset, height: this.sheetViewHeight - rowOffset }, { canScrollHorizontally: false, canScrollVertically }, { x: 0, y: canScrollVertically ? previousOffset.y : 0 })) ||\n                undefined,\n            bottomRight: new InternalViewport(this.getters, sheetId, { left: xSplit, right: nCols - 1, top: ySplit, bottom: nRows - 1 }, {\n                width: this.sheetViewWidth - colOffset,\n                height: this.sheetViewHeight - rowOffset,\n            }, { canScrollHorizontally, canScrollVertically }, {\n                x: canScrollHorizontally ? previousOffset.x : 0,\n                y: canScrollVertically ? previousOffset.y : 0,\n            }),\n        };\n        this.viewports[sheetId] = sheetViewports;\n    }\n    /**\n     * Adjust the viewport such that the anchor position is visible\n     */\n    refreshViewport(sheetId, anchorPosition) {\n        this.getSubViewports(sheetId).forEach((viewport) => {\n            viewport.adjustViewportZone();\n            viewport.adjustPosition(anchorPosition);\n        });\n    }\n    /**\n     * Shift the viewport vertically and move the selection anchor\n     * such that it remains at the same place relative to the\n     * viewport top.\n     */\n    shiftVertically(offset) {\n        const sheetId = this.getters.getActiveSheetId();\n        const { top } = this.getMainInternalViewport(sheetId);\n        const { scrollX } = this.getActiveSheetScrollInfo();\n        this.setSheetViewOffset(scrollX, offset);\n        const { anchor } = this.getters.getSelection();\n        if (anchor.cell.row >= this.getters.getPaneDivisions(sheetId).ySplit) {\n            const deltaRow = this.getMainInternalViewport(sheetId).top - top;\n            this.selection.selectCell(anchor.cell.col, anchor.cell.row + deltaRow);\n        }\n    }\n    getVisibleFigures() {\n        const sheetId = this.getters.getActiveSheetId();\n        const result = [];\n        const figures = this.getters.getFigures(sheetId);\n        const { scrollX, scrollY } = this.getActiveSheetScrollInfo();\n        const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();\n        const { width, height } = this.getters.getSheetViewDimensionWithHeaders();\n        for (const figure of figures) {\n            if (figure.x >= offsetCorrectionX &&\n                (figure.x + figure.width <= offsetCorrectionX + scrollX ||\n                    figure.x >= width + scrollX + offsetCorrectionX)) {\n                continue;\n            }\n            if (figure.y >= offsetCorrectionY &&\n                (figure.y + figure.height <= offsetCorrectionY + scrollY ||\n                    figure.y >= height + scrollY + offsetCorrectionY)) {\n                continue;\n            }\n            result.push(figure);\n        }\n        return result;\n    }\n    isPositionVisible(position) {\n        const { scrollX, scrollY } = this.getters.getActiveSheetScrollInfo();\n        const { x: mainViewportX, y: mainViewportY } = this.getters.getMainViewportCoordinates();\n        const { width, height } = this.getters.getSheetViewDimension();\n        if (position.x >= mainViewportX &&\n            (position.x < mainViewportX + scrollX || position.x > width + scrollX + mainViewportX)) {\n            return false;\n        }\n        if (position.y >= mainViewportY &&\n            (position.y < mainViewportY + scrollY || position.y > height + scrollY + mainViewportY)) {\n            return false;\n        }\n        return true;\n    }\n    getFrozenSheetViewRatio(sheetId) {\n        const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);\n        const offsetCorrectionX = this.getters.getColDimensions(sheetId, xSplit).start;\n        const offsetCorrectionY = this.getters.getRowDimensions(sheetId, ySplit).start;\n        const width = this.sheetViewWidth + this.gridOffsetX;\n        const height = this.sheetViewHeight + this.gridOffsetY;\n        return { xRatio: offsetCorrectionX / width, yRatio: offsetCorrectionY / height };\n    }\n}\n\nclass HeaderPositionsUIPlugin extends UIPlugin {\n    static getters = [\"getColDimensions\", \"getRowDimensions\", \"getColRowOffset\"];\n    headerPositions = {};\n    isDirty = true;\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type)) {\n            this.headerPositions = {};\n            this.isDirty = true;\n        }\n        switch (cmd.type) {\n            case \"START\":\n                for (const sheetId of this.getters.getSheetIds()) {\n                    this.headerPositions[sheetId] = this.computeHeaderPositionsOfSheet(sheetId);\n                }\n                break;\n            case \"UPDATE_CELL\":\n                this.headerPositions = {};\n                this.isDirty = true;\n                break;\n            case \"UPDATE_FILTER\":\n            case \"UPDATE_TABLE\":\n            case \"REMOVE_TABLE\":\n                this.headerPositions = {};\n                this.isDirty = true;\n                break;\n            case \"REMOVE_COLUMNS_ROWS\":\n            case \"RESIZE_COLUMNS_ROWS\":\n            case \"HIDE_COLUMNS_ROWS\":\n            case \"ADD_COLUMNS_ROWS\":\n            case \"UNHIDE_COLUMNS_ROWS\":\n            case \"FOLD_HEADER_GROUP\":\n            case \"UNFOLD_HEADER_GROUP\":\n            case \"FOLD_HEADER_GROUPS_IN_ZONE\":\n            case \"UNFOLD_HEADER_GROUPS_IN_ZONE\":\n            case \"UNFOLD_ALL_HEADER_GROUPS\":\n            case \"FOLD_ALL_HEADER_GROUPS\":\n            case \"UNGROUP_HEADERS\":\n            case \"GROUP_HEADERS\":\n            case \"CREATE_SHEET\":\n                this.headerPositions[cmd.sheetId] = this.computeHeaderPositionsOfSheet(cmd.sheetId);\n                break;\n            case \"DUPLICATE_SHEET\":\n                this.headerPositions[cmd.sheetIdTo] = deepCopy(this.headerPositions[cmd.sheetId]);\n                break;\n        }\n    }\n    finalize() {\n        if (this.isDirty) {\n            for (const sheetId of this.getters.getSheetIds()) {\n                this.headerPositions[sheetId] = this.computeHeaderPositionsOfSheet(sheetId);\n            }\n            this.isDirty = false;\n        }\n    }\n    /**\n     * Returns the size, start and end coordinates of a column on an unfolded sheet\n     */\n    getColDimensions(sheetId, col) {\n        const start = this.headerPositions[sheetId][\"COL\"][col];\n        const size = this.getters.getColSize(sheetId, col);\n        const isColHidden = this.getters.isColHidden(sheetId, col);\n        return {\n            start,\n            size,\n            end: start + (isColHidden ? 0 : size),\n        };\n    }\n    /**\n     * Returns the size, start and end coordinates of a row an unfolded sheet\n     */\n    getRowDimensions(sheetId, row) {\n        const start = this.headerPositions[sheetId][\"ROW\"][row];\n        const size = this.getters.getRowSize(sheetId, row);\n        const isRowHidden = this.getters.isRowHidden(sheetId, row);\n        return {\n            start,\n            size: size,\n            end: start + (isRowHidden ? 0 : size),\n        };\n    }\n    /**\n     * Returns the offset of a header (determined by the dimension) at the given index\n     * based on the referenceIndex given. If start === 0, this method will return\n     * the start attribute of the header.\n     *\n     * i.e. The size from A to B is the distance between A.start and B.end\n     */\n    getColRowOffset(dimension, referenceIndex, index, sheetId = this.getters.getActiveSheetId()) {\n        const referencePosition = this.headerPositions[sheetId][dimension][referenceIndex];\n        const position = this.headerPositions[sheetId][dimension][index];\n        return position - referencePosition;\n    }\n    computeHeaderPositionsOfSheet(sheetId) {\n        return {\n            COL: this.computePositions(sheetId, \"COL\"),\n            ROW: this.computePositions(sheetId, \"ROW\"),\n        };\n    }\n    computePositions(sheetId, dimension) {\n        const positions = {};\n        let offset = 0;\n        // loop on number of headers +1 so the position of (last header + 1) is the end of the sheet\n        for (let i = 0; i < this.getters.getNumberHeaders(sheetId, dimension) + 1; i++) {\n            positions[i] = offset;\n            if (this.getters.isHeaderHidden(sheetId, dimension, i)) {\n                continue;\n            }\n            offset += this.getters.getHeaderSize(sheetId, dimension, i);\n        }\n        return positions;\n    }\n}\n\nconst corePluginRegistry = new Registry()\n    .add(\"settings\", SettingsPlugin)\n    .add(\"sheet\", SheetPlugin)\n    .add(\"header grouping\", HeaderGroupingPlugin)\n    .add(\"header visibility\", HeaderVisibilityPlugin)\n    .add(\"tables\", TablePlugin)\n    .add(\"dataValidation\", DataValidationPlugin)\n    .add(\"cell\", CellPlugin)\n    .add(\"merge\", MergePlugin)\n    .add(\"headerSize\", HeaderSizePlugin)\n    .add(\"borders\", BordersPlugin)\n    .add(\"conditional formatting\", ConditionalFormatPlugin)\n    .add(\"figures\", FigurePlugin)\n    .add(\"chart\", ChartPlugin)\n    .add(\"image\", ImagePlugin)\n    .add(\"pivot_core\", PivotCorePlugin)\n    .add(\"spreadsheet_pivot_core\", SpreadsheetPivotCorePlugin)\n    .add(\"tableStyle\", TableStylePlugin);\n// Plugins which handle a specific feature, without handling any core commands\nconst featurePluginRegistry = new Registry()\n    .add(\"ui_sheet\", SheetUIPlugin)\n    .add(\"ui_options\", UIOptionsPlugin)\n    .add(\"autofill\", AutofillPlugin)\n    .add(\"sort\", SortPlugin)\n    .add(\"automatic_sum\", AutomaticSumPlugin)\n    .add(\"format\", FormatPlugin)\n    .add(\"insert_pivot\", InsertPivotPlugin)\n    .add(\"split_to_columns\", SplitToColumnsPlugin)\n    .add(\"collaborative\", CollaborativePlugin)\n    .add(\"history\", HistoryPlugin)\n    .add(\"data_cleanup\", DataCleanupPlugin)\n    .add(\"table_autofill\", TableAutofillPlugin)\n    .add(\"table_ui_resize\", TableResizeUI)\n    .add(\"datavalidation_insert\", DataValidationInsertionPlugin);\n// Plugins which have a state, but which should not be shared in collaborative\nconst statefulUIPluginRegistry = new Registry()\n    .add(\"selection\", GridSelectionPlugin)\n    .add(\"evaluation_filter\", FilterEvaluationPlugin)\n    .add(\"header_visibility_ui\", HeaderVisibilityUIPlugin)\n    .add(\"cell_computed_style\", CellComputedStylePlugin)\n    .add(\"table_computed_style\", TableComputedStylePlugin)\n    .add(\"header_positions\", HeaderPositionsUIPlugin)\n    .add(\"viewport\", SheetViewPlugin)\n    .add(\"clipboard\", ClipboardPlugin);\n// Plugins which have a derived state from core data\nconst coreViewsPluginRegistry = new Registry()\n    .add(\"evaluation\", EvaluationPlugin)\n    .add(\"evaluation_chart\", EvaluationChartPlugin)\n    .add(\"evaluation_cf\", EvaluationConditionalFormatPlugin)\n    .add(\"row_size\", HeaderSizeUIPlugin)\n    .add(\"data_validation_ui\", EvaluationDataValidationPlugin)\n    .add(\"dynamic_tables\", DynamicTablesPlugin)\n    .add(\"custom_colors\", CustomColorsPlugin)\n    .add(\"pivot_ui\", PivotUIPlugin);\n\nconst clickableCellRegistry = new Registry();\nclickableCellRegistry.add(\"link\", {\n    condition: (position, getters) => {\n        return !!getters.getEvaluatedCell(position).link;\n    },\n    execute: (position, env) => openLink(env.model.getters.getEvaluatedCell(position).link, env),\n    sequence: 5,\n});\n\nclass ImageProvider {\n    fileStore;\n    constructor(fileStore) {\n        this.fileStore = fileStore;\n    }\n    async requestImage() {\n        const file = await this.getImageFromUser();\n        const path = await this.fileStore.upload(file);\n        const size = await this.getImageOriginalSize(path);\n        return { path, size, mimetype: file.type };\n    }\n    getImageFromUser() {\n        return new Promise((resolve, reject) => {\n            const input = document.createElement(\"input\");\n            input.setAttribute(\"type\", \"file\");\n            input.setAttribute(\"accept\", \"image/*\");\n            input.addEventListener(\"change\", async () => {\n                if (input.files === null || input.files.length != 1) {\n                    reject();\n                }\n                else {\n                    resolve(input.files[0]);\n                }\n            });\n            input.click();\n        });\n    }\n    getImageOriginalSize(path) {\n        return new Promise((resolve, reject) => {\n            const image = new Image();\n            image.src = path;\n            image.addEventListener(\"load\", () => {\n                const size = { width: image.width, height: image.height };\n                resolve(size);\n            });\n            image.addEventListener(\"error\", reject);\n        });\n    }\n}\n\nconst RIPPLE_KEY_FRAMES = [\n    { transform: \"scale(0)\" },\n    { transform: \"scale(0.8)\", offset: 0.33 },\n    { opacity: \"0\", transform: \"scale(1)\", offset: 1 },\n];\ncss /* scss */ `\n  .o-ripple {\n    z-index: 1;\n  }\n`;\nclass RippleEffect extends Component {\n    static template = \"o-spreadsheet-RippleEffect\";\n    static props = {\n        x: String,\n        y: String,\n        color: String,\n        opacity: Number,\n        duration: Number,\n        width: Number,\n        height: Number,\n        offsetY: Number,\n        offsetX: Number,\n        allowOverflow: Boolean,\n        onAnimationEnd: Function,\n        style: String,\n    };\n    rippleRef = useRef(\"ripple\");\n    setup() {\n        let animation = undefined;\n        onMounted(() => {\n            const rippleEl = this.rippleRef.el;\n            if (!rippleEl || !rippleEl.animate)\n                return;\n            animation = rippleEl.animate(RIPPLE_KEY_FRAMES, {\n                duration: this.props.duration,\n                easing: \"ease-out\",\n            });\n            animation.addEventListener(\"finish\", this.props.onAnimationEnd);\n        });\n        onWillUnmount(() => {\n            animation?.removeEventListener(\"finish\", this.props.onAnimationEnd);\n        });\n    }\n    get rippleStyle() {\n        const { x, y, width, height } = this.props;\n        const offsetX = this.props.offsetX || 0;\n        const offsetY = this.props.offsetY || 0;\n        return cssPropertiesToCss({\n            transform: \"scale(0)\",\n            left: x,\n            top: y,\n            \"margin-left\": `${-width / 2 + offsetX}px`,\n            \"margin-top\": `${-height / 2 + offsetY}px`,\n            width: `${width}px`,\n            height: `${height}px`,\n            background: this.props.color,\n            \"border-radius\": \"100%\",\n            opacity: `${this.props.opacity}`,\n        });\n    }\n}\nclass Ripple extends Component {\n    static template = \"o-spreadsheet-Ripple\";\n    static props = {\n        color: { type: String, optional: true },\n        opacity: { type: Number, optional: true },\n        duration: { type: Number, optional: true },\n        ignoreClickPosition: { type: Boolean, optional: true },\n        width: { type: Number, optional: true },\n        height: { type: Number, optional: true },\n        offsetY: { type: Number, optional: true },\n        offsetX: { type: Number, optional: true },\n        allowOverflow: { type: Boolean, optional: true },\n        enabled: { type: Boolean, optional: true },\n        onAnimationEnd: { type: Function, optional: true },\n        slots: Object,\n        class: { type: String, optional: true },\n    };\n    static components = { RippleEffect };\n    static defaultProps = {\n        color: \"#aaaaaa\",\n        opacity: 0.4,\n        duration: 800,\n        enabled: true,\n        onAnimationEnd: () => { },\n        class: \"\",\n    };\n    childContainer = useRef(\"childContainer\");\n    state = useState({ ripples: [] });\n    currentId = 1;\n    onClick(ev) {\n        if (!this.props.enabled)\n            return;\n        const containerEl = this.childContainer.el;\n        if (!containerEl)\n            return;\n        const rect = this.getRippleChildRectInfo();\n        const { x, y, width, height } = rect;\n        const maxDim = Math.max(width, height);\n        const rippleRect = {\n            x: ev.clientX - x,\n            y: ev.clientY - y,\n            width: this.props.width || maxDim * 2.85,\n            height: this.props.height || maxDim * 2.85,\n        };\n        this.state.ripples.push({ rippleRect, id: this.currentId++ });\n    }\n    getRippleStyle() {\n        const containerEl = this.childContainer.el;\n        if (!containerEl || containerEl.childElementCount !== 1 || !containerEl.firstElementChild) {\n            return \"\";\n        }\n        const rect = this.getRippleChildRectInfo();\n        return cssPropertiesToCss({\n            top: rect.marginTop + \"px\",\n            left: rect.marginLeft + \"px\",\n            width: rect.width + \"px\",\n            height: rect.height + \"px\",\n        });\n    }\n    getRippleChildRectInfo() {\n        const el = this.childContainer.el;\n        if (!el)\n            throw new Error(\"No child container element found\");\n        if (el.childElementCount !== 1 || !el.firstElementChild) {\n            const boundingRect = getBoundingRectAsPOJO(el);\n            return { ...boundingRect, marginLeft: 0, marginTop: 0 };\n        }\n        const childEl = el.firstElementChild;\n        const margins = getElementMargins(childEl);\n        const boundingRect = getBoundingRectAsPOJO(childEl);\n        return {\n            ...boundingRect,\n            marginLeft: margins.left,\n            marginTop: margins.top,\n        };\n    }\n    removeRipple(id) {\n        const index = this.state.ripples.findIndex((r) => r.id === id);\n        if (index === -1)\n            return;\n        this.state.ripples.splice(index, 1);\n    }\n    getRippleEffectProps(id) {\n        const rect = this.state.ripples.find((r) => r.id === id)?.rippleRect;\n        if (!rect)\n            throw new Error(\"Cannot find a ripple with the id \" + id);\n        return {\n            color: this.props.color,\n            opacity: this.props.opacity,\n            duration: this.props.duration,\n            x: this.props.ignoreClickPosition ? \"50%\" : rect.x + \"px\",\n            y: this.props.ignoreClickPosition ? \"50%\" : rect.y + \"px\",\n            width: rect.width,\n            height: rect.height,\n            offsetX: this.props.offsetX || 0,\n            offsetY: this.props.offsetY || 0,\n            allowOverflow: this.props.allowOverflow || false,\n            style: this.getRippleStyle(),\n            onAnimationEnd: () => this.removeRipple(id),\n        };\n    }\n}\n\nfunction interactiveRenameSheet(env, sheetId, name, errorCallback) {\n    const result = env.model.dispatch(\"RENAME_SHEET\", { sheetId, name });\n    if (result.reasons.includes(\"MissingSheetName\" /* CommandResult.MissingSheetName */)) {\n        env.raiseError(_t(\"The sheet name cannot be empty.\"), errorCallback);\n    }\n    else if (result.reasons.includes(\"DuplicatedSheetName\" /* CommandResult.DuplicatedSheetName */)) {\n        env.raiseError(_t(\"A sheet with the name %s already exists. Please select another name.\", name), errorCallback);\n    }\n    else if (result.reasons.includes(\"ForbiddenCharactersInSheetName\" /* CommandResult.ForbiddenCharactersInSheetName */)) {\n        env.raiseError(_t(\"Some used characters are not allowed in a sheet name (Forbidden characters are %s).\", FORBIDDEN_SHEETNAME_CHARS.join(\" \")), errorCallback);\n    }\n}\n\ncss /* scss */ `\n  .o-sheet {\n    padding: 0 15px;\n    padding-right: 10px;\n    height: ${BOTTOMBAR_HEIGHT}px;\n    border-left: 1px solid #c1c1c1;\n    border-right: 1px solid #c1c1c1;\n    margin-left: -1px;\n    cursor: pointer;\n    &:hover {\n      background-color: rgba(0, 0, 0, 0.08);\n    }\n\n    &.active {\n      color: ${ACTION_COLOR};\n      background-color: #ffffff;\n      box-shadow: 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n    }\n\n    .o-sheet-icon {\n      z-index: 1;\n\n      &:hover {\n        background-color: rgba(0, 0, 0, 0.08);\n      }\n    }\n\n    .o-sheet-name {\n      outline: none;\n      padding: 2px 4px;\n\n      &.o-sheet-name-editable {\n        border-radius: 2px;\n        border: 2px solid mediumblue;\n        /* negative margins so nothing moves when the border is added */\n        margin-left: -2px;\n        margin-right: -2px;\n      }\n    }\n\n    .o-sheet-color {\n      bottom: 0;\n      left: 0;\n      height: 6px;\n      z-index: 1;\n      width: calc(100% - 1px);\n    }\n  }\n`;\nclass BottomBarSheet extends Component {\n    static template = \"o-spreadsheet-BottomBarSheet\";\n    static props = {\n        sheetId: String,\n        openContextMenu: Function,\n        style: { type: String, optional: true },\n        onMouseDown: { type: Function, optional: true },\n    };\n    static components = { Ripple, ColorPicker };\n    static defaultProps = {\n        onMouseDown: () => { },\n        style: \"\",\n    };\n    state = useState({ isEditing: false, pickerOpened: false });\n    sheetDivRef = useRef(\"sheetDiv\");\n    sheetNameRef = useRef(\"sheetNameSpan\");\n    editionState = \"initializing\";\n    DOMFocusableElementStore;\n    setup() {\n        onMounted(() => {\n            if (this.isSheetActive) {\n                this.scrollToSheet();\n            }\n        });\n        onPatched(() => {\n            if (this.sheetNameRef.el && this.state.isEditing && this.editionState === \"initializing\") {\n                this.editionState = \"editing\";\n                this.focusInputAndSelectContent();\n            }\n        });\n        this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);\n        useExternalListener(window, \"click\", () => (this.state.pickerOpened = false));\n    }\n    focusInputAndSelectContent() {\n        if (!this.state.isEditing || !this.sheetNameRef.el)\n            return;\n        this.sheetNameRef.el.focus();\n        const selection = window.getSelection();\n        if (selection && this.sheetNameRef.el.firstChild) {\n            selection.setBaseAndExtent(this.sheetNameRef.el.firstChild, 0, this.sheetNameRef.el.firstChild, this.sheetNameRef.el.textContent?.length || 0);\n        }\n    }\n    scrollToSheet() {\n        this.sheetDivRef.el?.scrollIntoView?.();\n    }\n    onFocusOut() {\n        if (this.state.isEditing && this.editionState !== \"initializing\") {\n            this.stopEdition();\n        }\n    }\n    onMouseDown(ev) {\n        this.activateSheet();\n        this.props.onMouseDown(ev);\n    }\n    activateSheet() {\n        this.env.model.dispatch(\"ACTIVATE_SHEET\", {\n            sheetIdFrom: this.env.model.getters.getActiveSheetId(),\n            sheetIdTo: this.props.sheetId,\n        });\n        this.scrollToSheet();\n    }\n    onDblClick() {\n        if (this.env.model.getters.isReadonly()) {\n            return;\n        }\n        this.startEdition();\n    }\n    onKeyDown(ev) {\n        if (!this.state.isEditing)\n            return;\n        if (ev.key === \"Enter\") {\n            ev.preventDefault();\n            this.stopEdition();\n            this.DOMFocusableElementStore.focus();\n        }\n        if (ev.key === \"Escape\") {\n            this.cancelEdition();\n            this.DOMFocusableElementStore.focus();\n        }\n    }\n    onMouseEventSheetName(ev) {\n        if (this.state.isEditing)\n            ev.stopPropagation();\n    }\n    startEdition() {\n        this.state.isEditing = true;\n        this.editionState = \"initializing\";\n    }\n    stopEdition() {\n        const input = this.sheetNameRef.el;\n        if (!this.state.isEditing || !input)\n            return;\n        this.state.isEditing = false;\n        this.editionState = \"initializing\";\n        input.blur();\n        const inputValue = this.getInputContent() || \"\";\n        input.innerText = inputValue;\n        interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());\n    }\n    cancelEdition() {\n        this.state.isEditing = false;\n        this.editionState = \"initializing\";\n        this.sheetNameRef.el?.blur();\n        this.setInputContent(this.sheetName);\n    }\n    onIconClick(ev) {\n        if (!this.isSheetActive) {\n            this.activateSheet();\n        }\n        this.props.openContextMenu(this.contextMenuRegistry, ev);\n    }\n    onContextMenu(ev) {\n        if (!this.isSheetActive) {\n            this.activateSheet();\n        }\n        this.props.openContextMenu(this.contextMenuRegistry, ev);\n    }\n    getInputContent() {\n        return this.sheetNameRef.el?.textContent;\n    }\n    setInputContent(content) {\n        if (this.sheetNameRef.el)\n            this.sheetNameRef.el.textContent = content;\n    }\n    onColorPicked(color) {\n        this.state.pickerOpened = false;\n        this.env.model.dispatch(\"COLOR_SHEET\", { sheetId: this.props.sheetId, color });\n    }\n    get colorPickerAnchorRect() {\n        const button = this.sheetDivRef.el;\n        return getBoundingRectAsPOJO(button);\n    }\n    get contextMenuRegistry() {\n        return getSheetMenuRegistry({\n            renameSheetCallback: () => {\n                this.scrollToSheet();\n                this.startEdition();\n            },\n            openSheetColorPickerCallback: () => {\n                this.state.pickerOpened = true;\n            },\n        });\n    }\n    get isSheetActive() {\n        return this.env.model.getters.getActiveSheetId() === this.props.sheetId;\n    }\n    get sheetName() {\n        return this.env.model.getters.getSheetName(this.props.sheetId);\n    }\n    get sheetColorStyle() {\n        const color = this.env.model.getters.getSheet(this.props.sheetId).color || \"\";\n        return cssPropertiesToCss({ background: color });\n    }\n}\n\nconst selectionStatisticFunctions = [\n    {\n        name: _t(\"Sum\"),\n        types: [CellValueType.number],\n        compute: (values, locale) => sum([[values]], locale),\n    },\n    {\n        name: _t(\"Avg\"),\n        types: [CellValueType.number],\n        compute: (values, locale) => average([[values]], locale),\n    },\n    {\n        name: _t(\"Min\"),\n        types: [CellValueType.number],\n        compute: (values, locale) => min([[values]], locale).value,\n    },\n    {\n        name: _t(\"Max\"),\n        types: [CellValueType.number],\n        compute: (values, locale) => max([[values]], locale).value,\n    },\n    {\n        name: _t(\"Count\"),\n        types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],\n        compute: (values) => countAny([[values]]),\n    },\n    {\n        name: _t(\"Count Numbers\"),\n        types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],\n        compute: (values, locale) => countNumbers([[values]], locale),\n    },\n];\nclass AggregateStatisticsStore extends SpreadsheetStore {\n    statisticFnResults = this._computeStatisticFnResults();\n    isDirty = false;\n    constructor(get) {\n        super(get);\n        this.model.selection.observe(this, {\n            handleEvent: this.handleEvent.bind(this),\n        });\n    }\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            (cmd.type === \"UPDATE_CELL\" && \"content\" in cmd)) {\n            this.isDirty = true;\n        }\n        switch (cmd.type) {\n            case \"HIDE_COLUMNS_ROWS\":\n            case \"UNHIDE_COLUMNS_ROWS\":\n            case \"GROUP_HEADERS\":\n            case \"UNGROUP_HEADERS\":\n            case \"ACTIVATE_SHEET\":\n            case \"ACTIVATE_NEXT_SHEET\":\n            case \"ACTIVATE_PREVIOUS_SHEET\":\n            case \"EVALUATE_CELLS\":\n            case \"UNDO\":\n            case \"REDO\":\n                this.isDirty = true;\n        }\n    }\n    finalize() {\n        if (this.isDirty) {\n            this.isDirty = false;\n            this.statisticFnResults = this._computeStatisticFnResults();\n        }\n    }\n    handleEvent() {\n        if (this.getters.isGridSelectionActive()) {\n            this.statisticFnResults = this._computeStatisticFnResults();\n        }\n    }\n    _computeStatisticFnResults() {\n        const getters = this.getters;\n        const sheetId = getters.getActiveSheetId();\n        const cells = [];\n        const recomputedZones = recomputeZones(getters.getSelectedZones(), []);\n        const heightMax = this.getters.getSheetSize(sheetId).numberOfRows - 1;\n        const widthMax = this.getters.getSheetSize(sheetId).numberOfCols - 1;\n        for (const zone of recomputedZones) {\n            for (let col = zone.left; col <= (zone.right ?? widthMax); col++) {\n                for (let row = zone.top; row <= (zone.bottom ?? heightMax); row++) {\n                    if (getters.isRowHidden(sheetId, row) || getters.isColHidden(sheetId, col)) {\n                        continue; // Skip hidden cells\n                    }\n                    const evaluatedCell = getters.getEvaluatedCell({ sheetId, col, row });\n                    if (evaluatedCell.type !== CellValueType.empty) {\n                        cells.push(evaluatedCell);\n                    }\n                }\n            }\n        }\n        const locale = getters.getLocale();\n        let statisticFnResults = {};\n        const getCells = memoize((typeStr) => {\n            const types = typeStr.split(\",\");\n            return cells.filter((c) => types.includes(c.type));\n        });\n        for (let fn of selectionStatisticFunctions) {\n            // We don't want to display statistical information when there is no interest:\n            // We set the statistical result to undefined if the data handled by the selection\n            // does not match the data handled by the function.\n            // Ex: if there are only texts in the selection, we prefer that the SUM result\n            // be displayed as undefined rather than 0.\n            let fnResult = undefined;\n            const evaluatedCells = getCells(fn.types.sort().join(\",\"));\n            if (evaluatedCells.length) {\n                fnResult = lazy(() => fn.compute(evaluatedCells, locale));\n            }\n            statisticFnResults[fn.name] = fnResult;\n        }\n        return statisticFnResults;\n    }\n}\n\n// -----------------------------------------------------------------------------\n// SpreadSheet\n// -----------------------------------------------------------------------------\ncss /* scss */ `\n  .o-selection-statistic {\n    margin-right: 20px;\n    padding: 4px 4px 4px 8px;\n    color: #333;\n    cursor: pointer;\n    &:hover {\n      background-color: rgba(0, 0, 0, 0.08) !important;\n    }\n  }\n`;\nclass BottomBarStatistic extends Component {\n    static template = \"o-spreadsheet-BottomBarStatistic\";\n    static props = {\n        openContextMenu: Function,\n        closeContextMenu: Function,\n    };\n    static components = { Ripple };\n    selectedStatisticFn = \"\";\n    store;\n    setup() {\n        this.store = useStore(AggregateStatisticsStore);\n        onWillUpdateProps(() => {\n            if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {\n                this.props.closeContextMenu();\n            }\n        });\n    }\n    getSelectedStatistic() {\n        // don't display button if no function has a result\n        if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {\n            return undefined;\n        }\n        if (this.selectedStatisticFn === \"\") {\n            this.selectedStatisticFn = Object.keys(this.store.statisticFnResults)[0];\n        }\n        return this.getComposedFnName(this.selectedStatisticFn);\n    }\n    listSelectionStatistics(ev) {\n        const registry = new MenuItemRegistry();\n        let i = 0;\n        for (let [fnName] of Object.entries(this.store.statisticFnResults)) {\n            registry.add(fnName, {\n                name: () => this.getComposedFnName(fnName),\n                sequence: i,\n                isReadonlyAllowed: true,\n                execute: () => {\n                    this.selectedStatisticFn = fnName;\n                },\n            });\n            i++;\n        }\n        const target = ev.currentTarget;\n        const { top, left, width } = target.getBoundingClientRect();\n        this.props.openContextMenu(left + width, top, registry);\n    }\n    getComposedFnName(fnName) {\n        const locale = this.env.model.getters.getLocale();\n        const fnValue = this.store.statisticFnResults[fnName];\n        return fnName + \": \" + (fnValue !== undefined ? formatValue(fnValue(), { locale }) : \"__\");\n    }\n}\n\n// -----------------------------------------------------------------------------\n// SpreadSheet\n// -----------------------------------------------------------------------------\nconst MENU_MAX_HEIGHT = 250;\ncss /* scss */ `\n  .o-spreadsheet-bottom-bar {\n    background-color: ${BACKGROUND_GRAY_COLOR};\n    padding-left: ${HEADER_WIDTH}px;\n    font-size: 15px;\n    border-top: 1px solid lightgrey;\n\n    .o-sheet-item {\n      cursor: pointer;\n      &:hover {\n        background-color: rgba(0, 0, 0, 0.08);\n      }\n    }\n\n    .o-all-sheets {\n      max-width: 70%;\n      .o-bottom-bar-fade-out {\n        background-image: linear-gradient(-90deg, #cfcfcf, transparent 1%);\n      }\n\n      .o-bottom-bar-fade-in {\n        background-image: linear-gradient(90deg, #cfcfcf, transparent 1%);\n      }\n\n      .o-sheet-list {\n        overflow-y: hidden;\n        overflow-x: auto;\n\n        &::-webkit-scrollbar {\n          display: none; /* Chrome */\n        }\n        -ms-overflow-style: none; /* IE and Edge */\n        scrollbar-width: none; /* Firefox */\n      }\n    }\n\n    .o-bottom-bar-arrows {\n      .o-bottom-bar-arrow {\n        cursor: pointer;\n        &:hover:not([class*=\"o-disabled\"]) {\n          .o-icon {\n            opacity: 0.9;\n          }\n        }\n\n        .o-icon {\n          height: 18px;\n          width: 18px;\n          font-size: 18px;\n        }\n      }\n    }\n  }\n`;\nclass BottomBar extends Component {\n    static template = \"o-spreadsheet-BottomBar\";\n    static props = {\n        onClick: Function,\n    };\n    static components = { Menu, Ripple, BottomBarSheet, BottomBarStatistic };\n    bottomBarRef = useRef(\"bottomBar\");\n    sheetListRef = useRef(\"sheetList\");\n    dragAndDrop = useDragAndDropListItems();\n    targetScroll = undefined;\n    state = useState({\n        isSheetListScrollableLeft: false,\n        isSheetListScrollableRight: false,\n    });\n    menuMaxHeight = MENU_MAX_HEIGHT;\n    menuState = useState({\n        isOpen: false,\n        menuId: undefined,\n        position: null,\n        menuItems: [],\n    });\n    sheetList = this.getVisibleSheets();\n    setup() {\n        onWillUpdateProps(() => {\n            this.updateScrollState();\n            const visibleSheets = this.getVisibleSheets();\n            // Cancel sheet dragging when there is a change in the sheets\n            if (!deepEquals(this.sheetList, visibleSheets)) {\n                this.dragAndDrop.cancel();\n            }\n            this.sheetList = visibleSheets;\n        });\n    }\n    clickAddSheet(ev) {\n        const activeSheetId = this.env.model.getters.getActiveSheetId();\n        const position = this.env.model.getters.getSheetIds().findIndex((sheetId) => sheetId === activeSheetId) + 1;\n        const sheetId = this.env.model.uuidGenerator.uuidv4();\n        const name = this.env.model.getters.getNextSheetName(_t(\"Sheet\"));\n        this.env.model.dispatch(\"CREATE_SHEET\", { sheetId, position, name });\n        this.env.model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom: activeSheetId, sheetIdTo: sheetId });\n    }\n    getVisibleSheets() {\n        return this.env.model.getters.getVisibleSheetIds().map((sheetId) => {\n            const sheet = this.env.model.getters.getSheet(sheetId);\n            return { id: sheet.id, name: sheet.name };\n        });\n    }\n    clickListSheets(ev) {\n        const registry = new MenuItemRegistry();\n        const from = this.env.model.getters.getActiveSheetId();\n        let i = 0;\n        for (const sheetId of this.env.model.getters.getSheetIds()) {\n            const sheet = this.env.model.getters.getSheet(sheetId);\n            registry.add(sheetId, {\n                name: sheet.name,\n                sequence: i,\n                isReadonlyAllowed: true,\n                textColor: sheet.isVisible ? undefined : \"#808080\",\n                execute: (env) => {\n                    if (!this.env.model.getters.isSheetVisible(sheetId)) {\n                        this.env.model.dispatch(\"SHOW_SHEET\", { sheetId });\n                    }\n                    env.model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom: from, sheetIdTo: sheetId });\n                },\n                isEnabled: (env) => (env.model.getters.isReadonly() ? sheet.isVisible : true),\n                icon: sheet.color ? \"o-spreadsheet-Icon.SMALL_DOT_RIGHT_ALIGN\" : undefined,\n                iconColor: sheet.color,\n            });\n            i++;\n        }\n        const target = ev.currentTarget;\n        const { left } = target.getBoundingClientRect();\n        const top = this.bottomBarRef.el.getBoundingClientRect().top;\n        this.openContextMenu(left, top, \"listSheets\", registry);\n    }\n    openContextMenu(x, y, menuId, registry) {\n        this.menuState.isOpen = true;\n        this.menuState.menuId = menuId;\n        this.menuState.menuItems = registry.getMenuItems();\n        this.menuState.position = { x, y };\n    }\n    onSheetContextMenu(sheetId, registry, ev) {\n        const target = ev.currentTarget;\n        const { top, left } = target.getBoundingClientRect();\n        if (ev.closedMenuId === sheetId) {\n            this.closeMenu();\n            return;\n        }\n        this.openContextMenu(left, top, sheetId, registry);\n    }\n    closeMenu() {\n        this.menuState.isOpen = false;\n        this.menuState.menuId = undefined;\n        this.menuState.menuItems = [];\n        this.menuState.position = null;\n    }\n    closeContextMenuWithId(menuId) {\n        if (this.menuState.menuId === menuId) {\n            this.closeMenu();\n        }\n    }\n    onWheel(ev) {\n        this.targetScroll = undefined;\n        const target = ev.currentTarget;\n        target.scrollLeft += ev.deltaY * 0.5;\n    }\n    onScroll() {\n        this.updateScrollState();\n        if (this.targetScroll === this.sheetListCurrentScroll) {\n            this.targetScroll = undefined;\n        }\n    }\n    onArrowLeft(ev) {\n        if (!this.state.isSheetListScrollableLeft)\n            return;\n        if (!this.targetScroll)\n            this.targetScroll = this.sheetListCurrentScroll;\n        const newScroll = this.targetScroll - this.sheetListWidth;\n        this.scrollSheetListTo(Math.max(0, newScroll));\n    }\n    onArrowRight(ev) {\n        if (!this.state.isSheetListScrollableRight)\n            return;\n        if (!this.targetScroll)\n            this.targetScroll = this.sheetListCurrentScroll;\n        const newScroll = this.targetScroll + this.sheetListWidth;\n        this.scrollSheetListTo(Math.min(this.sheetListMaxScroll, newScroll));\n    }\n    updateScrollState() {\n        this.state.isSheetListScrollableLeft = this.sheetListCurrentScroll > 0;\n        this.state.isSheetListScrollableRight = this.sheetListCurrentScroll < this.sheetListMaxScroll;\n    }\n    scrollSheetListTo(scroll) {\n        if (!this.sheetListRef.el)\n            return;\n        this.targetScroll = scroll;\n        this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: \"smooth\" });\n    }\n    onSheetMouseDown(sheetId, event) {\n        if (event.button !== 0 || this.env.model.getters.isReadonly())\n            return;\n        this.closeMenu();\n        const visibleSheets = this.getVisibleSheets();\n        const sheetRects = this.getSheetItemRects();\n        const sheets = visibleSheets.map((sheet, index) => ({\n            id: sheet.id,\n            size: sheetRects[index].width,\n            position: sheetRects[index].x,\n        }));\n        this.dragAndDrop.start(\"horizontal\", {\n            draggedItemId: sheetId,\n            initialMousePosition: event.clientX,\n            items: sheets,\n            containerEl: this.sheetListRef.el,\n            onDragEnd: (sheetId, finalIndex) => this.onDragEnd(sheetId, finalIndex),\n        });\n    }\n    onDragEnd(sheetId, finalIndex) {\n        const originalIndex = this.getVisibleSheets().findIndex((sheet) => sheet.id === sheetId);\n        const delta = finalIndex - originalIndex;\n        if (sheetId && delta !== 0) {\n            this.env.model.dispatch(\"MOVE_SHEET\", {\n                sheetId: sheetId,\n                delta: delta,\n            });\n        }\n    }\n    getSheetStyle(sheetId) {\n        return this.dragAndDrop.itemsStyle[sheetId] || \"\";\n    }\n    getSheetItemRects() {\n        return Array.from(this.bottomBarRef.el.querySelectorAll(`.o-sheet`))\n            .map((sheetEl) => sheetEl.getBoundingClientRect())\n            .map((rect) => ({\n            x: rect.x,\n            width: rect.width - 1, // -1 to compensate negative margin\n            y: rect.y,\n            height: rect.height,\n        }));\n    }\n    get sheetListCurrentScroll() {\n        if (!this.sheetListRef.el)\n            return 0;\n        return this.sheetListRef.el.scrollLeft;\n    }\n    get sheetListWidth() {\n        if (!this.sheetListRef.el)\n            return 0;\n        return this.sheetListRef.el.clientWidth;\n    }\n    get sheetListMaxScroll() {\n        if (!this.sheetListRef.el)\n            return 0;\n        return this.sheetListRef.el.scrollWidth - this.sheetListRef.el.clientWidth;\n    }\n}\n\nclass ClickableCellsStore extends SpreadsheetStore {\n    _clickableCells = markRaw({});\n    _registryItems = markRaw(clickableCellRegistry.getAll().sort((a, b) => a.sequence - b.sequence));\n    handle(cmd) {\n        if (invalidateEvaluationCommands.has(cmd.type) ||\n            cmd.type === \"EVALUATE_CELLS\" ||\n            (cmd.type === \"UPDATE_CELL\" && (\"content\" in cmd || \"format\" in cmd))) {\n            this._clickableCells = markRaw({});\n            this._registryItems = markRaw(clickableCellRegistry.getAll().sort((a, b) => a.sequence - b.sequence));\n        }\n    }\n    getClickableAction(position) {\n        const { sheetId, col, row } = position;\n        const clickableCells = this._clickableCells;\n        const xc = toXC(col, row);\n        if (!clickableCells[sheetId]) {\n            clickableCells[sheetId] = {};\n        }\n        if (!(xc in clickableCells[sheetId])) {\n            clickableCells[sheetId][xc] = this.findClickableAction(position);\n        }\n        return clickableCells[sheetId][xc];\n    }\n    findClickableAction(position) {\n        const getters = this.getters;\n        for (const item of this._registryItems) {\n            if (item.condition(position, getters)) {\n                return item.execute;\n            }\n        }\n        return false;\n    }\n    get clickableCells() {\n        const cells = [];\n        const getters = this.getters;\n        const sheetId = getters.getActiveSheetId();\n        for (const col of getters.getSheetViewVisibleCols()) {\n            for (const row of getters.getSheetViewVisibleRows()) {\n                const position = { sheetId, col, row };\n                if (!getters.isMainCellPosition(position)) {\n                    continue;\n                }\n                const action = this.getClickableAction(position);\n                if (!action) {\n                    continue;\n                }\n                const zone = getters.expandZone(sheetId, positionToZone(position));\n                cells.push({\n                    coordinates: getters.getVisibleRect(zone),\n                    position,\n                    action,\n                });\n            }\n        }\n        return cells;\n    }\n}\n\ncss /* scss */ `\n  .o-dashboard-clickable-cell {\n    position: absolute;\n    cursor: pointer;\n  }\n`;\nclass SpreadsheetDashboard extends Component {\n    static template = \"o-spreadsheet-SpreadsheetDashboard\";\n    static props = {};\n    static components = {\n        GridOverlay,\n        GridPopover,\n        Popover,\n        VerticalScrollBar,\n        HorizontalScrollBar,\n    };\n    cellPopovers;\n    onMouseWheel;\n    canvasPosition;\n    hoveredCell;\n    clickableCellsStore;\n    setup() {\n        const gridRef = useRef(\"grid\");\n        this.canvasPosition = useAbsoluteBoundingRect(gridRef);\n        this.hoveredCell = useStore(HoveredCellStore);\n        this.clickableCellsStore = useStore(ClickableCellsStore);\n        useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });\n        useGridDrawing(\"canvas\", this.env.model, () => this.env.model.getters.getSheetViewDimension());\n        this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {\n            this.moveCanvas(deltaX, deltaY);\n            this.hoveredCell.clear();\n        });\n        this.cellPopovers = useStore(CellPopoverStore);\n    }\n    onCellHovered({ col, row }) {\n        this.hoveredCell.hover({ col, row });\n    }\n    get gridContainer() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const { right } = this.env.model.getters.getSheetZone(sheetId);\n        const { end } = this.env.model.getters.getColDimensions(sheetId, right);\n        return cssPropertiesToCss({ \"max-width\": `${end}px` });\n    }\n    get gridOverlayDimensions() {\n        return cssPropertiesToCss({\n            height: \"100%\",\n            width: \"100%\",\n        });\n    }\n    getCellClickableStyle(coordinates) {\n        return cssPropertiesToCss({\n            top: `${coordinates.y}px`,\n            left: `${coordinates.x}px`,\n            width: `${coordinates.width}px`,\n            height: `${coordinates.height}px`,\n        });\n    }\n    /**\n     * Get all the boxes for the cell in the sheet view that are clickable.\n     * This function is used to render an overlay over each clickable cell in\n     * order to display a pointer cursor.\n     *\n     */\n    getClickableCells() {\n        return toRaw(this.clickableCellsStore.clickableCells);\n    }\n    selectClickableCell(clickableCell) {\n        const { position, action } = clickableCell;\n        action(position, this.env);\n    }\n    onClosePopover() {\n        this.cellPopovers.close();\n    }\n    onGridResized({ height, width }) {\n        this.env.model.dispatch(\"RESIZE_SHEETVIEW\", {\n            width: width,\n            height: height,\n            gridOffsetX: 0,\n            gridOffsetY: 0,\n        });\n    }\n    moveCanvas(deltaX, deltaY) {\n        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();\n        this.env.model.dispatch(\"SET_VIEWPORT_OFFSET\", {\n            offsetX: scrollX + deltaX,\n            offsetY: scrollY + deltaY,\n        });\n    }\n    getGridRect() {\n        return { ...this.canvasPosition, ...this.env.model.getters.getSheetViewDimensionWithHeaders() };\n    }\n}\n\ncss /* scss */ `\n  .o-header-group {\n    .o-header-group-header {\n      z-index: ${ComponentsImportance.HeaderGroupingButton};\n      .o-group-fold-button {\n        cursor: pointer;\n        width: 13px;\n        height: 13px;\n        border: 1px solid ${HEADER_GROUPING_BORDER_COLOR};\n        .o-icon {\n          width: 7px;\n          height: 7px;\n        }\n\n        &:hover {\n          border-color: #777;\n        }\n      }\n    }\n    .o-group-border {\n      box-sizing: border-box;\n    }\n  }\n`;\nclass AbstractHeaderGroup extends Component {\n    static template = \"o-spreadsheet-HeaderGroup\";\n    static props = {\n        group: Object,\n        layerOffset: Number,\n        openContextMenu: Function,\n    };\n    toggleGroup() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const { start, end } = this.props.group;\n        interactiveToggleGroup(this.env, sheetId, this.dimension, start, end);\n    }\n    get groupBoxStyle() {\n        const groupBox = this.groupBox;\n        return cssPropertiesToCss({\n            top: `${groupBox.groupRect.y}px`,\n            left: `${groupBox.groupRect.x}px`,\n            width: `${groupBox.groupRect.width}px`,\n            height: `${groupBox.groupRect.height}px`,\n        });\n    }\n    get groupButtonStyle() {\n        return cssPropertiesToCss({\n            \"background-color\": this.isGroupFolded ? \"#333\" : \"#fff\",\n            color: this.isGroupFolded ? \"#fff\" : \"#333\",\n        });\n    }\n    get groupButtonIcon() {\n        return this.isGroupFolded ? \"o-spreadsheet-Icon.PLUS\" : \"o-spreadsheet-Icon.MINUS\";\n    }\n    get isGroupFolded() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const group = this.props.group;\n        return this.env.model.getters.isGroupFolded(sheetId, this.dimension, group.start, group.end);\n    }\n    onContextMenu(ev) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const position = { x: ev.clientX, y: ev.clientY };\n        const group = this.props.group;\n        const menuItems = getHeaderGroupContextMenu(sheetId, this.dimension, group.start, group.end);\n        this.props.openContextMenu(position, menuItems);\n    }\n}\nclass RowGroup extends AbstractHeaderGroup {\n    dimension = \"ROW\";\n    get groupBorderStyle() {\n        const groupBox = this.groupBox;\n        if (this.groupBox.groupRect.height === 0) {\n            return \"\";\n        }\n        return cssPropertiesToCss({\n            top: `${groupBox.headerRect.height / 2}px`,\n            left: `calc(50% - 1px)`, // -1px: we want the border to be on the center\n            width: `30%`,\n            height: `calc(100% - ${groupBox.headerRect.height / 2}px)`,\n            \"border-left\": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,\n            \"border-bottom\": groupBox.isEndHidden ? \"\" : `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,\n        });\n    }\n    get groupHeaderStyle() {\n        return cssPropertiesToCss({\n            width: `100%`,\n            height: `${this.groupBox.headerRect.height}px`,\n        });\n    }\n    get groupBox() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const { start: startRow, end: endRow } = this.props.group;\n        const startCoordinates = this.env.model.getters.getRowDimensions(sheetId, startRow).start;\n        const endCoordinates = this.env.model.getters.getRowDimensions(sheetId, endRow).end;\n        let groupHeaderY = 0;\n        let groupHeaderHeight = HEADER_HEIGHT;\n        if (startRow !== 0) {\n            const headerRowDims = this.env.model.getters.getRowDimensions(sheetId, startRow - 1);\n            groupHeaderY = HEADER_HEIGHT + headerRowDims.start;\n            groupHeaderHeight = headerRowDims.end - headerRowDims.start;\n        }\n        const headerRect = {\n            x: this.props.layerOffset,\n            y: groupHeaderY,\n            width: GROUP_LAYER_WIDTH,\n            height: groupHeaderHeight,\n        };\n        const groupRect = {\n            x: this.props.layerOffset,\n            y: headerRect.y,\n            width: GROUP_LAYER_WIDTH,\n            height: headerRect.height + (endCoordinates - startCoordinates),\n        };\n        return {\n            headerRect,\n            groupRect,\n            isEndHidden: this.env.model.getters.isRowHidden(sheetId, endRow),\n        };\n    }\n}\nclass ColGroup extends AbstractHeaderGroup {\n    dimension = \"COL\";\n    get groupBorderStyle() {\n        const groupBox = this.groupBox;\n        if (groupBox.groupRect.width === 0) {\n            return \"\";\n        }\n        return cssPropertiesToCss({\n            top: `calc(50% - 1px)`, // -1px: we want the border to be on the center\n            left: `${groupBox.headerRect.width / 2}px`,\n            width: `calc(100% - ${groupBox.headerRect.width / 2}px)`,\n            height: `30%`,\n            \"border-top\": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,\n            \"border-right\": groupBox.isEndHidden ? \"\" : `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,\n        });\n    }\n    get groupHeaderStyle() {\n        return cssPropertiesToCss({\n            width: `${this.groupBox.headerRect.width}px`,\n            height: `100%`,\n        });\n    }\n    get groupBox() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const { start: startCol, end: endCol } = this.props.group;\n        const startCoordinates = this.env.model.getters.getColDimensions(sheetId, startCol).start;\n        const endCoordinates = this.env.model.getters.getColDimensions(sheetId, endCol).end;\n        let groupHeaderX = 0;\n        let groupHeaderWidth = HEADER_WIDTH;\n        if (startCol !== 0) {\n            const headerRowDims = this.env.model.getters.getColDimensions(sheetId, startCol - 1);\n            groupHeaderX = HEADER_WIDTH + headerRowDims.start;\n            groupHeaderWidth = headerRowDims.end - headerRowDims.start;\n        }\n        const headerRect = {\n            x: groupHeaderX,\n            y: this.props.layerOffset,\n            width: groupHeaderWidth,\n            height: GROUP_LAYER_WIDTH,\n        };\n        const groupRect = {\n            x: headerRect.x,\n            y: this.props.layerOffset,\n            width: headerRect.width + (endCoordinates - startCoordinates),\n            height: GROUP_LAYER_WIDTH,\n        };\n        return {\n            headerRect,\n            groupRect,\n            isEndHidden: this.env.model.getters.isColHidden(sheetId, endCol),\n        };\n    }\n}\n\ncss /* scss */ `\n  .o-header-group-frozen-pane-border {\n    &.o-group-rows {\n      margin-top: -1px;\n      border-bottom: 3px solid ${FROZEN_PANE_HEADER_BORDER_COLOR};\n    }\n    &.o-group-columns {\n      margin-left: -1px;\n      border-right: 3px solid ${FROZEN_PANE_HEADER_BORDER_COLOR};\n    }\n  }\n\n  .o-header-group-main-pane {\n    &.o-group-rows {\n      margin-top: -2px; // Counteract o-header-group-frozen-pane-border offset\n    }\n    &.o-group-columns {\n      margin-left: -2px;\n    }\n  }\n`;\nclass HeaderGroupContainer extends Component {\n    static template = \"o-spreadsheet-HeaderGroupContainer\";\n    static props = {\n        dimension: String,\n        layers: Array,\n    };\n    static components = { RowGroup, ColGroup, Menu };\n    menu = useState({ isOpen: false, position: null, menuItems: [] });\n    getLayerOffset(layerIndex) {\n        return layerIndex * GROUP_LAYER_WIDTH;\n    }\n    onContextMenu(event) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const position = { x: event.clientX, y: event.clientY };\n        const menuItems = createHeaderGroupContainerContextMenu(sheetId, this.props.dimension);\n        this.openContextMenu(position, menuItems);\n    }\n    openContextMenu(position, menuItems) {\n        this.menu.isOpen = true;\n        this.menu.position = position;\n        this.menu.menuItems = menuItems;\n    }\n    closeMenu() {\n        this.menu.isOpen = false;\n        this.menu.position = null;\n        this.menu.menuItems = [];\n    }\n    get groupComponent() {\n        return this.props.dimension === \"ROW\" ? RowGroup : ColGroup;\n    }\n    get hasFrozenPane() {\n        const viewportCoordinates = this.env.model.getters.getMainViewportCoordinates();\n        return this.props.dimension === \"COL\" ? viewportCoordinates.x > 0 : viewportCoordinates.y > 0;\n    }\n    get scrollContainerStyle() {\n        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();\n        const cssProperties = {};\n        if (this.props.dimension === \"COL\") {\n            cssProperties.left = `${-scrollX - this.frozenPaneContainerSize}px`;\n        }\n        else {\n            cssProperties.top = `${-scrollY - this.frozenPaneContainerSize}px`;\n        }\n        return cssPropertiesToCss(cssProperties);\n    }\n    get frozenPaneContainerStyle() {\n        const cssProperties = {};\n        if (this.props.dimension === \"COL\") {\n            cssProperties.width = `${this.frozenPaneContainerSize}px`;\n        }\n        else {\n            cssProperties.height = `${this.frozenPaneContainerSize}px`;\n        }\n        return cssPropertiesToCss(cssProperties);\n    }\n    get frozenPaneContainerSize() {\n        if (!this.hasFrozenPane) {\n            return 0;\n        }\n        const viewportCoordinates = this.env.model.getters.getMainViewportCoordinates();\n        if (this.props.dimension === \"COL\") {\n            return HEADER_WIDTH + viewportCoordinates.x;\n        }\n        else {\n            return HEADER_HEIGHT + viewportCoordinates.y;\n        }\n    }\n}\n\ncss /* scss */ `\n  .o-sidePanel {\n    display: flex;\n    flex-direction: column;\n    overflow-x: hidden;\n    background-color: white;\n    border: solid ${GRAY_300};\n    border-width: 1px 0 0 1px;\n    user-select: none;\n    color: ${TEXT_BODY};\n\n    .o-sidePanelTitle {\n      line-height: 20px;\n      font-size: 16px;\n    }\n\n    .o-sidePanelHeader {\n      padding: 8px 16px;\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      border-bottom: 1px solid ${GRAY_300};\n\n      .o-sidePanelClose {\n        padding: 5px 10px;\n        cursor: pointer;\n        &:hover {\n          background-color: WhiteSmoke;\n        }\n      }\n    }\n    .o-sidePanelBody-container {\n      /* This overwrites the min-height: auto; of flex. Without this, a flex div cannot be smaller than its children */\n      min-height: 0;\n    }\n    .o-sidePanelBody {\n      overflow: auto;\n      width: 100%;\n      height: 100%;\n\n      .o-section {\n        padding: 16px;\n\n        .o-section-title {\n          font-weight: 500;\n          margin-bottom: 5px;\n        }\n\n        .o-section-subtitle {\n          font-weight: 500;\n          font-size: 13px;\n          line-height: 14px;\n          margin: 8px 0 4px 0;\n        }\n\n        .o-subsection-left {\n          display: inline-block;\n          width: 47%;\n          margin-right: 3%;\n        }\n\n        .o-subsection-right {\n          display: inline-block;\n          width: 47%;\n          margin-left: 3%;\n        }\n      }\n\n      .o-sidePanel-composer {\n        color: ${TEXT_BODY};\n      }\n    }\n\n    .o-sidePanelButtons {\n      display: flex;\n      gap: 8px;\n    }\n\n    .o-invalid {\n      border-width: 2px;\n      border-color: red;\n    }\n\n    .o-sidePanel-handle-container {\n      width: 8px;\n      position: fixed;\n      top: 50%;\n      z-index: 1;\n    }\n    .o-sidePanel-handle {\n      cursor: col-resize;\n      color: #a9a9a9;\n      .o-icon {\n        height: 25px;\n        margin-left: -5px;\n      }\n    }\n  }\n\n  .o-fw-bold {\n    font-weight: 500;\n  }\n`;\nclass SidePanel extends Component {\n    static template = \"o-spreadsheet-SidePanel\";\n    static props = {};\n    sidePanelStore;\n    spreadsheetRect = useSpreadsheetRect();\n    setup() {\n        this.sidePanelStore = useStore(SidePanelStore);\n        useEffect((isOpen) => {\n            if (!isOpen) {\n                this.sidePanelStore.close();\n            }\n        }, () => [this.sidePanelStore.isOpen]);\n    }\n    get panel() {\n        return sidePanelRegistry.get(this.sidePanelStore.componentTag);\n    }\n    close() {\n        this.sidePanelStore.close();\n    }\n    getTitle() {\n        const panel = this.panel;\n        return typeof panel.title === \"function\"\n            ? panel.title(this.env, this.sidePanelStore.panelProps)\n            : panel.title;\n    }\n    startHandleDrag(ev) {\n        const startingCursor = document.body.style.cursor;\n        const startSize = this.sidePanelStore.panelSize;\n        const startPosition = ev.clientX;\n        const onMouseMove = (ev) => {\n            document.body.style.cursor = \"col-resize\";\n            const newSize = startSize + startPosition - ev.clientX;\n            this.sidePanelStore.changePanelSize(newSize, this.spreadsheetRect.width);\n        };\n        const cleanUp = () => {\n            document.body.style.cursor = startingCursor;\n        };\n        startDnd(onMouseMove, cleanUp);\n    }\n}\n\ncss /* scss */ `\n  .o-menu-item-button {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    margin: 2px 1px;\n    padding: 0px 1px;\n    border-radius: 2px;\n    min-width: 20px;\n  }\n  .o-disabled {\n    opacity: 0.6;\n    cursor: default;\n  }\n`;\nclass ActionButton extends Component {\n    static template = \"o-spreadsheet-ActionButton\";\n    static props = {\n        action: Object,\n        hasTriangleDownIcon: { type: Boolean, optional: true },\n        selectedColor: { type: String, optional: true },\n        class: { type: String, optional: true },\n        onClick: { type: Function, optional: true },\n    };\n    actionButton = createAction(this.props.action);\n    setup() {\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.action !== this.props.action) {\n                this.actionButton = createAction(nextProps.action);\n            }\n        });\n    }\n    get isVisible() {\n        return this.actionButton.isVisible(this.env);\n    }\n    get isEnabled() {\n        return this.actionButton.isEnabled(this.env);\n    }\n    get isActive() {\n        return this.actionButton.isActive?.(this.env);\n    }\n    get title() {\n        const name = this.actionButton.name(this.env);\n        const description = this.actionButton.description(this.env);\n        return name + (description ? ` (${description})` : \"\");\n    }\n    get iconTitle() {\n        return this.actionButton.icon(this.env);\n    }\n    onClick(ev) {\n        if (this.isEnabled) {\n            this.props.onClick?.(ev);\n            this.actionButton.execute?.(this.env);\n        }\n    }\n    get buttonStyle() {\n        if (this.props.selectedColor) {\n            return cssPropertiesToCss({\n                \"border-bottom\": `4px solid ${this.props.selectedColor}`,\n                height: \"16px\",\n                \"margin-top\": \"2px\",\n            });\n        }\n        return \"\";\n    }\n}\n\n/**\n * List the available borders positions and the corresponding icons.\n * The structure of this array is defined to match the order/lines we want\n * to display in the topbar's border tool.\n */\nconst BORDER_POSITIONS = [\n    [\n        [\"all\", \"o-spreadsheet-Icon.BORDERS\"],\n        [\"hv\", \"o-spreadsheet-Icon.BORDER_HV\"],\n        [\"h\", \"o-spreadsheet-Icon.BORDER_H\"],\n        [\"v\", \"o-spreadsheet-Icon.BORDER_V\"],\n        [\"external\", \"o-spreadsheet-Icon.BORDER_EXTERNAL\"],\n    ],\n    [\n        [\"left\", \"o-spreadsheet-Icon.BORDER_LEFT\"],\n        [\"top\", \"o-spreadsheet-Icon.BORDER_TOP\"],\n        [\"right\", \"o-spreadsheet-Icon.BORDER_RIGHT\"],\n        [\"bottom\", \"o-spreadsheet-Icon.BORDER_BOTTOM\"],\n        [\"clear\", \"o-spreadsheet-Icon.BORDER_CLEAR\"],\n    ],\n];\n// -----------------------------------------------------------------------------\n// Border Editor\n// -----------------------------------------------------------------------------\ncss /* scss */ `\n  .o-border-selector {\n    padding: 4px;\n    background-color: white;\n\n    .o-divider {\n      border-right: 1px solid ${GRAY_300};\n      margin: 0 6px;\n    }\n\n    .o-border-selector-section {\n      .o-dropdown-line {\n        height: 30px;\n        margin: 1px;\n        .o-line-item {\n          padding: 4px;\n          width: 18px;\n          height: 18px;\n          &.active {\n            background-color: ${BUTTON_ACTIVE_BG};\n          }\n        }\n      }\n      .o-border-style-tool {\n        padding: 0px 3px;\n        margin: 2px;\n        height: 25px;\n      }\n    }\n  }\n\n  .o-border-style-dropdown {\n    background: #ffffff;\n    padding: 4px;\n    .o-dropdown-line {\n    }\n    .o-style-preview {\n      margin: 7px 5px 7px 5px;\n      width: 60px;\n      height: 5px;\n    }\n    .o-style-thin {\n      border-bottom: 1px solid #000000;\n    }\n    .o-style-medium {\n      border-bottom: 2px solid #000000;\n    }\n    .o-style-thick {\n      border-bottom: 3px solid #000000;\n    }\n    .o-style-dashed {\n      border-bottom: 1px dashed #000000;\n    }\n    .o-style-dotted {\n      border-bottom: 1px dotted #000000;\n    }\n    .o-dropdown-border-type {\n      cursor: pointer;\n      &:not(.o-disabled):not(.active):hover {\n        background-color: ${BUTTON_HOVER_BG};\n      }\n    }\n    .o-dropdown-border-check {\n      width: 20px;\n      font-size: 12px;\n    }\n    .o-border-picker-button {\n      padding: 0px !important;\n      margin: 5px 0px 0px 0px !important;\n      height: 25px !important;\n    }\n  }\n`;\nclass BorderEditor extends Component {\n    static template = \"o-spreadsheet-BorderEditor\";\n    static props = {\n        class: { type: String, optional: true },\n        currentBorderColor: { type: String, optional: false },\n        currentBorderStyle: { type: String, optional: false },\n        currentBorderPosition: { type: String, optional: true },\n        onBorderColorPicked: Function,\n        onBorderStylePicked: Function,\n        onBorderPositionPicked: Function,\n        maxHeight: { type: Number, optional: true },\n        anchorRect: Object,\n    };\n    static components = { ColorPickerWidget, Popover };\n    BORDER_POSITIONS = BORDER_POSITIONS;\n    lineStyleButtonRef = useRef(\"lineStyleButton\");\n    borderStyles = borderStyles;\n    state = useState({\n        activeTool: undefined,\n    });\n    toggleDropdownTool(tool) {\n        const isOpen = this.state.activeTool === tool;\n        this.state.activeTool = isOpen ? undefined : tool;\n    }\n    closeDropdown() {\n        this.state.activeTool = undefined;\n    }\n    setBorderPosition(position) {\n        this.props.onBorderPositionPicked(position);\n        this.closeDropdown();\n    }\n    setBorderColor(color) {\n        this.props.onBorderColorPicked(color);\n        this.closeDropdown();\n    }\n    setBorderStyle(style) {\n        this.props.onBorderStylePicked(style);\n        this.closeDropdown();\n    }\n    get lineStylePickerPopoverProps() {\n        return {\n            anchorRect: this.lineStylePickerAnchorRect,\n            positioning: \"BottomLeft\",\n            verticalOffset: 0,\n        };\n    }\n    get popoverProps() {\n        return {\n            anchorRect: this.props.anchorRect,\n            maxHeight: this.props.maxHeight,\n            positioning: \"BottomLeft\",\n            verticalOffset: 0,\n        };\n    }\n    get lineStylePickerAnchorRect() {\n        const button = this.lineStyleButtonRef.el;\n        if (button === null) {\n            return { x: 0, y: 0, width: 0, height: 0 };\n        }\n        const buttonRect = button.getBoundingClientRect();\n        return {\n            x: buttonRect.x,\n            y: buttonRect.y,\n            width: buttonRect.width,\n            height: buttonRect.height,\n        };\n    }\n}\n\nclass BorderEditorWidget extends Component {\n    static template = \"o-spreadsheet-BorderEditorWidget\";\n    static props = {\n        toggleBorderEditor: Function,\n        showBorderEditor: Boolean,\n        disabled: { type: Boolean, optional: true },\n        dropdownMaxHeight: { type: Number, optional: true },\n        class: { type: String, optional: true },\n    };\n    static components = { BorderEditor };\n    borderEditorButtonRef = useRef(\"borderEditorButton\");\n    state = useState({\n        currentColor: DEFAULT_BORDER_DESC.color,\n        currentStyle: DEFAULT_BORDER_DESC.style,\n        currentPosition: undefined,\n    });\n    get borderEditorAnchorRect() {\n        const button = this.borderEditorButtonRef.el;\n        const buttonRect = button.getBoundingClientRect();\n        return {\n            x: buttonRect.x,\n            y: buttonRect.y,\n            width: buttonRect.width,\n            height: buttonRect.height,\n        };\n    }\n    onBorderPositionPicked(position) {\n        this.state.currentPosition = position;\n        this.updateBorder();\n    }\n    onBorderColorPicked(color) {\n        this.state.currentColor = color;\n        this.updateBorder();\n    }\n    onBorderStylePicked(style) {\n        this.state.currentStyle = style;\n        this.updateBorder();\n    }\n    updateBorder() {\n        if (this.state.currentPosition === undefined) {\n            return;\n        }\n        this.env.model.dispatch(\"SET_ZONE_BORDERS\", {\n            sheetId: this.env.model.getters.getActiveSheetId(),\n            target: this.env.model.getters.getSelectedZones(),\n            border: {\n                position: this.state.currentPosition,\n                color: this.state.currentColor,\n                style: this.state.currentStyle,\n            },\n        });\n    }\n}\n\nconst COMPOSER_MAX_HEIGHT = 100;\n/* svg free of use from https://uxwing.com/formula-fx-icon/ */\nconst FX_SVG = /*xml*/ `\n<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 121.8 122.9' width='16' height='16' focusable='false'>\n  <path d='m28 34-4 5v2h10l-6 40c-4 22-6 28-7 30-2 2-3 3-5 3-3 0-7-2-9-4H4c-2 2-4 4-4 7s4 6 8 6 9-2 15-8c8-7 13-17 18-39l7-35 13-1 3-6H49c4-23 7-27 11-27 2 0 5 2 8 6h4c1-1 4-4 4-7 0-2-3-6-9-6-5 0-13 4-20 10-6 7-9 14-11 24h-8zm41 16c4-5 7-7 8-7s2 1 5 9l3 12c-7 11-12 17-16 17l-3-1-2-1c-3 0-6 3-6 7s3 7 7 7c6 0 12-6 22-23l3 10c3 9 6 13 10 13 5 0 11-4 18-15l-3-4c-4 6-7 8-8 8-2 0-4-3-6-10l-5-15 8-10 6-4 3 1 3 2c2 0 6-3 6-7s-2-7-6-7c-6 0-11 5-21 20l-2-6c-3-9-5-14-9-14-5 0-12 6-18 15l3 3z' fill='#BDBDBD'/>\n</svg>\n`;\ncss /* scss */ `\n  .o-topbar-composer {\n    height: fit-content;\n    margin-top: -1px;\n    border: 1px solid;\n    font-family: ${DEFAULT_FONT};\n\n    .o-composer:empty:not(:focus):not(.active)::before {\n      content: url(\"data:image/svg+xml,${encodeURIComponent(FX_SVG)}\");\n      position: relative;\n      top: 20%;\n    }\n  }\n\n  .user-select-text {\n    user-select: text;\n  }\n`;\nclass TopBarComposer extends Component {\n    static template = \"o-spreadsheet-TopBarComposer\";\n    static props = {};\n    static components = { Composer };\n    composerFocusStore;\n    composerStore;\n    composerInterface;\n    setup() {\n        this.composerFocusStore = useStore(ComposerFocusStore);\n        const composerStore = useStore(CellComposerStore);\n        this.composerStore = composerStore;\n        this.composerInterface = {\n            id: \"topbarComposer\",\n            get editionMode() {\n                return composerStore.editionMode;\n            },\n            startEdition: this.composerStore.startEdition,\n            setCurrentContent: this.composerStore.setCurrentContent,\n            stopEdition: this.composerStore.stopEdition,\n        };\n    }\n    get focus() {\n        return this.composerFocusStore.activeComposer === this.composerInterface\n            ? this.composerFocusStore.focusMode\n            : \"inactive\";\n    }\n    get composerStyle() {\n        const style = {\n            padding: \"5px 0px 5px 8px\",\n            \"max-height\": `${COMPOSER_MAX_HEIGHT}px`,\n            \"line-height\": \"24px\",\n        };\n        style.height = this.focus === \"inactive\" ? `${TOPBAR_TOOLBAR_HEIGHT}px` : \"fit-content\";\n        return cssPropertiesToCss(style);\n    }\n    get containerStyle() {\n        if (this.focus === \"inactive\") {\n            return cssPropertiesToCss({\n                \"border-color\": SEPARATOR_COLOR,\n                \"border-right\": \"none\",\n            });\n        }\n        return cssPropertiesToCss({\n            \"border-color\": SELECTION_BORDER_COLOR,\n            \"z-index\": String(ComponentsImportance.TopBarComposer),\n        });\n    }\n    onFocus(selection) {\n        this.composerFocusStore.focusComposer(this.composerInterface, { selection });\n    }\n}\n\ncss /* scss */ `\n  .o-font-size-editor {\n    height: calc(100% - 4px);\n    input.o-font-size {\n      outline-color: ${SELECTION_BORDER_COLOR};\n      height: 20px;\n      width: 23px;\n    }\n  }\n  .o-text-options > div {\n    cursor: pointer;\n    line-height: 26px;\n    padding: 3px 12px;\n    &:hover {\n      background-color: rgba(0, 0, 0, 0.08);\n    }\n  }\n`;\nclass FontSizeEditor extends Component {\n    static template = \"o-spreadsheet-FontSizeEditor\";\n    static props = {\n        onToggle: Function,\n        dropdownStyle: String,\n        class: String,\n    };\n    static components = {};\n    fontSizes = FONT_SIZES;\n    dropdown = useState({ isOpen: false });\n    inputRef = useRef(\"inputFontSize\");\n    rootEditorRef = useRef(\"FontSizeEditor\");\n    setup() {\n        useExternalListener(window, \"click\", this.onExternalClick, { capture: true });\n    }\n    onExternalClick(ev) {\n        if (!isChildEvent(this.rootEditorRef.el, ev)) {\n            this.closeFontList();\n        }\n    }\n    get currentFontSize() {\n        return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;\n    }\n    toggleFontList() {\n        const isOpen = this.dropdown.isOpen;\n        if (!isOpen) {\n            this.props.onToggle();\n            this.inputRef.el.focus();\n        }\n        else {\n            this.closeFontList();\n        }\n    }\n    closeFontList() {\n        this.dropdown.isOpen = false;\n    }\n    setSize(fontSizeStr) {\n        const fontSize = clip(Math.floor(parseFloat(fontSizeStr)), 1, 400);\n        setStyle(this.env, { fontSize });\n        this.closeFontList();\n    }\n    setSizeFromInput(ev) {\n        this.setSize(ev.target.value);\n    }\n    setSizeFromList(fontSizeStr) {\n        this.setSize(fontSizeStr);\n    }\n    onInputFocused(ev) {\n        this.dropdown.isOpen = true;\n        ev.target.select();\n    }\n    onInputKeydown(ev) {\n        if (ev.key === \"Enter\" || ev.key === \"Escape\") {\n            this.closeFontList();\n            const target = ev.target;\n            // In the case of a ESCAPE key, we get the previous font size back\n            if (ev.key === \"Escape\") {\n                target.value = `${this.currentFontSize}`;\n            }\n            this.props.onToggle();\n        }\n    }\n}\n\nclass TableDropdownButton extends Component {\n    static template = \"o-spreadsheet-TableDropdownButton\";\n    static components = { TableStylesPopover, ActionButton };\n    static props = {};\n    state = useState({ popoverProps: undefined });\n    onStylePicked(styleId) {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const tableConfig = { ...this.tableConfig, styleId };\n        const result = interactiveCreateTable(this.env, sheetId, tableConfig);\n        if (result.isSuccessful) {\n            this.env.openSidePanel(\"TableSidePanel\", {});\n        }\n        this.closePopover();\n    }\n    onClick(ev) {\n        if (ev.hasClosedTableStylesPopover || this.state.popoverProps) {\n            this.closePopover();\n            return;\n        }\n        if (this.env.model.getters.getFirstTableInSelection()) {\n            this.env.toggleSidePanel(\"TableSidePanel\", {});\n            return;\n        }\n        // Open the popover\n        const target = ev.currentTarget;\n        const { bottom, left } = target.getBoundingClientRect();\n        this.state.popoverProps = {\n            anchorRect: { x: left, y: bottom, width: 0, height: 0 },\n            positioning: \"BottomLeft\",\n            verticalOffset: 0,\n        };\n    }\n    closePopover() {\n        this.state.popoverProps = undefined;\n    }\n    get action() {\n        return {\n            name: (env) => this.env.model.getters.getFirstTableInSelection() ? _t(\"Edit table\") : _t(\"Insert table\"),\n            icon: (env) => this.env.model.getters.getFirstTableInSelection()\n                ? \"o-spreadsheet-Icon.EDIT_TABLE\"\n                : \"o-spreadsheet-Icon.PAINT_TABLE\",\n        };\n    }\n    get tableConfig() {\n        return { ...DEFAULT_TABLE_CONFIG, numberOfHeaders: 1, bandedRows: true };\n    }\n}\n\nclass PaintFormatButton extends Component {\n    static template = \"o-spreadsheet-PaintFormatButton\";\n    static props = {\n        class: { type: String, optional: true },\n    };\n    paintFormatStore;\n    setup() {\n        this.paintFormatStore = useStore(PaintFormatStore);\n    }\n    get isActive() {\n        return this.paintFormatStore.isActive;\n    }\n    onDblClick() {\n        this.paintFormatStore.activate({ persistent: true });\n    }\n    togglePaintFormat() {\n        if (this.isActive) {\n            this.paintFormatStore.cancel();\n        }\n        else {\n            this.paintFormatStore.activate({ persistent: false });\n        }\n    }\n}\n\n// -----------------------------------------------------------------------------\n// TopBar\n// -----------------------------------------------------------------------------\ncss /* scss */ `\n  .o-spreadsheet-topbar {\n    line-height: 1.2;\n    font-size: 13px;\n    font-weight: 500;\n    background-color: #fff;\n\n    .o-topbar-top {\n      border-bottom: 1px solid ${SEPARATOR_COLOR};\n      padding: 2px 10px;\n\n      /* Menus */\n      .o-topbar-topleft {\n        .o-topbar-menu {\n          padding: 4px 6px;\n          margin: 0 2px;\n\n          &.active {\n            background-color: ${BUTTON_ACTIVE_BG};\n            color: ${BUTTON_ACTIVE_TEXT_COLOR};\n          }\n        }\n      }\n    }\n\n    .o-topbar-composer {\n      flex-grow: 1;\n    }\n\n    /* Toolbar */\n    .o-topbar-toolbar {\n      height: ${TOPBAR_TOOLBAR_HEIGHT}px;\n\n      .o-readonly-toolbar {\n        background-color: ${BACKGROUND_HEADER_COLOR};\n        padding-left: 18px;\n        padding-right: 18px;\n      }\n\n      /* Toolbar */\n      .o-toolbar-tools {\n        display: flex;\n        flex-shrink: 0;\n        margin: 0px 6px 0px 16px;\n        cursor: default;\n\n        .o-divider {\n          display: inline-block;\n          border-right: 1px solid ${SEPARATOR_COLOR};\n          width: 0;\n          margin: 0 6px;\n        }\n\n        .o-dropdown {\n          position: relative;\n          display: flex;\n          align-items: center;\n\n          > span {\n            height: 30px;\n          }\n\n          .o-dropdown-content {\n            position: absolute;\n            top: 100%;\n            left: 0;\n            overflow-y: auto;\n            overflow-x: hidden;\n            padding: 2px;\n            z-index: ${ComponentsImportance.Dropdown};\n            box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);\n            background-color: white;\n\n            .o-dropdown-line {\n              display: flex;\n\n              > span {\n                padding: 4px;\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n`;\nclass TopBar extends Component {\n    static template = \"o-spreadsheet-TopBar\";\n    static props = {\n        onClick: Function,\n        dropdownMaxHeight: Number,\n    };\n    get dropdownStyle() {\n        return `max-height:${this.props.dropdownMaxHeight}px`;\n    }\n    static components = {\n        ColorPickerWidget,\n        ColorPicker,\n        Menu,\n        TopBarComposer,\n        FontSizeEditor,\n        ActionButton,\n        PaintFormatButton,\n        BorderEditorWidget,\n        TableDropdownButton,\n    };\n    state = useState({\n        menuState: { isOpen: false, position: null, menuItems: [] },\n        activeTool: \"\",\n        fillColor: \"#ffffff\",\n        textColor: \"#000000\",\n    });\n    isSelectingMenu = false;\n    openedEl = null;\n    menus = [];\n    EDIT = ACTION_EDIT;\n    FORMAT = ACTION_FORMAT;\n    DATA = ACTION_DATA;\n    formatNumberMenuItemSpec = formatNumberMenuItemSpec;\n    isntToolbarMenu = false;\n    composerFocusStore;\n    setup() {\n        this.composerFocusStore = useStore(ComposerFocusStore);\n        useExternalListener(window, \"click\", this.onExternalClick);\n        onWillStart(() => this.updateCellState());\n        onWillUpdateProps(() => this.updateCellState());\n    }\n    get topbarComponents() {\n        return topbarComponentRegistry\n            .getAllOrdered()\n            .filter((item) => !item.isVisible || item.isVisible(this.env));\n    }\n    onExternalClick(ev) {\n        // TODO : manage click events better. We need this piece of code\n        // otherwise the event opening the menu would close it on the same frame.\n        // And we cannot stop the event propagation because it's used in an\n        // external listener of the Menu component to close the context menu when\n        // clicking on the top bar\n        if (this.openedEl === ev.target) {\n            return;\n        }\n        this.closeMenus();\n    }\n    onClick() {\n        this.props.onClick();\n        this.closeMenus();\n    }\n    onMenuMouseOver(menu, ev) {\n        if (this.isSelectingMenu && this.isntToolbarMenu) {\n            this.openMenu(menu, ev);\n        }\n    }\n    toggleDropdownTool(tool, ev) {\n        const isOpen = this.state.activeTool === tool;\n        this.closeMenus();\n        this.state.activeTool = isOpen ? \"\" : tool;\n        this.openedEl = isOpen ? null : ev.target;\n    }\n    toggleContextMenu(menu, ev) {\n        if (this.state.menuState.isOpen && this.isntToolbarMenu) {\n            this.closeMenus();\n        }\n        else {\n            this.openMenu(menu, ev);\n            this.isntToolbarMenu = true;\n        }\n    }\n    toggleToolbarContextMenu(menuSpec, ev) {\n        if (this.state.menuState.isOpen && !this.isntToolbarMenu) {\n            this.closeMenus();\n        }\n        else {\n            const menu = createAction(menuSpec);\n            this.openMenu(menu, ev);\n            this.isntToolbarMenu = false;\n        }\n    }\n    openMenu(menu, ev) {\n        const { left, top, height } = ev.currentTarget.getBoundingClientRect();\n        this.state.activeTool = \"\";\n        this.state.menuState.isOpen = true;\n        this.state.menuState.position = { x: left, y: top + height };\n        this.state.menuState.menuItems = menu\n            .children(this.env)\n            .sort((a, b) => a.sequence - b.sequence);\n        this.state.menuState.parentMenu = menu;\n        this.isSelectingMenu = true;\n        this.openedEl = ev.target;\n        this.composerFocusStore.activeComposer.stopEdition();\n    }\n    closeMenus() {\n        this.state.activeTool = \"\";\n        this.state.menuState.isOpen = false;\n        this.state.menuState.parentMenu = undefined;\n        this.isSelectingMenu = false;\n        this.openedEl = null;\n    }\n    updateCellState() {\n        const style = this.env.model.getters.getCurrentStyle();\n        this.state.fillColor = style.fillColor || \"#ffffff\";\n        this.state.textColor = style.textColor || \"#000000\";\n        this.menus = topbarMenuRegistry.getMenuItems();\n    }\n    getMenuName(menu) {\n        return menu.name(this.env);\n    }\n    setColor(target, color) {\n        setStyle(this.env, { [target]: color });\n        this.onClick();\n    }\n}\n\nfunction instantiateClipboard() {\n    return new WebClipboardWrapper(navigator.clipboard);\n}\nclass WebClipboardWrapper {\n    clipboard;\n    // Can be undefined because navigator.clipboard doesn't exist in old browsers\n    constructor(clipboard) {\n        this.clipboard = clipboard;\n    }\n    async write(clipboardContent) {\n        if (this.clipboard?.write) {\n            try {\n                await this.clipboard?.write(this.getClipboardItems(clipboardContent));\n            }\n            catch (e) {\n                /**\n                 * Some browsers do not support writing custom mimetypes in the clipboard.\n                 * Therefore, we try to catch any errors and fallback on writing only standard\n                 * mimetypes to prevent the whole copy action from crashing.\n                 */\n                try {\n                    await this.clipboard?.write([\n                        new ClipboardItem({\n                            [ClipboardMIMEType.PlainText]: this.getBlob(clipboardContent, ClipboardMIMEType.PlainText),\n                            [ClipboardMIMEType.Html]: this.getBlob(clipboardContent, ClipboardMIMEType.Html),\n                        }),\n                    ]);\n                }\n                catch (e) { }\n            }\n        }\n        else {\n            await this.writeText(clipboardContent[ClipboardMIMEType.PlainText] ?? \"\");\n        }\n    }\n    async writeText(text) {\n        try {\n            this.clipboard?.writeText(text);\n        }\n        catch (e) { }\n    }\n    async read() {\n        let permissionResult = undefined;\n        try {\n            //@ts-ignore - clipboard-read is not implemented in all browsers\n            permissionResult = await navigator.permissions.query({ name: \"clipboard-read\" });\n        }\n        catch (e) { }\n        if (this.clipboard?.read) {\n            try {\n                const clipboardItems = await this.clipboard.read();\n                const clipboardContent = {};\n                for (const item of clipboardItems) {\n                    for (const type of item.types) {\n                        const blob = await item.getType(type);\n                        const text = await blob.text();\n                        clipboardContent[type] = text;\n                    }\n                }\n                return { status: \"ok\", content: clipboardContent };\n            }\n            catch (e) {\n                const status = permissionResult?.state === \"denied\" ? \"permissionDenied\" : \"notImplemented\";\n                return { status };\n            }\n        }\n        else {\n            return {\n                status: \"ok\",\n                content: {\n                    [ClipboardMIMEType.PlainText]: await this.clipboard?.readText(),\n                },\n            };\n        }\n    }\n    getClipboardItems(content) {\n        const clipboardItemData = {\n            [ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),\n            [ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),\n        };\n        return [new ClipboardItem(clipboardItemData)];\n    }\n    getBlob(clipboardContent, type) {\n        return new Blob([clipboardContent[type] || \"\"], {\n            type,\n        });\n    }\n}\n\n// -----------------------------------------------------------------------------\n// SpreadSheet\n// -----------------------------------------------------------------------------\nconst CARET_DOWN_SVG = /*xml*/ `\n<svg xmlns='http://www.w3.org/2000/svg' width='7' height='4' viewBox='0 0 7 4'>\n  <polygon fill='%23374151' points='3.5 4 7 0 0 0'/>\n</svg>\n`;\ncss /* scss */ `\n  .o-spreadsheet {\n    position: relative;\n    display: grid;\n    color: ${TEXT_BODY};\n    font-size: 14px;\n\n    input {\n      background-color: white;\n    }\n    .text-muted {\n      color: ${TEXT_BODY_MUTED} !important;\n    }\n    .o-disabled {\n      opacity: 0.4;\n      cursor: default;\n      pointer-events: none;\n    }\n\n    &,\n    *,\n    *:before,\n    *:after {\n      box-sizing: content-box;\n      /** rtl not supported ATM */\n      direction: ltr;\n    }\n    .o-separator {\n      border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};\n      margin-top: ${MENU_SEPARATOR_PADDING}px;\n      margin-bottom: ${MENU_SEPARATOR_PADDING}px;\n    }\n    .o-hoverable-button {\n      border-radius: 2px;\n      cursor: pointer;\n      .o-icon {\n        color: ${TEXT_BODY};\n      }\n      &:not(.o-disabled):not(.active):hover {\n        background-color: ${BUTTON_HOVER_BG};\n        color: ${BUTTON_HOVER_TEXT_COLOR};\n        .o-icon {\n          color: ${BUTTON_HOVER_TEXT_COLOR};\n        }\n      }\n      &.active {\n        background-color: ${BUTTON_ACTIVE_BG};\n        color: ${BUTTON_ACTIVE_TEXT_COLOR};\n        .o-icon {\n          color: ${BUTTON_ACTIVE_TEXT_COLOR};\n        }\n      }\n    }\n\n    .o-grid-container {\n      display: grid;\n      background-color: ${HEADER_GROUPING_BACKGROUND_COLOR};\n\n      .o-top-left {\n        border: 1px solid ${GRID_BORDER_COLOR};\n        margin-bottom: -1px;\n        margin-right: -1px;\n      }\n\n      .o-column-groups {\n        grid-column-start: 2;\n        border-top: 1px solid ${GRID_BORDER_COLOR};\n      }\n\n      .o-row-groups {\n        grid-row-start: 2;\n      }\n\n      .o-group-grid {\n        border-top: 1px solid ${GRID_BORDER_COLOR};\n        border-left: 1px solid ${GRID_BORDER_COLOR};\n      }\n    }\n\n    .o-input {\n      min-width: 0px;\n      padding: 1px 0;\n      box-sizing: border-box;\n      width: 100%;\n      outline: none;\n      border-color: ${GRAY_300};\n      color: ${GRAY_900};\n\n      &::placeholder {\n        opacity: 0.5;\n      }\n      &:focus {\n        border-color: ${ACTION_COLOR};\n      }\n    }\n\n    select.o-input {\n      cursor: pointer;\n      border-width: 0 0 1px 0;\n      padding: 1px 6px 1px 0px;\n\n      appearance: none;\n      -webkit-appearance: none;\n      -moz-appearance: none;\n      background: transparent url(\"data:image/svg+xml,${encodeURIComponent(CARET_DOWN_SVG)}\")\n        no-repeat right center;\n      text-overflow: ellipsis;\n\n      &:disabled {\n        color: ${DISABLED_TEXT_COLOR};\n        opacity: 0.4;\n        cursor: default;\n      }\n    }\n\n    .o-input[type=\"text\"] {\n      border-width: 0 0 1px 0;\n    }\n\n    .o-input[type=\"number\"],\n    .o-number-input {\n      border-width: 0 0 1px 0;\n      /* Remove number input arrows */\n      appearance: textfield;\n      &::-webkit-outer-spin-button,\n      &::-webkit-inner-spin-button {\n        -webkit-appearance: none;\n        margin: 0;\n      }\n    }\n  }\n\n  .o-two-columns {\n    grid-column: 1 / 3;\n  }\n\n  .o-text-icon {\n    vertical-align: middle;\n  }\n`;\n// -----------------------------------------------------------------------------\n// GRID STYLE\n// -----------------------------------------------------------------------------\ncss /* scss */ `\n  .o-grid {\n    position: relative;\n    overflow: hidden;\n    background-color: ${BACKGROUND_GRAY_COLOR};\n    &:focus {\n      outline: none;\n    }\n\n    > canvas {\n      border-bottom: 1px solid #e2e3e3;\n    }\n    .o-scrollbar {\n      &.corner {\n        right: 0px;\n        bottom: 0px;\n        height: ${SCROLLBAR_WIDTH}px;\n        width: ${SCROLLBAR_WIDTH}px;\n        border-top: 1px solid #e2e3e3;\n        border-left: 1px solid #e2e3e3;\n      }\n    }\n\n    .o-grid-overlay {\n      position: absolute;\n      outline: none;\n    }\n  }\n\n  .o-button {\n    border: 1px solid;\n    border-radius: 4px;\n    font-weight: 500;\n    font-size: 14px;\n    height: 30px;\n    line-height: 16px;\n    flex-grow: 1;\n    background-color: ${BUTTON_BG};\n    border: 1px solid ${GRAY_200};\n    color: ${TEXT_BODY};\n\n    &:disabled {\n      color: ${DISABLED_TEXT_COLOR};\n    }\n\n    &.primary {\n      background-color: ${PRIMARY_BUTTON_BG};\n      border-color: ${PRIMARY_BUTTON_BG};\n      color: #fff;\n      &:hover:enabled {\n        color: #fff;\n        background-color: ${PRIMARY_BUTTON_HOVER_BG};\n      }\n      &:active:enabled {\n        background-color: ${PRIMARY_BUTTON_ACTIVE_BG};\n        color: ${PRIMARY_BUTTON_BG};\n      }\n      &.o-disabled,\n      &:disabled {\n        opacity: 0.5;\n      }\n    }\n\n    &:hover:enabled {\n      color: ${BUTTON_HOVER_TEXT_COLOR};\n      background-color: ${BUTTON_HOVER_BG};\n    }\n    &:active:enabled {\n      color: ${BUTTON_ACTIVE_TEXT_COLOR};\n      background-color: ${BUTTON_ACTIVE_BG};\n    }\n\n    &.o-disabled,\n    &:disabled {\n      opacity: 0.8;\n    }\n\n    &.o-button-danger:hover {\n      color: #ffffff;\n      background: ${ALERT_DANGER_BORDER};\n    }\n  }\n\n  .o-button-link {\n    cursor: pointer;\n    text-decoration: none;\n    color: ${ACTION_COLOR};\n    font-weight: 500;\n    &:hover,\n    &:active {\n      color: ${ACTION_COLOR_HOVER};\n    }\n  }\n\n  .o-button-icon {\n    cursor: pointer;\n    color: ${TEXT_BODY_MUTED};\n    font-weight: 500;\n    &:hover,\n    &:active {\n      color: ${TEXT_BODY};\n    }\n  }\n`;\nclass Spreadsheet extends Component {\n    static template = \"o-spreadsheet-Spreadsheet\";\n    static props = {\n        model: Object,\n        notifyUser: { type: Function, optional: true },\n        raiseError: { type: Function, optional: true },\n        askConfirmation: { type: Function, optional: true },\n    };\n    static components = {\n        TopBar,\n        Grid,\n        BottomBar,\n        SidePanel,\n        SpreadsheetDashboard,\n        HeaderGroupContainer,\n    };\n    sidePanel;\n    spreadsheetRef = useRef(\"spreadsheet\");\n    spreadsheetRect = useSpreadsheetRect();\n    _focusGrid;\n    keyDownMapping;\n    isViewportTooSmall = false;\n    notificationStore;\n    composerFocusStore;\n    get model() {\n        return this.props.model;\n    }\n    getStyle() {\n        const properties = {};\n        if (this.env.isDashboard()) {\n            properties[\"grid-template-rows\"] = `auto`;\n        }\n        else {\n            properties[\"grid-template-rows\"] = `${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;\n        }\n        properties[\"grid-template-columns\"] = `auto ${this.sidePanel.panelSize}px`;\n        return cssPropertiesToCss(properties);\n    }\n    setup() {\n        const stores = useStoreProvider();\n        stores.inject(ModelStore, this.model);\n        this.notificationStore = useStore(NotificationStore);\n        this.composerFocusStore = useStore(ComposerFocusStore);\n        this.sidePanel = useStore(SidePanelStore);\n        this.keyDownMapping = {\n            \"CTRL+H\": () => this.sidePanel.toggle(\"FindAndReplace\", {}),\n            \"CTRL+F\": () => this.sidePanel.toggle(\"FindAndReplace\", {}),\n        };\n        const fileStore = this.model.config.external.fileStore;\n        useSubEnv({\n            model: this.model,\n            imageProvider: fileStore ? new ImageProvider(fileStore) : undefined,\n            loadCurrencies: this.model.config.external.loadCurrencies,\n            loadLocales: this.model.config.external.loadLocales,\n            isDashboard: () => this.model.getters.isDashboard(),\n            openSidePanel: this.sidePanel.open.bind(this.sidePanel),\n            toggleSidePanel: this.sidePanel.toggle.bind(this.sidePanel),\n            clipboard: this.env.clipboard || instantiateClipboard(),\n            startCellEdition: (content) => this.composerFocusStore.focusActiveComposer({ content }),\n            notifyUser: (notification) => this.notificationStore.notifyUser(notification),\n            askConfirmation: (text, confirm, cancel) => this.notificationStore.askConfirmation(text, confirm, cancel),\n            raiseError: (text, cb) => this.notificationStore.raiseError(text, cb),\n        });\n        this.notificationStore.updateNotificationCallbacks({ ...this.props });\n        useEffect(() => {\n            /**\n             * Only refocus the grid if the active element is not a child of the spreadsheet\n             * (i.e. activeElement is outside of the spreadsheetRef component)\n             * and spreadsheet is a child of that element. Anything else means that the focus\n             * is on an element that needs to keep it.\n             */\n            if (!this.spreadsheetRef.el.contains(document.activeElement) &&\n                document.activeElement?.contains(this.spreadsheetRef.el)) {\n                this.focusGrid();\n            }\n        }, () => [this.env.model.getters.getActiveSheetId()]);\n        useExternalListener(window, \"resize\", () => this.render(true));\n        // For some reason, the wheel event is not properly registered inside templates\n        // in Chromium-based browsers based on chromium 125\n        // This hack ensures the event declared in the template is properly registered/working\n        useExternalListener(document.body, \"wheel\", () => { });\n        this.bindModelEvents();\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.model !== this.props.model) {\n                throw new Error(\"Changing the props model is not supported at the moment.\");\n            }\n            if (nextProps.notifyUser !== this.props.notifyUser ||\n                nextProps.askConfirmation !== this.props.askConfirmation ||\n                nextProps.raiseError !== this.props.raiseError) {\n                this.notificationStore.updateNotificationCallbacks({ ...nextProps });\n            }\n        });\n        const render = batched(this.render.bind(this, true));\n        onMounted(() => {\n            this.checkViewportSize();\n            stores.on(\"store-updated\", this, render);\n            resizeObserver.observe(this.spreadsheetRef.el);\n        });\n        onWillUnmount(() => {\n            this.unbindModelEvents();\n            stores.off(\"store-updated\", this);\n            resizeObserver.disconnect();\n        });\n        onPatched(() => {\n            this.checkViewportSize();\n        });\n        const resizeObserver = new ResizeObserver(() => {\n            this.sidePanel.changePanelSize(this.sidePanel.panelSize, this.spreadsheetRect.width);\n        });\n    }\n    bindModelEvents() {\n        this.model.on(\"update\", this, () => this.render(true));\n        this.model.on(\"notify-ui\", this, (notification) => this.notificationStore.notifyUser(notification));\n        this.model.on(\"raise-error-ui\", this, ({ text }) => this.notificationStore.raiseError(text));\n    }\n    unbindModelEvents() {\n        this.model.off(\"update\", this);\n        this.model.off(\"notify-ui\", this);\n        this.model.off(\"raise-error-ui\", this);\n    }\n    checkViewportSize() {\n        const { xRatio, yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());\n        if (!isFinite(xRatio) || !isFinite(yRatio)) {\n            // before mounting, the ratios can be NaN or Infinity if the viewport size is 0\n            return;\n        }\n        if (yRatio > MAXIMAL_FREEZABLE_RATIO || xRatio > MAXIMAL_FREEZABLE_RATIO) {\n            if (this.isViewportTooSmall) {\n                return;\n            }\n            this.notificationStore.notifyUser({\n                text: _t(\"The current window is too small to display this sheet properly. Consider resizing your browser window or adjusting frozen rows and columns.\"),\n                type: \"warning\",\n                sticky: false,\n            });\n            this.isViewportTooSmall = true;\n        }\n        else {\n            this.isViewportTooSmall = false;\n        }\n    }\n    focusGrid() {\n        if (!this._focusGrid) {\n            return;\n        }\n        this._focusGrid();\n    }\n    onKeydown(ev) {\n        let keyDownString = \"\";\n        if (isCtrlKey(ev)) {\n            keyDownString += \"CTRL+\";\n        }\n        keyDownString += ev.key.toUpperCase();\n        let handler = this.keyDownMapping[keyDownString];\n        if (handler) {\n            ev.preventDefault();\n            ev.stopPropagation();\n            handler();\n            return;\n        }\n    }\n    get gridHeight() {\n        const { height } = this.env.model.getters.getSheetViewDimension();\n        return height;\n    }\n    get gridContainerStyle() {\n        const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;\n        const gridRowSize = GROUP_LAYER_WIDTH * this.colLayers.length;\n        return cssPropertiesToCss({\n            \"grid-template-columns\": `${gridColSize ? gridColSize + 2 : 0}px auto`, // +2: margins\n            \"grid-template-rows\": `${gridRowSize ? gridRowSize + 2 : 0}px auto`,\n        });\n    }\n    get rowLayers() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.env.model.getters.getVisibleGroupLayers(sheetId, \"ROW\");\n    }\n    get colLayers() {\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        return this.env.model.getters.getVisibleGroupLayers(sheetId, \"COL\");\n    }\n}\n\nclass LocalTransportService {\n    listeners = [];\n    async sendMessage(message) {\n        for (const { callback } of this.listeners) {\n            callback(message);\n        }\n    }\n    onNewMessage(id, callback) {\n        this.listeners.push({ id, callback });\n    }\n    leave(id) {\n        this.listeners = this.listeners.filter((listener) => listener.id !== id);\n    }\n}\n\nfunction inverseCommand(cmd) {\n    return inverseCommandRegistry.get(cmd.type)(cmd);\n}\n\n/**\n * A branch holds a sequence of operations.\n * It can be represented as \"A - B - C - D\" if A, B, C and D are executed one\n * after the other.\n *\n * @param buildTransformation Factory to build transformations\n * @param operations initial operations\n */\nclass Branch {\n    buildTransformation;\n    operations;\n    constructor(buildTransformation, operations = []) {\n        this.buildTransformation = buildTransformation;\n        this.operations = operations;\n    }\n    getOperations() {\n        return this.operations;\n    }\n    getOperation(operationId) {\n        const operation = this.operations.find((op) => op.id === operationId);\n        if (!operation) {\n            throw new Error(`Operation ${operationId} not found`);\n        }\n        return operation;\n    }\n    getLastOperationId() {\n        return this.operations[this.operations.length - 1]?.id;\n    }\n    /**\n     * Get the id of the operation appears first in the list of operations\n     */\n    getFirstOperationAmong(op1, op2) {\n        for (const operation of this.operations) {\n            if (operation.id === op1)\n                return op1;\n            if (operation.id === op2)\n                return op2;\n        }\n        throw new Error(`Operation ${op1} and ${op2} not found`);\n    }\n    contains(operationId) {\n        return !!this.operations.find((operation) => operation.id === operationId);\n    }\n    /**\n     * Add the given operation as the first operation\n     */\n    prepend(operation) {\n        const transformation = this.buildTransformation.with(operation.data);\n        this.operations = [\n            operation,\n            ...this.operations.map((operation) => operation.transformed(transformation)),\n        ];\n    }\n    /**\n     * add the given operation after the given predecessorOpId\n     */\n    insert(newOperation, predecessorOpId) {\n        const transformation = this.buildTransformation.with(newOperation.data);\n        const { before, operation, after } = this.locateOperation(predecessorOpId);\n        this.operations = [\n            ...before,\n            operation,\n            newOperation,\n            ...after.map((operation) => operation.transformed(transformation)),\n        ];\n    }\n    /**\n     * Add the given operation as the last operation\n     */\n    append(operation) {\n        this.operations.push(operation);\n    }\n    /**\n     * Append operations in the given branch to this branch.\n     */\n    appendBranch(branch) {\n        this.operations = this.operations.concat(branch.operations);\n    }\n    /**\n     * Create and return a copy of this branch, starting after the given operationId\n     */\n    fork(operationId) {\n        const { after } = this.locateOperation(operationId);\n        return new Branch(this.buildTransformation, after);\n    }\n    /**\n     * Transform all the operations in this branch with the given transformation\n     */\n    transform(transformation) {\n        this.operations = this.operations.map((operation) => operation.transformed(transformation));\n    }\n    /**\n     * Cut the branch before the operation, meaning the operation\n     * and all following operations are dropped.\n     */\n    cutBefore(operationId) {\n        this.operations = this.locateOperation(operationId).before;\n    }\n    /**\n     * Cut the branch after the operation, meaning all following operations are dropped.\n     */\n    cutAfter(operationId) {\n        const { before, operation } = this.locateOperation(operationId);\n        this.operations = before.concat([operation]);\n    }\n    /**\n     * Find an operation in this branch based on its id.\n     * This returns the operation itself, operations which comes before it\n     * and operation which comes after it.\n     */\n    locateOperation(operationId) {\n        const operationIndex = this.operations.findIndex((step) => step.id === operationId);\n        if (operationIndex === -1) {\n            throw new Error(`Operation ${operationId} not found`);\n        }\n        return {\n            before: this.operations.slice(0, operationIndex),\n            operation: this.operations[operationIndex],\n            after: this.operations.slice(operationIndex + 1),\n        };\n    }\n}\n\n/**\n * An Operation can be executed to change a data structure from state A\n * to state B.\n * It should hold the necessary data used to perform this transition.\n * It should be possible to revert the changes made by this operation.\n *\n * In the context of o-spreadsheet, the data from an operation would\n * be a revision (the commands are used to execute it, the `changes` are used\n * to revert it).\n */\nclass Operation {\n    id;\n    data;\n    constructor(id, data) {\n        this.id = id;\n        this.data = data;\n    }\n    transformed(transformation) {\n        return new LazyOperation(this.id, lazy(() => transformation(this.data)));\n    }\n}\nclass LazyOperation {\n    id;\n    lazyData;\n    constructor(id, lazyData) {\n        this.id = id;\n        this.lazyData = lazyData;\n    }\n    get data() {\n        return this.lazyData();\n    }\n    transformed(transformation) {\n        return new LazyOperation(this.id, this.lazyData.map(transformation));\n    }\n}\n\n/**\n * An execution object is a sequence of executionSteps (each execution step is an operation in a branch).\n *\n * You can iterate over the steps of an execution\n * ```js\n * for (const operation of execution) {\n *   // ... do something\n * }\n * ```\n */\nclass OperationSequence {\n    operations;\n    constructor(operations) {\n        this.operations = operations;\n    }\n    [Symbol.iterator]() {\n        return this.operations[Symbol.iterator]();\n    }\n    /**\n     * Stop the operation sequence at a given operation\n     * @param operationId included\n     */\n    stopWith(operationId) {\n        function* filter(execution, operationId) {\n            for (const step of execution) {\n                yield step;\n                if (step.operation.id === operationId) {\n                    return;\n                }\n            }\n        }\n        return new OperationSequence(filter(this.operations, operationId));\n    }\n    /**\n     * Stop the operation sequence before a given operation\n     * @param operationId excluded\n     */\n    stopBefore(operationId) {\n        function* filter(execution, operationId) {\n            for (const step of execution) {\n                if (step.operation.id === operationId) {\n                    return;\n                }\n                yield step;\n            }\n        }\n        return new OperationSequence(filter(this.operations, operationId));\n    }\n    /**\n     * Start the operation sequence at a given operation\n     * @param operationId excluded\n     */\n    startAfter(operationId) {\n        function* filter(execution, operationId) {\n            let skip = true;\n            for (const step of execution) {\n                if (!skip) {\n                    yield step;\n                }\n                if (step.operation.id === operationId) {\n                    skip = false;\n                }\n            }\n        }\n        return new OperationSequence(filter(this.operations, operationId));\n    }\n}\n\n/**\n * The tree is a data structure used to maintain the different branches of the\n * SelectiveHistory.\n *\n * Branches can be \"stacked\" on each other and an execution path can be derived\n * from any stack of branches. The rules to derive this path is explained below.\n *\n * An operation can be cancelled/undone by inserting a new branch below\n * this operation.\n * e.g\n *    Given the branch A    B   C\n *    To undo B, a new branching branch is inserted at operation B.\n *    ```txt\n *    A   B   C   D\n *        >   C'  D'\n *    ```\n *    A new execution path can now be derived. At each operation:\n *    - if there is a lower branch, don't execute it and go to the operation below\n *    - if not, execute it and go to the operation on the right.\n *    The execution path is   A   C'    D'\n *    Operation C and D have been adapted (transformed) in the lower branch\n *    since operation B is not executed in this branch.\n *\n */\nclass Tree {\n    buildTransformation;\n    branches;\n    branchingOperationIds = new Map();\n    constructor(buildTransformation, initialBranch) {\n        this.buildTransformation = buildTransformation;\n        this.branches = [initialBranch];\n    }\n    /**\n     * Return the last branch of the entire stack of branches.\n     */\n    getLastBranch() {\n        return this.branches[this.branches.length - 1];\n    }\n    /**\n     * Return the sequence of operations from this branch\n     * until the very last branch.\n     */\n    execution(branch) {\n        return new OperationSequence(linkNext(this._execution(branch), this._execution(branch)));\n    }\n    /**\n     * Return the sequence of operations from this branch\n     * to the very first branch.\n     */\n    revertedExecution(branch) {\n        return new OperationSequence(linkNext(this._revertedExecution(branch), this._revertedExecution(branch)));\n    }\n    /**\n     * Append an operation to the end of the tree.\n     * Also insert the (transformed) operation in all previous branches.\n     *\n     * Adding operation `D` to the last branch\n     * ```txt\n     *  A1   B1   C1\n     *  >    B2   C2\n     * ```\n     * will give\n     * ```txt\n     *  A1   B1   C1   D'   with D' = D transformed with A1\n     *  >    B2   C2   D\n     * ```\n     */\n    insertOperationLast(branch, operation) {\n        const insertAfter = branch.getLastOperationId() || this.previousBranch(branch)?.getLastOperationId();\n        branch.append(operation);\n        if (insertAfter) {\n            this.insertPrevious(branch, operation, insertAfter);\n        }\n    }\n    /**\n     * Insert a new operation after an other operation.\n     * The operation will be inserted in this branch, in next branches (transformed)\n     * and in previous branches (also transformed).\n     *\n     * Given\n     * ```txt\n     *  1: A1   B1   C1\n     *  2: >    B2   C2\n     *  3:      >    C3\n     * ```\n     * Inserting D to branch 2 gives\n     * ```txt\n     *  1: A1   B1   C1   D1          D1 = D transformed with A1\n     *  2: >    B2   C2   D     with  D  = D\n     *  3:      >    C3   D2          D2 = D transformed without B2 (B2\u207b\u00b9)\n     * ```\n     */\n    insertOperationAfter(branch, operation, predecessorOpId) {\n        branch.insert(operation, predecessorOpId);\n        this.updateNextWith(branch, operation, predecessorOpId);\n        this.insertPrevious(branch, operation, predecessorOpId);\n    }\n    /**\n     * Create a new branching branch at the given operation.\n     * This cancels the operation from the execution path.\n     */\n    undo(branch, operation) {\n        const transformation = this.buildTransformation.without(operation.data);\n        const branchingId = this.branchingOperationIds.get(branch);\n        this.branchingOperationIds.set(branch, operation.id);\n        const nextBranch = branch.fork(operation.id);\n        if (branchingId) {\n            this.branchingOperationIds.set(nextBranch, branchingId);\n        }\n        this.insertBranchAfter(branch, nextBranch);\n        this.transform(nextBranch, transformation);\n    }\n    /**\n     * Remove the branch just after this one. This un-cancels (redo) the branching\n     * operation. Lower branches will be transformed accordingly.\n     *\n     * Given\n     * ```txt\n     *  1: A1   B1   C1\n     *  2: >    B2   C2\n     *  3:      >    C3\n     * ```\n     * removing the next branch of 1 gives\n     *\n     * ```txt\n     *  1: A1   B1   C1\n     *  2:      >    C3'   with  C3' = C1 transformed without B1 (B1\u207b\u00b9)\n     * ```\n     */\n    redo(branch) {\n        const removedBranch = this.nextBranch(branch);\n        if (!removedBranch)\n            return;\n        const nextBranch = this.nextBranch(removedBranch);\n        this.removeBranchFromTree(removedBranch);\n        const undoBranchingId = this.branchingOperationIds.get(removedBranch);\n        if (undoBranchingId) {\n            this.branchingOperationIds.set(branch, undoBranchingId);\n        }\n        else {\n            this.branchingOperationIds.delete(branch);\n        }\n        if (nextBranch) {\n            this.rebaseUp(nextBranch);\n        }\n    }\n    /**\n     * Drop the operation and all following operations in every\n     * branch\n     */\n    drop(operationId) {\n        for (const branch of this.branches) {\n            if (branch.contains(operationId)) {\n                branch.cutBefore(operationId);\n            }\n        }\n    }\n    /**\n     * Find the operation in the execution path.\n     */\n    findOperation(branch, operationId) {\n        for (const operation of this.revertedExecution(branch)) {\n            if (operation.operation.id === operationId) {\n                return operation;\n            }\n        }\n        throw new Error(`Operation ${operationId} not found`);\n    }\n    /**\n     * Rebuild transformed operations of this branch based on the upper branch.\n     *\n     * Given the following structure:\n     * ```txt\n     *  1: A1   B1    C1\n     *  2: >    B2    C2\n     *  3:      >     C3\n     * ```\n     * Rebasing branch \"2\" gives\n     * ```txt\n     *  1: A1   B1    C1\n     *  2: >    B2'   C2'  With  B2' = B1 transformed without A1 and C2' = C1 transformed without A1\n     *  3:      >     C3'        C3' = C2' transformed without B2'\n     * ```\n     */\n    rebaseUp(branch) {\n        const { previousBranch, branchingOperation } = this.findPreviousBranchingOperation(branch);\n        if (!previousBranch || !branchingOperation)\n            return;\n        const rebaseTransformation = this.buildTransformation.without(branchingOperation.data);\n        const newBranch = previousBranch.fork(branchingOperation.id);\n        this.branchingOperationIds.set(newBranch, this.branchingOperationIds.get(branch));\n        this.removeBranchFromTree(branch);\n        this.insertBranchAfter(previousBranch, newBranch);\n        newBranch.transform(rebaseTransformation);\n        const nextBranch = this.nextBranch(newBranch);\n        if (nextBranch) {\n            this.rebaseUp(nextBranch);\n        }\n    }\n    removeBranchFromTree(branch) {\n        const index = this.branches.findIndex((l) => l === branch);\n        this.branches.splice(index, 1);\n    }\n    insertBranchAfter(branch, toInsert) {\n        const index = this.branches.findIndex((l) => l === branch);\n        this.branches.splice(index + 1, 0, toInsert);\n    }\n    /**\n     * Update the branching branch of this branch, either by (1) inserting the new\n     * operation in it or (2) by transforming it.\n     * (1) If the operation is positioned before the branching branch, the branching\n     *     branch should be transformed with this operation.\n     * (2) If it's positioned after, the operation should be inserted in the\n     *     branching branch.\n     */\n    updateNextWith(branch, operation, predecessorOpId) {\n        const branchingId = this.branchingOperationIds.get(branch);\n        const nextBranch = this.nextBranch(branch);\n        if (!branchingId || !nextBranch) {\n            return;\n        }\n        if (branch.getFirstOperationAmong(predecessorOpId, branchingId) === branchingId) {\n            const transformedOperation = this.addToNextBranch(branch, nextBranch, branchingId, operation, predecessorOpId);\n            this.updateNextWith(nextBranch, transformedOperation, predecessorOpId);\n        }\n        else {\n            const transformation = this.buildTransformation.with(operation.data);\n            this.transform(nextBranch, transformation);\n        }\n    }\n    addToNextBranch(branch, nextBranch, branchingId, operation, predecessorOpId) {\n        // If the operation is inserted after the branching operation, it should\n        // be positioned first.\n        let transformedOperation = operation;\n        if (predecessorOpId === branchingId) {\n            transformedOperation = this.getTransformedOperation(branch, branchingId, operation);\n            nextBranch.prepend(transformedOperation);\n        }\n        else if (nextBranch.contains(predecessorOpId)) {\n            transformedOperation = this.getTransformedOperation(branch, branchingId, operation);\n            nextBranch.insert(transformedOperation, predecessorOpId);\n        }\n        else {\n            nextBranch.append(operation);\n        }\n        return transformedOperation;\n    }\n    getTransformedOperation(branch, branchingId, operation) {\n        const branchingOperation = branch.getOperation(branchingId);\n        const branchingTransformation = this.buildTransformation.without(branchingOperation.data);\n        return operation.transformed(branchingTransformation);\n    }\n    /**\n     * Check if this branch should execute the given operation.\n     * i.e. If the operation is not cancelled by a branching branch.\n     */\n    shouldExecute(branch, operation) {\n        return operation.id !== this.branchingOperationIds.get(branch);\n    }\n    transform(branch, transformation) {\n        branch.transform(transformation);\n        const nextBranch = this.nextBranch(branch);\n        if (nextBranch) {\n            this.transform(nextBranch, transformation);\n        }\n    }\n    /**\n     * Insert a new operation in previous branches. The operations which are\n     * positioned after the inserted operations are transformed with the newly\n     * inserted operations. This one is also transformed, with the branching\n     * operation.\n     */\n    insertPrevious(branch, newOperation, insertAfter) {\n        const { previousBranch, branchingOperation } = this.findPreviousBranchingOperation(branch);\n        if (!previousBranch || !branchingOperation)\n            return;\n        const transformation = this.buildTransformation.with(branchingOperation.data);\n        const branchTail = branch.fork(insertAfter);\n        branchTail.transform(transformation);\n        previousBranch.cutAfter(insertAfter);\n        previousBranch.appendBranch(branchTail);\n        const operationToInsert = newOperation.transformed(transformation);\n        this.insertPrevious(previousBranch, operationToInsert, insertAfter);\n    }\n    findPreviousBranchingOperation(branch) {\n        const previousBranch = this.previousBranch(branch);\n        if (!previousBranch)\n            return { previousBranch: undefined, branchingOperation: undefined };\n        const previousBranchingId = this.branchingOperationIds.get(previousBranch);\n        if (!previousBranchingId)\n            return { previousBranch: undefined, branchingOperation: undefined };\n        return {\n            previousBranch,\n            branchingOperation: previousBranch.getOperation(previousBranchingId),\n        };\n    }\n    /**\n     * Retrieve the next branch of the given branch\n     */\n    nextBranch(branch) {\n        const index = this.branches.findIndex((l) => l === branch);\n        if (index === -1) {\n            return undefined;\n        }\n        return this.branches[index + 1];\n    }\n    /**\n     * Retrieve the previous branch of the given branch\n     */\n    previousBranch(branch) {\n        const index = this.branches.findIndex((l) => l === branch);\n        if (index === -1) {\n            return undefined;\n        }\n        return this.branches[index - 1];\n    }\n    /**\n     * Yields the sequence of operations to execute, in reverse order.\n     */\n    *_revertedExecution(branch) {\n        const branchingOperationId = this.branchingOperationIds.get(branch);\n        let afterBranchingPoint = !!branchingOperationId;\n        const operations = branch.getOperations();\n        for (let i = operations.length - 1; i >= 0; i--) {\n            const operation = operations[i];\n            if (operation.id === branchingOperationId) {\n                afterBranchingPoint = false;\n            }\n            if (!afterBranchingPoint) {\n                yield {\n                    operation: operation,\n                    branch: branch,\n                    isCancelled: !this.shouldExecute(branch, operation),\n                };\n            }\n        }\n        const previous = this.previousBranch(branch);\n        yield* previous ? this._revertedExecution(previous) : [];\n    }\n    /**\n     * Yields the sequence of operations to execute\n     */\n    *_execution(branch) {\n        for (const operation of branch.getOperations()) {\n            yield {\n                operation: operation,\n                branch: branch,\n                isCancelled: !this.shouldExecute(branch, operation),\n            };\n            if (operation.id === this.branchingOperationIds.get(branch)) {\n                const next = this.nextBranch(branch);\n                yield* next ? this._execution(next) : [];\n                return;\n            }\n        }\n        if (!this.branchingOperationIds.get(branch)) {\n            const next = this.nextBranch(branch);\n            yield* next ? this._execution(next) : [];\n        }\n    }\n}\n\nclass SelectiveHistory {\n    HEAD_BRANCH;\n    HEAD_OPERATION;\n    tree;\n    applyOperation;\n    revertOperation;\n    buildEmpty;\n    buildTransformation;\n    /**\n     * The selective history is a data structure used to register changes/updates of a state.\n     * Each change/update is called an \"operation\".\n     * The data structure allows to easily cancel (and redo) any operation individually.\n     * An operation can be represented by any data structure. It can be a \"command\", a \"diff\", etc.\n     * However it must have the following properties:\n     * - it can be applied to modify the state\n     * - it can be reverted on the state such that it was never executed.\n     * - it can be transformed given other operation (Operational Transformation)\n     *\n     * Since this data structure doesn't know anything about the state nor the structure of\n     * operations, the actual work must be performed by external functions given as parameters.\n     * @param initialOperationId\n     * @param applyOperation a function which can apply an operation to the state\n     * @param revertOperation  a function which can revert an operation from the state\n     * @param buildEmpty  a function returning an \"empty\" operation.\n     *                    i.e an operation that leaves the state unmodified once applied or reverted\n     *                    (used for internal implementation)\n     * @param buildTransformation Factory used to build transformations\n     */\n    constructor(args) {\n        this.applyOperation = args.applyOperation;\n        this.revertOperation = args.revertOperation;\n        this.buildEmpty = args.buildEmpty;\n        this.buildTransformation = args.buildTransformation;\n        this.HEAD_BRANCH = new Branch(this.buildTransformation);\n        this.tree = new Tree(this.buildTransformation, this.HEAD_BRANCH);\n        const initialOperationId = args.initialOperationId;\n        const initial = new Operation(initialOperationId, this.buildEmpty(initialOperationId));\n        this.tree.insertOperationLast(this.HEAD_BRANCH, initial);\n        this.HEAD_OPERATION = initial;\n    }\n    /**\n     * Return the operation identified by its id.\n     */\n    get(operationId) {\n        return this.tree.findOperation(this.HEAD_BRANCH, operationId).operation.data;\n    }\n    /**\n     * Append a new operation as the last one\n     */\n    append(operationId, data) {\n        const operation = new Operation(operationId, data);\n        const branch = this.tree.getLastBranch();\n        this.tree.insertOperationLast(branch, operation);\n        this.HEAD_BRANCH = branch;\n        this.HEAD_OPERATION = operation;\n    }\n    /**\n     * Insert a new operation after a specific operation (may not be the last operation).\n     * Following operations will be transformed according\n     * to the new operation.\n     */\n    insert(operationId, data, insertAfter) {\n        const operation = new Operation(operationId, data);\n        this.revertTo(insertAfter);\n        this.tree.insertOperationAfter(this.HEAD_BRANCH, operation, insertAfter);\n        this.fastForward();\n    }\n    /**\n     * @param operationId operation to undo\n     * @param undoId the id of the \"undo operation\"\n     * @param insertAfter the id of the operation after which to insert the undo\n     */\n    undo(operationId, undoId, insertAfter) {\n        const { branch, operation } = this.tree.findOperation(this.HEAD_BRANCH, operationId);\n        this.revertBefore(operationId);\n        this.tree.undo(branch, operation);\n        this.fastForward();\n        this.insert(undoId, this.buildEmpty(undoId), insertAfter);\n    }\n    /**\n     * @param operationId operation to redo\n     * @param redoId the if of the \"redo operation\"\n     * @param insertAfter the id of the operation after which to insert the redo\n     */\n    redo(operationId, redoId, insertAfter) {\n        const { branch } = this.tree.findOperation(this.HEAD_BRANCH, operationId);\n        this.revertBefore(operationId);\n        this.tree.redo(branch);\n        this.fastForward();\n        this.insert(redoId, this.buildEmpty(redoId), insertAfter);\n    }\n    drop(operationId) {\n        this.revertBefore(operationId);\n        this.tree.drop(operationId);\n    }\n    /**\n     * Revert the state as it was *before* the given operation was executed.\n     */\n    revertBefore(operationId) {\n        const execution = this.tree.revertedExecution(this.HEAD_BRANCH).stopWith(operationId);\n        this.revert(execution);\n    }\n    /**\n     * Revert the state as it was *after* the given operation was executed.\n     */\n    revertTo(operationId) {\n        const execution = operationId\n            ? this.tree.revertedExecution(this.HEAD_BRANCH).stopBefore(operationId)\n            : this.tree.revertedExecution(this.HEAD_BRANCH);\n        this.revert(execution);\n    }\n    /**\n     * Revert an execution\n     */\n    revert(execution) {\n        for (const { next, operation, isCancelled } of execution) {\n            if (!isCancelled) {\n                this.revertOperation(operation.data);\n            }\n            if (next) {\n                this.HEAD_BRANCH = next.branch;\n                this.HEAD_OPERATION = next.operation;\n            }\n        }\n    }\n    /**\n     * Replay the operations between the current HEAD_BRANCH and the end of the tree\n     */\n    fastForward() {\n        const operations = this.HEAD_OPERATION\n            ? this.tree.execution(this.HEAD_BRANCH).startAfter(this.HEAD_OPERATION.id)\n            : this.tree.execution(this.HEAD_BRANCH);\n        for (const { operation: operation, branch, isCancelled } of operations) {\n            if (!isCancelled) {\n                this.applyOperation(operation.data);\n            }\n            this.HEAD_OPERATION = operation;\n            this.HEAD_BRANCH = branch;\n        }\n    }\n}\n\nfunction buildRevisionLog(args) {\n    return new SelectiveHistory({\n        initialOperationId: args.initialRevisionId,\n        applyOperation: (revision) => {\n            const commands = revision.commands.slice();\n            const { changes } = args.recordChanges(() => {\n                for (const command of commands) {\n                    args.dispatch(command);\n                }\n            });\n            revision.setChanges(changes);\n        },\n        revertOperation: (revision) => revertChanges([revision]),\n        buildEmpty: (id) => new Revision(id, \"empty\", []),\n        buildTransformation: {\n            with: (revision) => (toTransform) => {\n                return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands), toTransform.rootCommand, undefined, toTransform.timestamp);\n            },\n            without: (revision) => (toTransform) => {\n                return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands.map(inverseCommand).flat()), toTransform.rootCommand, undefined, toTransform.timestamp);\n            },\n        },\n    });\n}\n/**\n * Revert changes from the given revisions\n */\nfunction revertChanges(revisions) {\n    for (const revision of revisions.slice().reverse()) {\n        for (let i = revision.changes.length - 1; i >= 0; i--) {\n            const change = revision.changes[i];\n            applyChange(change);\n        }\n    }\n}\n/**\n * Apply the changes of the given HistoryChange to the state\n */\nfunction applyChange(change) {\n    const target = change.target;\n    const key = change.key;\n    const before = change.before;\n    if (before === undefined) {\n        delete target[key];\n    }\n    else {\n        target[key] = before;\n    }\n}\n\n/**\n * Stateless sequence of events that can be processed by consumers.\n *\n * There are three kind of consumers:\n * - the main consumer\n * - the default consumer\n * - observer consumers\n *\n * Main consumer\n * -------------\n * Anyone can capture the event stream and become the main consumer.\n * If there is already a main consumer, it is kicked off and it will no longer\n * receive events.\n * The main consumer can release the stream at any moment to stop listening\n * events.\n *\n * Default consumer\n * ----------------\n * When the main consumer releases the stream and until the stream is captured\n * again, all events are transmitted to the default consumer.\n *\n * Observer consumers\n * ------------------\n * Observers permanently receive events.\n *\n */\nclass EventStream {\n    observers = new Map();\n    /**\n     * the one we default to when someone releases the stream by themeselves\n     */\n    defaultSubscription;\n    mainSubscription;\n    registerAsDefault(owner, callbacks) {\n        this.defaultSubscription = { owner, callbacks };\n        if (!this.mainSubscription) {\n            this.mainSubscription = this.defaultSubscription;\n        }\n    }\n    /**\n     * Register callbacks to observe the stream\n     */\n    observe(owner, callbacks) {\n        this.observers.set(owner, { owner, callbacks });\n    }\n    /**\n     * Capture the stream for yourself\n     */\n    capture(owner, callbacks) {\n        if (this.observers.get(owner)) {\n            throw new Error(\"You are already subscribed forever\");\n        }\n        if (this.mainSubscription?.owner && this.mainSubscription.owner !== owner) {\n            this.mainSubscription.callbacks.release?.();\n        }\n        this.mainSubscription = { owner, callbacks };\n    }\n    release(owner) {\n        if (this.mainSubscription?.owner !== owner || this.observers.get(owner)) {\n            return;\n        }\n        this.mainSubscription = this.defaultSubscription;\n    }\n    /**\n     * Release whichever subscription in charge and get back to the default subscription\n     */\n    getBackToDefault() {\n        if (this.mainSubscription === this.defaultSubscription) {\n            return;\n        }\n        this.mainSubscription?.callbacks.release?.();\n        this.mainSubscription = this.defaultSubscription;\n    }\n    /**\n     * Check if you are currently the main stream consumer\n     */\n    isListening(owner) {\n        return this.mainSubscription?.owner === owner;\n    }\n    /**\n     * Push an event to the stream and broadcast it to consumers\n     */\n    send(event) {\n        this.mainSubscription?.callbacks.handleEvent(event);\n        this.observers.forEach((sub) => sub.callbacks.handleEvent(event));\n    }\n}\n\n/**\n * Processes all selection updates (usually from user inputs) and emits an event\n * with the new selected anchor\n */\nclass SelectionStreamProcessorImpl {\n    getters;\n    stream;\n    /**\n     * \"Active\" anchor used as a reference to compute new anchors\n     * An new initial value is given each time the stream is\n     * captured. The value is updated with each new anchor.\n     */\n    anchor;\n    defaultAnchor;\n    constructor(getters) {\n        this.getters = getters;\n        this.stream = new EventStream();\n        this.anchor = { cell: { col: 0, row: 0 }, zone: positionToZone({ col: 0, row: 0 }) };\n        this.defaultAnchor = this.anchor;\n    }\n    capture(owner, anchor, callbacks) {\n        this.stream.capture(owner, callbacks);\n        this.anchor = anchor;\n    }\n    /**\n     * Register as default subscriber and capture the event stream.\n     */\n    registerAsDefault(owner, anchor, callbacks) {\n        this.checkAnchorZoneOrThrow(anchor);\n        this.stream.registerAsDefault(owner, callbacks);\n        this.defaultAnchor = anchor;\n        this.capture(owner, anchor, callbacks);\n    }\n    resetDefaultAnchor(owner, anchor) {\n        this.checkAnchorZoneOrThrow(anchor);\n        if (this.stream.isListening(owner)) {\n            this.anchor = anchor;\n        }\n        this.defaultAnchor = anchor;\n    }\n    resetAnchor(owner, anchor) {\n        this.checkAnchorZoneOrThrow(anchor);\n        if (this.stream.isListening(owner)) {\n            this.anchor = anchor;\n        }\n    }\n    observe(owner, callbacks) {\n        this.stream.observe(owner, callbacks);\n    }\n    release(owner) {\n        if (this.stream.isListening(owner)) {\n            this.stream.release(owner);\n            this.anchor = this.defaultAnchor;\n        }\n    }\n    getBackToDefault() {\n        this.stream.getBackToDefault();\n    }\n    modifyAnchor(anchor, mode, options) {\n        const sheetId = this.getters.getActiveSheetId();\n        anchor = {\n            ...anchor,\n            zone: this.getters.expandZone(sheetId, anchor.zone),\n        };\n        return this.processEvent({\n            options,\n            anchor,\n            mode,\n        });\n    }\n    /**\n     * Select a new anchor\n     */\n    selectZone(anchor, options = { scrollIntoView: true }) {\n        return this.modifyAnchor(anchor, \"overrideSelection\", options);\n    }\n    /**\n     * Select a single cell as the new anchor.\n     */\n    selectCell(col, row) {\n        const zone = positionToZone({ col, row });\n        return this.selectZone({ zone, cell: { col, row } }, { scrollIntoView: true });\n    }\n    /**\n     * Set the selection to one of the cells adjacent to the current anchor cell.\n     */\n    moveAnchorCell(direction, step = 1) {\n        if (step !== \"end\" && step <= 0) {\n            return new DispatchResult(\"InvalidSelectionStep\" /* CommandResult.InvalidSelectionStep */);\n        }\n        const { col, row } = this.getNextAvailablePosition(direction, step);\n        return this.selectCell(col, row);\n    }\n    /**\n     * Update the current anchor such that it includes the given\n     * cell position.\n     */\n    setAnchorCorner(col, row) {\n        const sheetId = this.getters.getActiveSheetId();\n        const { col: anchorCol, row: anchorRow } = this.anchor.cell;\n        const zone = {\n            left: Math.min(anchorCol, col),\n            top: Math.min(anchorRow, row),\n            right: Math.max(anchorCol, col),\n            bottom: Math.max(anchorRow, row),\n        };\n        const expandedZone = this.getters.expandZone(sheetId, zone);\n        const anchor = { zone: expandedZone, cell: { col: anchorCol, row: anchorRow } };\n        return this.processEvent({\n            mode: \"updateAnchor\",\n            anchor: anchor,\n            options: { scrollIntoView: false },\n        });\n    }\n    /**\n     * Add a new cell to the current selection\n     */\n    addCellToSelection(col, row) {\n        const sheetId = this.getters.getActiveSheetId();\n        ({ col, row } = this.getters.getMainCellPosition({ sheetId, col, row }));\n        const zone = this.getters.expandZone(sheetId, positionToZone({ col, row }));\n        return this.processEvent({\n            options: { scrollIntoView: true },\n            anchor: { zone, cell: { col, row } },\n            mode: \"newAnchor\",\n        });\n    }\n    /**\n     * Increase or decrease the size of the current anchor zone.\n     * The anchor cell remains where it is. It's the opposite side\n     * of the anchor zone which moves.\n     */\n    resizeAnchorZone(direction, step = 1) {\n        if (step !== \"end\" && step <= 0) {\n            return new DispatchResult(\"InvalidSelectionStep\" /* CommandResult.InvalidSelectionStep */);\n        }\n        const sheetId = this.getters.getActiveSheetId();\n        const anchor = this.anchor;\n        const { col: anchorCol, row: anchorRow } = anchor.cell;\n        const { left, right, top, bottom } = anchor.zone;\n        const starting = this.getStartingPosition(direction);\n        let [deltaCol, deltaRow] = this.deltaToTarget(starting, direction, step);\n        if (deltaCol === 0 && deltaRow === 0) {\n            return DispatchResult.Success;\n        }\n        let result = anchor.zone;\n        const expand = (z) => {\n            z = reorderZone(z);\n            const { left, right, top, bottom } = this.getters.expandZone(sheetId, z);\n            return {\n                left: Math.max(0, left),\n                right: Math.min(this.getters.getNumberCols(sheetId) - 1, right),\n                top: Math.max(0, top),\n                bottom: Math.min(this.getters.getNumberRows(sheetId) - 1, bottom),\n            };\n        };\n        const { col: refCol, row: refRow } = this.getReferencePosition();\n        // check if we can shrink selection\n        let n = 0;\n        while (result !== null) {\n            n++;\n            if (deltaCol < 0) {\n                const newRight = this.getNextAvailableCol(deltaCol, right - (n - 1), refRow);\n                result = refCol <= right - n ? expand({ top, left, bottom, right: newRight }) : null;\n            }\n            if (deltaCol > 0) {\n                const newLeft = this.getNextAvailableCol(deltaCol, left + (n - 1), refRow);\n                result = left + n <= refCol ? expand({ top, left: newLeft, bottom, right }) : null;\n            }\n            if (deltaRow < 0) {\n                const newBottom = this.getNextAvailableRow(deltaRow, refCol, bottom - (n - 1));\n                result = refRow <= bottom - n ? expand({ top, left, bottom: newBottom, right }) : null;\n            }\n            if (deltaRow > 0) {\n                const newTop = this.getNextAvailableRow(deltaRow, refCol, top + (n - 1));\n                result = top + n <= refRow ? expand({ top: newTop, left, bottom, right }) : null;\n            }\n            result = result ? reorderZone(result) : result;\n            if (result && !isEqual(result, anchor.zone)) {\n                return this.processEvent({\n                    options: { scrollIntoView: true },\n                    mode: \"updateAnchor\",\n                    anchor: { zone: result, cell: { col: anchorCol, row: anchorRow } },\n                });\n            }\n        }\n        const currentZone = {\n            top: anchorRow,\n            bottom: anchorRow,\n            left: anchorCol,\n            right: anchorCol,\n        };\n        const zoneWithDelta = reorderZone({\n            top: this.getNextAvailableRow(deltaRow, refCol, top),\n            left: this.getNextAvailableCol(deltaCol, left, refRow),\n            bottom: this.getNextAvailableRow(deltaRow, refCol, bottom),\n            right: this.getNextAvailableCol(deltaCol, right, refRow),\n        });\n        result = expand(union(currentZone, zoneWithDelta));\n        const newAnchor = { zone: result, cell: { col: anchorCol, row: anchorRow } };\n        return this.processEvent({\n            anchor: newAnchor,\n            mode: \"updateAnchor\",\n            options: { scrollIntoView: true },\n        });\n    }\n    selectColumn(index, mode) {\n        const sheetId = this.getters.getActiveSheetId();\n        const bottom = this.getters.getNumberRows(sheetId) - 1;\n        let zone = { left: index, right: index, top: 0, bottom };\n        const top = this.getters.findFirstVisibleColRowIndex(sheetId, \"ROW\");\n        let col, row;\n        switch (mode) {\n            case \"overrideSelection\":\n            case \"newAnchor\":\n                col = index;\n                row = top;\n                break;\n            case \"updateAnchor\":\n                ({ col, row } = this.anchor.cell);\n                zone = union(zone, { left: col, right: col, top, bottom });\n                break;\n        }\n        return this.processEvent({\n            options: {\n                scrollIntoView: false,\n                unbounded: true,\n            },\n            anchor: { zone, cell: { col, row } },\n            mode,\n        });\n    }\n    selectRow(index, mode) {\n        const sheetId = this.getters.getActiveSheetId();\n        const right = this.getters.getNumberCols(sheetId) - 1;\n        let zone = { top: index, bottom: index, left: 0, right };\n        const left = this.getters.findFirstVisibleColRowIndex(sheetId, \"COL\");\n        let col, row;\n        switch (mode) {\n            case \"overrideSelection\":\n            case \"newAnchor\":\n                col = left;\n                row = index;\n                break;\n            case \"updateAnchor\":\n                ({ col, row } = this.anchor.cell);\n                zone = union(zone, { left, right, top: row, bottom: row });\n                break;\n        }\n        return this.processEvent({\n            options: {\n                scrollIntoView: false,\n                unbounded: true,\n            },\n            anchor: { zone, cell: { col, row } },\n            mode,\n        });\n    }\n    /**\n     * Loop the current selection while keeping the same anchor. The selection will loop through:\n     *  1) the smallest zone that contain the anchor and that have only empty cells bordering it\n     *  2) the whole sheet\n     *  3) the anchor cell\n     */\n    loopSelection() {\n        const sheetId = this.getters.getActiveSheetId();\n        const anchor = this.anchor;\n        // The whole sheet is selected, select the anchor cell\n        if (isEqual(this.anchor.zone, this.getters.getSheetZone(sheetId))) {\n            return this.modifyAnchor({ ...anchor, zone: positionToZone(anchor.cell) }, \"updateAnchor\", {\n                scrollIntoView: false,\n            });\n        }\n        const tableZone = this.getters.getContiguousZone(sheetId, anchor.zone);\n        return !deepEquals(tableZone, anchor.zone)\n            ? this.modifyAnchor({ ...anchor, zone: tableZone }, \"updateAnchor\", {\n                scrollIntoView: false,\n            })\n            : this.selectAll();\n    }\n    /**\n     * Select a \"table\" around the current selection.\n     * We define a table by the smallest zone that contain the anchor and that have only empty\n     * cells bordering it\n     */\n    selectTableAroundSelection() {\n        const sheetId = this.getters.getActiveSheetId();\n        const tableZone = this.getters.getContiguousZone(sheetId, this.anchor.zone);\n        return this.modifyAnchor({ ...this.anchor, zone: tableZone }, \"updateAnchor\", {\n            scrollIntoView: false,\n        });\n    }\n    /**\n     * Select the entire sheet\n     */\n    selectAll() {\n        const sheetId = this.getters.getActiveSheetId();\n        const bottom = this.getters.getNumberRows(sheetId) - 1;\n        const right = this.getters.getNumberCols(sheetId) - 1;\n        const zone = { left: 0, top: 0, bottom, right };\n        return this.processEvent({\n            mode: \"overrideSelection\",\n            anchor: { zone, cell: this.anchor.cell },\n            options: {\n                scrollIntoView: false,\n            },\n        });\n    }\n    isListening(owner) {\n        return this.stream.isListening(owner);\n    }\n    /**\n     * Process a new anchor selection event. If the new anchor is inside\n     * the sheet boundaries, the event is pushed to the event stream to\n     * be processed.\n     */\n    processEvent(newAnchorEvent) {\n        const event = { ...newAnchorEvent, previousAnchor: deepCopy(this.anchor) };\n        const commandResult = this.checkEventAnchorZone(event);\n        if (commandResult !== \"Success\" /* CommandResult.Success */) {\n            return new DispatchResult(commandResult);\n        }\n        this.anchor = event.anchor;\n        this.stream.send(event);\n        return DispatchResult.Success;\n    }\n    checkEventAnchorZone(event) {\n        return this.checkAnchorZone(event.anchor);\n    }\n    checkAnchorZone(anchor) {\n        const { cell, zone } = anchor;\n        if (!isInside(cell.col, cell.row, zone)) {\n            return \"InvalidAnchorZone\" /* CommandResult.InvalidAnchorZone */;\n        }\n        const { left, right, top, bottom } = zone;\n        const sheetId = this.getters.getActiveSheetId();\n        const refCol = this.getters.findVisibleHeader(sheetId, \"COL\", left, right);\n        const refRow = this.getters.findVisibleHeader(sheetId, \"ROW\", top, bottom);\n        if (refRow === undefined || refCol === undefined) {\n            return \"SelectionOutOfBound\" /* CommandResult.SelectionOutOfBound */;\n        }\n        return \"Success\" /* CommandResult.Success */;\n    }\n    checkAnchorZoneOrThrow(anchor) {\n        const result = this.checkAnchorZone(anchor);\n        if (result === \"InvalidAnchorZone\" /* CommandResult.InvalidAnchorZone */) {\n            throw new Error(_t(\"The provided anchor is invalid. The cell must be part of the zone.\"));\n        }\n    }\n    /**\n     *  ---- PRIVATE ----\n     */\n    /** Computes the next cell position in the direction of deltaX and deltaY\n     * by crossing through merges and skipping hidden cells.\n     * Note that the resulting position might be out of the sheet, it needs to be validated.\n     */\n    getNextAvailablePosition(direction, step = 1) {\n        const { col, row } = this.anchor.cell;\n        const delta = this.deltaToTarget({ col, row }, direction, step);\n        return {\n            col: this.getNextAvailableCol(delta[0], col, row),\n            row: this.getNextAvailableRow(delta[1], col, row),\n        };\n    }\n    getNextAvailableCol(delta, colIndex, rowIndex) {\n        const sheetId = this.getters.getActiveSheetId();\n        const position = { col: colIndex, row: rowIndex };\n        const isInPositionMerge = (nextCol) => this.getters.isInSameMerge(sheetId, colIndex, rowIndex, nextCol, rowIndex);\n        return this.getNextAvailableHeader(delta, \"COL\", colIndex, position, isInPositionMerge);\n    }\n    getNextAvailableRow(delta, colIndex, rowIndex) {\n        const sheetId = this.getters.getActiveSheetId();\n        const position = { col: colIndex, row: rowIndex };\n        const isInPositionMerge = (nextRow) => this.getters.isInSameMerge(sheetId, colIndex, rowIndex, colIndex, nextRow);\n        return this.getNextAvailableHeader(delta, \"ROW\", rowIndex, position, isInPositionMerge);\n    }\n    getNextAvailableHeader(delta, dimension, startingHeaderIndex, position, isInPositionMerge) {\n        const sheetId = this.getters.getActiveSheetId();\n        if (delta === 0) {\n            return startingHeaderIndex;\n        }\n        const step = Math.sign(delta);\n        let header = startingHeaderIndex + delta;\n        while (isInPositionMerge(header)) {\n            header += step;\n        }\n        while (this.getters.isHeaderHidden(sheetId, dimension, header)) {\n            header += step;\n        }\n        const outOfBound = header < 0 || header > this.getters.getNumberHeaders(sheetId, dimension) - 1;\n        if (outOfBound) {\n            if (this.getters.isHeaderHidden(sheetId, dimension, startingHeaderIndex)) {\n                return this.getNextAvailableHeader(-step, dimension, startingHeaderIndex, position, isInPositionMerge);\n            }\n            else {\n                return startingHeaderIndex;\n            }\n        }\n        return header;\n    }\n    /**\n     * Finds a visible cell in the currently selected zone starting with the anchor.\n     * If the anchor is hidden, browses from left to right and top to bottom to\n     * find a visible cell.\n     */\n    getReferencePosition() {\n        const sheetId = this.getters.getActiveSheetId();\n        const anchor = this.anchor;\n        const { left, right, top, bottom } = anchor.zone;\n        const { col: anchorCol, row: anchorRow } = anchor.cell;\n        return {\n            col: this.getters.isColHidden(sheetId, anchorCol)\n                ? this.getters.findVisibleHeader(sheetId, \"COL\", left, right) || anchorCol\n                : anchorCol,\n            row: this.getters.isRowHidden(sheetId, anchorRow)\n                ? this.getters.findVisibleHeader(sheetId, \"ROW\", top, bottom) || anchorRow\n                : anchorRow,\n        };\n    }\n    deltaToTarget(position, direction, step) {\n        switch (direction) {\n            case \"up\":\n                return step !== \"end\"\n                    ? [0, -step]\n                    : [0, this.getEndOfCluster(position, \"rows\", -1) - position.row];\n            case \"down\":\n                return step !== \"end\"\n                    ? [0, step]\n                    : [0, this.getEndOfCluster(position, \"rows\", 1) - position.row];\n            case \"left\":\n                return step !== \"end\"\n                    ? [-step, 0]\n                    : [this.getEndOfCluster(position, \"cols\", -1) - position.col, 0];\n            case \"right\":\n                return step !== \"end\"\n                    ? [step, 0]\n                    : [this.getEndOfCluster(position, \"cols\", 1) - position.col, 0];\n        }\n    }\n    // TODO rename this\n    getStartingPosition(direction) {\n        let { col, row } = this.getPosition();\n        const zone = this.anchor.zone;\n        switch (direction) {\n            case \"down\":\n            case \"up\":\n                row = row === zone.top ? zone.bottom : zone.top;\n                break;\n            case \"left\":\n            case \"right\":\n                col = col === zone.right ? zone.left : zone.right;\n                break;\n        }\n        return { col, row };\n    }\n    /**\n     * Given a starting position, compute the end of the cluster containing the position in the given\n     * direction or the start of the next cluster. We define cluster here as side-by-side cells that\n     * all have a content.\n     *\n     * We will return the end of the cluster if the given cell is inside a cluster, and the start of the\n     * next cluster if the given cell is outside a cluster or at the border of a cluster in the given direction.\n     */\n    getEndOfCluster(startPosition, dim, dir) {\n        const sheetId = this.getters.getActiveSheetId();\n        let currentPosition = startPosition;\n        // If both the current cell and the next cell are not empty, we want to go to the end of the cluster\n        const nextCellPosition = this.getNextCellPosition(startPosition, dim, dir);\n        let mode = !this.isCellSkippableInCluster({ ...currentPosition, sheetId }) &&\n            !this.isCellSkippableInCluster({ ...nextCellPosition, sheetId })\n            ? \"endOfCluster\"\n            : \"nextCluster\";\n        while (true) {\n            const nextCellPosition = this.getNextCellPosition(currentPosition, dim, dir);\n            // Break if nextPosition === currentPosition, which happens if there's no next valid position\n            if (currentPosition.col === nextCellPosition.col &&\n                currentPosition.row === nextCellPosition.row) {\n                break;\n            }\n            const isNextCellEmpty = this.isCellSkippableInCluster({ ...nextCellPosition, sheetId });\n            if (mode === \"endOfCluster\" && isNextCellEmpty) {\n                break;\n            }\n            else if (mode === \"nextCluster\" && !isNextCellEmpty) {\n                // We want to return the start of the next cluster, not the end of the empty zone\n                currentPosition = nextCellPosition;\n                break;\n            }\n            currentPosition = nextCellPosition;\n        }\n        return dim === \"cols\" ? currentPosition.col : currentPosition.row;\n    }\n    /** Computes the next cell position in the given direction by crossing through merges and skipping hidden cells.\n     *\n     * This has the same behaviour as getNextAvailablePosition() for certain arguments, but use this method instead\n     * inside directionToDelta(), which is called in getNextAvailablePosition(), to avoid possible infinite\n     * recursion.\n     */\n    getNextCellPosition(currentPosition, dimension, direction) {\n        const dimOfInterest = dimension === \"cols\" ? \"col\" : \"row\";\n        const startingPosition = { ...currentPosition };\n        const nextCoord = dimension === \"cols\"\n            ? this.getNextAvailableCol(direction, startingPosition.col, startingPosition.row)\n            : this.getNextAvailableRow(direction, startingPosition.col, startingPosition.row);\n        startingPosition[dimOfInterest] = nextCoord;\n        return { col: startingPosition.col, row: startingPosition.row };\n    }\n    getPosition() {\n        return { ...this.anchor.cell };\n    }\n    isCellSkippableInCluster(position) {\n        const mainPosition = this.getters.getMainCellPosition(position);\n        const cell = this.getters.getEvaluatedCell(mainPosition);\n        return (cell.type === CellValueType.empty || (cell.type === CellValueType.text && cell.value === \"\"));\n    }\n}\n\n/**\n * Create an empty structure according to the type of the node key:\n * string: object\n * number: array\n */\nfunction createEmptyStructure(node) {\n    if (typeof node === \"string\") {\n        return {};\n    }\n    else if (typeof node === \"number\") {\n        return [];\n    }\n    throw new Error(`Cannot create new node`);\n}\n\nclass StateObserver {\n    changes;\n    commands = [];\n    /**\n     * Record the changes which could happen in the given callback, save them in a\n     * new revision with the given id and userId.\n     */\n    recordChanges(callback) {\n        this.changes = [];\n        this.commands = [];\n        callback();\n        return { changes: this.changes, commands: this.commands };\n    }\n    addCommand(command) {\n        this.commands.push(command);\n    }\n    addChange(...args) {\n        const val = args.pop();\n        const root = args[0];\n        let value = root;\n        let key = args.at(-1);\n        const pathLength = args.length - 2;\n        for (let pathIndex = 1; pathIndex <= pathLength; pathIndex++) {\n            const p = args[pathIndex];\n            if (value[p] === undefined) {\n                const nextPath = args[pathIndex + 1];\n                value[p] = createEmptyStructure(nextPath);\n            }\n            value = value[p];\n        }\n        if (value[key] === val) {\n            return;\n        }\n        this.changes?.push({\n            key,\n            target: value,\n            before: value[key],\n        });\n        if (val === undefined) {\n            delete value[key];\n        }\n        else {\n            value[key] = val;\n        }\n    }\n}\n\n/**\n * Each axis present inside a graph needs to be identified by an unsigned integer\n * The value does not matter, it can be hardcoded.\n */\nconst catAxId = 17781237;\nconst secondaryCatAxId = 17781238;\nconst valAxId = 88853993;\nconst secondaryValAxId = 88853994;\nfunction createChart(chart, chartSheetIndex, data) {\n    const namespaces = [\n        [\"xmlns:r\", RELATIONSHIP_NSR],\n        [\"xmlns:a\", DRAWING_NS_A],\n        [\"xmlns:c\", DRAWING_NS_C],\n    ];\n    const chartShapeProperty = shapeProperty({\n        backgroundColor: chart.data.backgroundColor,\n        line: { color: \"000000\" },\n    });\n    // <manualLayout/> to manually position the chart in the figure container\n    let title = escapeXml ``;\n    if (chart.data.title?.text) {\n        const color = chart.data.title.color\n            ? toXlsxHexColor(chart.data.title.color)\n            : chart.data.fontColor;\n        title = escapeXml /*xml*/ `\n      <c:title>\n        ${insertText(chart.data.title.text, color, DEFAULT_CHART_FONT_SIZE, chart.data.title)}\n        <c:overlay val=\"0\" />\n      </c:title>\n    `;\n    }\n    // switch on chart type\n    let plot = escapeXml ``;\n    switch (chart.data.type) {\n        case \"bar\":\n            plot = addBarChart(chart.data);\n            break;\n        case \"combo\":\n            plot = addComboChart(chart.data);\n            break;\n        case \"line\":\n            plot = addLineChart(chart.data);\n            break;\n        case \"scatter\":\n            plot = addScatterChart(chart.data);\n            break;\n        case \"pie\":\n            plot = addDoughnutChart(chart.data, chartSheetIndex, data, { holeSize: 0 });\n            break;\n    }\n    let position = \"t\";\n    switch (chart.data.legendPosition) {\n        case \"bottom\":\n            position = \"b\";\n            break;\n        case \"left\":\n            position = \"l\";\n            break;\n        case \"right\":\n            position = \"r\";\n            break;\n        case \"top\":\n            position = \"t\";\n            break;\n    }\n    const fontColor = chart.data.fontColor;\n    const xml = escapeXml /*xml*/ `\n    <c:chartSpace ${formatAttributes(namespaces)}>\n      <c:roundedCorners val=\"0\" />\n      <!-- <manualLayout/> to manually position the chart in the figure container -->\n      ${chartShapeProperty}\n      <c:chart>\n        ${title}\n        <c:autoTitleDeleted val=\"0\" />\n        <c:plotArea>\n          <!-- how the chart element is placed on the chart -->\n          <c:layout />\n          ${plot}\n          ${shapeProperty({ backgroundColor: chart.data.backgroundColor })}\n        </c:plotArea>\n        ${addLegend(position, fontColor)}\n      </c:chart>\n    </c:chartSpace>\n  `;\n    return parseXML(xml);\n}\nfunction shapeProperty(params) {\n    return escapeXml /*xml*/ `\n    <c:spPr>\n      ${params.backgroundColor ? solidFill(params.backgroundColor) : \"\"}\n      ${params.line ? lineAttributes(params.line) : \"\"}\n    </c:spPr>\n  `;\n}\nfunction solidFill(color) {\n    return escapeXml /*xml*/ `\n    <a:solidFill>\n      <a:srgbClr val=\"${color}\"/>\n    </a:solidFill>\n  `;\n}\nfunction lineAttributes(params) {\n    const attrs = [[\"cmpd\", \"sng\"]];\n    if (params.width) {\n        attrs.push([\"w\", convertDotValueToEMU(params.width)]);\n    }\n    const lineStyle = params.style ? escapeXml /*xml*/ `<a:prstDash val=\"${params.style}\"/>` : \"\";\n    return escapeXml /*xml*/ `\n    <a:ln ${formatAttributes(attrs)}>\n      ${solidFill(params.color)}\n      ${lineStyle}\n    </a:ln>\n  `;\n}\nfunction insertText(text, fontColor = \"000000\", fontsize = DEFAULT_CHART_FONT_SIZE, style = {}) {\n    return escapeXml /*xml*/ `\n    <c:tx>\n      <c:rich>\n        <a:bodyPr />\n        <a:lstStyle />\n        <a:p>\n          <a:pPr lvl=\"0\">\n            <a:defRPr b=\"${style?.bold ? 1 : 0}\" i=\"${style?.italic ? 1 : 0}\">\n              ${solidFill(fontColor)}\n              <a:latin typeface=\"+mn-lt\"/>\n            </a:defRPr>\n          </a:pPr>\n          <a:r> <!-- Runs -->\n            <a:rPr b=\"${style?.bold ? 1 : 0}\" i=\"${style?.italic ? 1 : 0}\" sz=\"${fontsize * 100}\"/>\n            <a:t>${text}</a:t>\n          </a:r>\n        </a:p>\n      </c:rich>\n    </c:tx>\n  `;\n}\nfunction insertTextProperties(fontsize = 12, fontColor = \"000000\", bold = false, italic = false) {\n    const defPropertiesAttributes = [\n        [\"b\", bold ? \"1\" : \"0\"],\n        [\"i\", italic ? \"1\" : \"0\"],\n        [\"sz\", fontsize * 100],\n    ];\n    return escapeXml /*xml*/ `\n    <c:txPr>\n      <a:bodyPr/>\n      <a:lstStyle/>\n      <a:p>\n        <a:pPr lvl=\"0\">\n          <a:defRPr ${formatAttributes(defPropertiesAttributes)}>\n            ${solidFill(fontColor)}\n            <a:latin typeface=\"+mn-lt\"/>\n          </a:defRPr>\n        </a:pPr>\n      </a:p>\n    </c:txPr>\n  `;\n}\nfunction extractDataSetLabel(label) {\n    if (!label) {\n        return escapeXml /*xml*/ ``;\n    }\n    if (\"text\" in label && label.text) {\n        return escapeXml /*xml*/ `\n      <c:tx><c:v>${label.text}</c:v></c:tx>\n    `;\n    }\n    if (\"reference\" in label && label.reference) {\n        return escapeXml /*xml*/ `\n      <c:tx>\n      ${stringRef(label.reference)}\n      </c:tx>\n    `;\n    }\n    return escapeXml /*xml*/ ``;\n}\nfunction addBarChart(chart) {\n    // gapWitdh and overlap that define the space between clusters (in %) and the overlap between datasets (from -100: completely scattered to 100, completely overlapped)\n    // see gapWidth : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_gapWidth_topic_ID0EFVEQB.html#topic_ID0EFVEQB\n    // see overlap : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_overlap_topic_ID0ELYQQB.html#topic_ID0ELYQQB\n    //\n    // overlap and gapWitdh seems to be by default at -20 and 20 in chart.js.\n    // See https://www.chartjs.org/docs/latest/charts/bar.html and https://www.chartjs.org/docs/latest/charts/bar.html#barpercentage-vs-categorypercentage\n    const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? \"\");\n    const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);\n    const leftDataSetsNodes = [];\n    const rightDataSetsNodes = [];\n    for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {\n        const color = toXlsxHexColor(colors.next());\n        const dataShapeProperty = shapeProperty({\n            backgroundColor: color,\n            line: { color },\n        });\n        const dataSetNode = escapeXml /*xml*/ `\n      <c:ser>\n        <c:idx val=\"${dsIndex}\"/>\n        <c:order val=\"${dsIndex}\"/>\n        ${extractDataSetLabel(dataset.label)}\n        ${dataShapeProperty}\n        ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : \"\"} <!-- x-coordinate values -->\n        <c:val> <!-- x-coordinate values -->\n          ${numberRef(dataset.range)}\n        </c:val>\n      </c:ser>\n    `;\n        if (dataset.rightYAxis) {\n            rightDataSetsNodes.push(dataSetNode);\n        }\n        else {\n            leftDataSetsNodes.push(dataSetNode);\n        }\n    }\n    const grouping = chart.stacked ? \"stacked\" : \"clustered\";\n    const overlap = chart.stacked ? 100 : -20;\n    return escapeXml /*xml*/ `\n  ${leftDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n        <c:barChart>\n          <c:barDir val=\"col\"/>\n          <c:grouping val=\"${grouping}\"/>\n          <c:overlap val=\"${overlap}\"/>\n          <c:gapWidth val=\"70\"/>\n          <!-- each data marker in the series does not have a different color -->\n          <c:varyColors val=\"0\"/>\n          ${joinXmlNodes(leftDataSetsNodes)}\n          <c:axId val=\"${catAxId}\" />\n          <c:axId val=\"${valAxId}\" />\n        </c:barChart>\n        ${addAx(\"b\", \"c:catAx\", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)}\n        ${addAx(\"l\", \"c:valAx\", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}\n      `\n        : \"\"}\n  ${rightDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n        <c:barChart>\n          <c:barDir val=\"col\"/>\n          <c:grouping val=\"${grouping}\"/>\n          <c:overlap val=\"${overlap}\"/>\n          <c:gapWidth val=\"70\"/>\n          <!-- each data marker in the series does not have a different color -->\n          <c:varyColors val=\"0\"/>\n          ${joinXmlNodes(rightDataSetsNodes)}\n          <c:axId val=\"${catAxId + 1}\" />\n          <c:axId val=\"${valAxId + 1}\" />\n        </c:barChart>\n        ${addAx(\"b\", \"c:catAx\", catAxId + 1, valAxId + 1, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length ? 1 : 0)}\n        ${addAx(\"r\", \"c:valAx\", valAxId + 1, catAxId + 1, chart.axesDesign?.y1?.title, chart.fontColor)}\n      `\n        : \"\"}`;\n}\nfunction addComboChart(chart) {\n    // gapWitdh and overlap that define the space between clusters (in %) and the overlap between datasets (from -100: completely scattered to 100, completely overlapped)\n    // see gapWidth : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_gapWidth_topic_ID0EFVEQB.html#topic_ID0EFVEQB\n    // see overlap : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_overlap_topic_ID0ELYQQB.html#topic_ID0ELYQQB\n    //\n    // overlap and gapWitdh seems to be by default at -20 and 20 in chart.js.\n    // See https://www.chartjs.org/docs/latest/charts/bar.html and https://www.chartjs.org/docs/latest/charts/bar.html#barpercentage-vs-categorypercentage\n    const dataSets = chart.dataSets;\n    const dataSetsColors = dataSets.map((ds) => ds.backgroundColor ?? \"\");\n    const colors = new ColorGenerator(dataSets.length, dataSetsColors);\n    let dataSet = dataSets[0];\n    const firstColor = toXlsxHexColor(colors.next());\n    const useRightAxisForBarSerie = dataSet.rightYAxis ?? false;\n    const barDataSetNode = escapeXml /*xml*/ `\n    <c:ser>\n      <c:idx val=\"0\"/>\n      <c:order val=\"0\"/>\n      ${extractDataSetLabel(dataSet.label)}\n      ${shapeProperty({\n        backgroundColor: firstColor,\n        line: { color: firstColor },\n    })}\n      ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : \"\"}\n      <!-- x-coordinate values -->\n      <c:val>\n        ${numberRef(dataSet.range)}\n      </c:val>\n    </c:ser>\n  `;\n    const leftDataSetsNodes = [];\n    const rightDataSetsNodes = [];\n    for (let dsIndex = 1; dsIndex < dataSets.length; dsIndex++) {\n        dataSet = dataSets[dsIndex];\n        const color = toXlsxHexColor(colors.next());\n        const dataShapeProperty = shapeProperty({\n            backgroundColor: color,\n            line: { color },\n        });\n        const dataSetNode = escapeXml /*xml*/ `\n      <c:ser>\n        <c:idx val=\"${dsIndex}\"/>\n        <c:order val=\"${dsIndex}\"/>\n        <c:smooth val=\"0\"/>\n        <c:marker>\n          <c:symbol val=\"circle\" />\n          <c:size val=\"5\"/>\n          ${dataShapeProperty}\n        </c:marker>\n        ${extractDataSetLabel(dataSet.label)}\n        ${dataShapeProperty}\n        ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : \"\"}\n        <!-- x-coordinate values -->\n        <c:val>\n          ${numberRef(dataSet.range)}\n        </c:val>\n      </c:ser>\n    `;\n        if (dataSet.rightYAxis) {\n            rightDataSetsNodes.push(dataSetNode);\n        }\n        else {\n            leftDataSetsNodes.push(dataSetNode);\n        }\n    }\n    const overlap = chart.stacked ? 100 : -20;\n    return escapeXml /*xml*/ `\n    <c:barChart>\n      <c:barDir val=\"col\"/>\n      <c:grouping val=\"clustered\"/>\n      <c:overlap val=\"${overlap}\"/>\n      <c:gapWidth val=\"70\"/>\n      <!-- each data marker in the series does not have a different color -->\n      <c:varyColors val=\"0\"/>\n      ${barDataSetNode}\n      <c:axId val=\"${useRightAxisForBarSerie ? secondaryCatAxId : catAxId}\" />\n      <c:axId val=\"${useRightAxisForBarSerie ? secondaryValAxId : valAxId}\" />\n    </c:barChart>\n    ${leftDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n        <c:lineChart>\n          <c:grouping val=\"standard\"/>\n          <!-- each data marker in the series does not have a different color -->\n          <c:varyColors val=\"0\"/>\n          ${joinXmlNodes(leftDataSetsNodes)}\n          <c:axId val=\"${catAxId}\" />\n          <c:axId val=\"${valAxId}\" />\n        </c:lineChart>\n      `\n        : \"\"}\n    ${rightDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n        <c:lineChart>\n          <c:grouping val=\"standard\"/>\n          <!-- each data marker in the series does not have a different color -->\n          <c:varyColors val=\"0\"/>\n          ${joinXmlNodes(rightDataSetsNodes)}\n          <c:axId val=\"${secondaryCatAxId}\" />\n          <c:axId val=\"${secondaryValAxId}\" />\n        </c:lineChart>\n      `\n        : \"\"}\n    ${!useRightAxisForBarSerie || leftDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n        ${addAx(\"b\", \"c:catAx\", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length ? 1 : 0)}\n        ${addAx(\"l\", \"c:valAx\", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}\n      `\n        : \"\"}\n    ${useRightAxisForBarSerie || rightDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n        ${addAx(\"b\", \"c:catAx\", secondaryCatAxId, secondaryValAxId, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length || !useRightAxisForBarSerie ? 1 : 0)}\n        ${addAx(\"r\", \"c:valAx\", secondaryValAxId, secondaryCatAxId, chart.axesDesign?.y1?.title, chart.fontColor)}\n      `\n        : \"\"}\n  `;\n}\nfunction addLineChart(chart) {\n    const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? \"\");\n    const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);\n    const leftDataSetsNodes = [];\n    const rightDataSetsNodes = [];\n    for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {\n        const color = toXlsxHexColor(colors.next());\n        const dataShapeProperty = shapeProperty({\n            line: {\n                width: 2.5,\n                style: \"solid\",\n                color,\n            },\n        });\n        const dataSetNode = escapeXml /*xml*/ `\n      <c:ser>\n        <c:idx val=\"${dsIndex}\"/>\n        <c:order val=\"${dsIndex}\"/>\n        <c:smooth val=\"0\"/>\n        <c:marker>\n          <c:symbol val=\"circle\" />\n          <c:size val=\"5\"/>\n          ${shapeProperty({ backgroundColor: color, line: { color } })}\n        </c:marker>\n        ${extractDataSetLabel(dataset.label)}\n        ${dataShapeProperty}\n        ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : \"\"} <!-- x-coordinate values -->\n        <c:val> <!-- x-coordinate values -->\n          ${numberRef(dataset.range)}\n        </c:val>\n      </c:ser>\n    `;\n        if (dataset.rightYAxis) {\n            rightDataSetsNodes.push(dataSetNode);\n        }\n        else {\n            leftDataSetsNodes.push(dataSetNode);\n        }\n    }\n    const grouping = chart.stacked ? \"stacked\" : \"standard\";\n    return escapeXml /*xml*/ `\n    ${leftDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n        <c:lineChart>\n          <c:grouping val=\"${grouping}\"/>\n          <!-- each data marker in the series does not have a different color -->\n          <c:varyColors val=\"0\"/>\n          ${joinXmlNodes(leftDataSetsNodes)}\n          <c:axId val=\"${catAxId}\" />\n          <c:axId val=\"${valAxId}\" />\n        </c:lineChart>\n        ${addAx(\"b\", \"c:catAx\", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)}\n        ${addAx(\"l\", \"c:valAx\", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}\n      `\n        : \"\"}\n    ${rightDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n        <c:lineChart>\n          <c:grouping val=\"${grouping}\"/>\n          <!-- each data marker in the series does not have a different color -->\n          <c:varyColors val=\"0\"/>\n          ${joinXmlNodes(rightDataSetsNodes)}\n          <c:axId val=\"${catAxId + 1}\" />\n          <c:axId val=\"${valAxId + 1}\" />\n        </c:lineChart>\n        ${addAx(\"b\", \"c:catAx\", catAxId + 1, valAxId + 1, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length ? 1 : 0)}\n        ${addAx(\"r\", \"c:valAx\", valAxId + 1, catAxId + 1, chart.axesDesign?.y1?.title, chart.fontColor)}\n      `\n        : \"\"}\n  `;\n}\nfunction addScatterChart(chart) {\n    const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? \"\");\n    const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);\n    const leftDataSetsNodes = [];\n    const rightDataSetsNodes = [];\n    for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {\n        const color = toXlsxHexColor(colors.next());\n        const dataSetNode = escapeXml /*xml*/ `\n      <c:ser>\n        <c:idx val=\"${dsIndex}\"/>\n        <c:order val=\"${dsIndex}\"/>\n        <c:smooth val=\"0\"/>\n        <c:spPr>\n          <a:ln w=\"19050\" cap=\"rnd\">\n            <a:noFill/>\n            <a:round/>\n          </a:ln>\n          <a:effectLst/>\n        </c:spPr>\n        <c:marker>\n          <c:symbol val=\"circle\" />\n          <c:size val=\"5\"/>\n          ${shapeProperty({ backgroundColor: color, line: { color } })}\n        </c:marker>\n        ${extractDataSetLabel(dataset.label)}\n        ${chart.labelRange\n            ? escapeXml /*xml*/ `<c:xVal> <!-- x-coordinate values -->\n              ${numberRef(chart.labelRange)}\n            </c:xVal>`\n            : \"\"}\n        <c:yVal> <!-- y-coordinate values -->\n          ${numberRef(dataset.range)}\n        </c:yVal>\n      </c:ser>\n    `;\n        if (dataset.rightYAxis) {\n            rightDataSetsNodes.push(dataSetNode);\n        }\n        else {\n            leftDataSetsNodes.push(dataSetNode);\n        }\n    }\n    return escapeXml /*xml*/ `\n  ${leftDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n      <c:scatterChart>\n        <!-- each data marker in the series does not have a different color -->\n        <c:varyColors val=\"0\"/>\n        <c:scatterStyle val=\"lineMarker\"/>\n        ${joinXmlNodes(leftDataSetsNodes)}\n        <c:axId val=\"${catAxId}\" />\n        <c:axId val=\"${valAxId}\" />\n      </c:scatterChart>\n      ${addAx(\"b\", \"c:valAx\", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)}\n      ${addAx(\"l\", \"c:valAx\", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}\n    `\n        : \"\"}\n  ${rightDataSetsNodes.length\n        ? escapeXml /*xml*/ `\n      <c:scatterChart>\n        <!-- each data marker in the series does not have a different color -->\n        <c:varyColors val=\"0\"/>\n        <c:scatterStyle val=\"lineMarker\"/>\n        ${joinXmlNodes(rightDataSetsNodes)}\n        <c:axId val=\"${catAxId + 1}\" />\n        <c:axId val=\"${valAxId + 1}\" />\n      </c:scatterChart>\n      ${addAx(\"b\", \"c:valAx\", catAxId + 1, valAxId + 1, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length ? 1 : 0)}\n      ${addAx(\"r\", \"c:valAx\", valAxId + 1, catAxId + 1, chart.axesDesign?.y1?.title, chart.fontColor)}\n    `\n        : \"\"}`;\n}\nfunction addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {\n    const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));\n    const colors = new ColorGenerator(maxLength);\n    const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));\n    const dataSetsNodes = [];\n    for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {\n        //dataset slice labels\n        const dsSize = getRangeSize(dataset.range, chartSheetIndex, data);\n        const dataPoints = [];\n        for (const index of range(0, dsSize)) {\n            const pointShapeProperty = shapeProperty({\n                backgroundColor: doughnutColors[index],\n                line: { color: \"FFFFFF\", width: 1.5 },\n            });\n            dataPoints.push(escapeXml /*xml*/ `\n        <c:dPt>\n          <c:idx val=\"${index}\"/>\n          ${pointShapeProperty}\n        </c:dPt>\n      `);\n        }\n        dataSetsNodes.push(escapeXml /*xml*/ `\n      <c:ser>\n        <c:idx val=\"${dsIndex}\"/>\n        <c:order val=\"${dsIndex}\"/>\n        ${extractDataSetLabel(dataset.label)}\n        ${joinXmlNodes(dataPoints)}\n        ${insertDataLabels({ showLeaderLines: true })}\n        ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : \"\"}\n        <c:val>\n          ${numberRef(dataset.range)}\n        </c:val>\n      </c:ser>\n    `);\n    }\n    return escapeXml /*xml*/ `\n    <c:doughnutChart>\n      <c:varyColors val=\"1\" />\n      <c:holeSize val=\"${holeSize}\" />\n      ${insertDataLabels()}\n      ${joinXmlNodes(dataSetsNodes)}\n    </c:doughnutChart>\n  `;\n}\nfunction insertDataLabels({ showLeaderLines } = { showLeaderLines: false }) {\n    return escapeXml /*xml*/ `\n    <dLbls>\n      <c:showLegendKey val=\"0\"/>\n      <c:showVal val=\"0\"/>\n      <c:showCatName val=\"0\"/>\n      <c:showSerName val=\"0\"/>\n      <c:showPercent val=\"0\"/>\n      <c:showBubbleSize val=\"0\"/>\n      <c:showLeaderLines val=\"${showLeaderLines ? \"1\" : \"0\"}\"/>\n    </dLbls>\n  `;\n}\nfunction addAx(position, axisName, axId, crossAxId, title, defaultFontColor, deleteAxis = 0) {\n    // Each Axis present inside a graph needs to be identified by an unsigned integer in order to be referenced by its crossAxis.\n    // I.e. x-axis, will reference y-axis and vice-versa.\n    const color = title?.color ? toXlsxHexColor(title.color) : defaultFontColor;\n    return escapeXml /*xml*/ `\n    <${axisName}>\n      <c:axId val=\"${axId}\"/>\n      <c:crossAx val=\"${crossAxId}\"/> <!-- reference to the other axe of the chart -->\n      <c:crosses val=\"${position === \"b\" || position === \"l\" ? \"min\" : \"max\"}\"/>\n      <c:delete val=\"${deleteAxis}\"/> <!-- by default, axis are not displayed -->\n      <c:scaling>\n        <c:orientation  val=\"minMax\" />\n      </c:scaling>\n      <c:axPos val=\"${position}\" />\n      ${insertMajorGridLines()}\n      <c:majorTickMark val=\"out\" />\n      <c:minorTickMark val=\"none\" />\n      <c:numFmt formatCode=\"General\" sourceLinked=\"1\" />\n      <c:title>\n        ${insertText(title?.text ?? \"\", color, 10, title)}\n      </c:title>\n      ${insertTextProperties(10, defaultFontColor)}\n    </${axisName}>\n    <!-- <tickLblPos/> omitted -->\n  `;\n}\nfunction addLegend(position, fontColor) {\n    return escapeXml /*xml*/ `\n    <c:legend>\n      <c:legendPos val=\"${position}\"/>\n      <c:overlay val=\"0\"/>\n      ${insertTextProperties(10, fontColor)}\n    </c:legend>\n  `;\n}\nfunction insertMajorGridLines(color = \"B7B7B7\") {\n    return escapeXml /*xml*/ `\n    <c:majorGridlines>\n      ${shapeProperty({ line: { color } })}\n    </c:majorGridlines>\n  `;\n}\nfunction stringRef(reference) {\n    return escapeXml /*xml*/ `\n    <c:strRef>\n      <c:f>${reference}</c:f>\n    </c:strRef>\n  `;\n}\nfunction numberRef(reference) {\n    return escapeXml /*xml*/ `\n    <c:numRef>\n      <c:f>${reference}</c:f>\n      <c:numCache />\n    </c:numRef>\n  `;\n}\n\nfunction addFormula(cell) {\n    const formula = cell.content;\n    if (!formula) {\n        return { attrs: [], node: escapeXml `` };\n    }\n    const type = getCellType(cell.value);\n    if (type === undefined) {\n        return { attrs: [], node: escapeXml `` };\n    }\n    const attrs = [[\"t\", type]];\n    const XlsxFormula = adaptFormulaToExcel(formula);\n    const exportedValue = adaptFormulaValueToExcel(cell.value);\n    const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;\n    return { attrs, node };\n}\nfunction addContent(content, sharedStrings, forceString = false) {\n    let value = content;\n    const attrs = [];\n    const clearValue = value.trim().toUpperCase();\n    if (!forceString && [\"TRUE\", \"FALSE\"].includes(clearValue)) {\n        value = clearValue === \"TRUE\" ? \"1\" : \"0\";\n        attrs.push([\"t\", \"b\"]);\n    }\n    else if (forceString || !isNumber(value, DEFAULT_LOCALE)) {\n        value = pushElement(content, sharedStrings);\n        attrs.push([\"t\", \"s\"]);\n    }\n    return { attrs, node: escapeXml /*xml*/ `<v>${value}</v>` };\n}\nfunction adaptFormulaToExcel(formulaText) {\n    if (formulaText[0] === \"=\") {\n        formulaText = formulaText.slice(1);\n    }\n    let ast;\n    try {\n        ast = parse(formulaText);\n    }\n    catch (error) {\n        return formulaText;\n    }\n    ast = convertAstNodes(ast, \"STRING\", convertDateFormat);\n    ast = convertAstNodes(ast, \"FUNCALL\", (ast) => {\n        ast = { ...ast, value: ast.value.toUpperCase() };\n        ast = prependNonRetrocompatibleFunction(ast);\n        ast = addMissingRequiredArgs(ast);\n        return ast;\n    });\n    ast = convertAstNodes(ast, \"REFERENCE\", (ast) => {\n        return ast.value === CellErrorType.InvalidReference ? { ...ast, value: \"#REF!\" } : ast;\n    });\n    return ast ? astToFormula(ast) : formulaText;\n}\nfunction adaptFormulaValueToExcel(formulaValue) {\n    return formulaValue === CellErrorType.InvalidReference ? \"#REF!\" : formulaValue;\n}\n/**\n * Some Excel function need required args that might not be mandatory in o-spreadsheet.\n * This adds those missing args.\n */\nfunction addMissingRequiredArgs(ast) {\n    const formulaName = ast.value.toUpperCase();\n    const args = ast.args;\n    const exportDefaultArgs = FORCE_DEFAULT_ARGS_FUNCTIONS[formulaName];\n    if (exportDefaultArgs) {\n        const requiredArgs = functionRegistry.content[formulaName].args.filter((el) => !el.optional);\n        const diffArgs = requiredArgs.length - ast.args.length;\n        if (diffArgs) {\n            // We know that we have at least 1 default Value missing\n            for (let i = ast.args.length; i < requiredArgs.length; i++) {\n                const currentDefaultArg = exportDefaultArgs[i - diffArgs];\n                args.push({ type: currentDefaultArg.type, value: currentDefaultArg.value });\n            }\n        }\n    }\n    return { ...ast, args };\n}\n/**\n * Prepend function names that are not compatible with Old Excel versions\n */\nfunction prependNonRetrocompatibleFunction(ast) {\n    const formulaName = ast.value.toUpperCase();\n    return {\n        ...ast,\n        value: NON_RETROCOMPATIBLE_FUNCTIONS.includes(formulaName)\n            ? `_xlfn.${formulaName}`\n            : formulaName,\n    };\n}\n/**\n * Convert strings that correspond to a date to the format YYYY-DD-MM\n */\nfunction convertDateFormat(ast) {\n    const value = ast.value.replace(new RegExp('\"', \"g\"), \"\");\n    const internalDate = parseDateTime(value, DEFAULT_LOCALE);\n    if (internalDate) {\n        let format = [];\n        if (mdyDateRegexp.test(value) || ymdDateRegexp.test(value)) {\n            format.push(\"yyyy-mm-dd\");\n        }\n        if (timeRegexp.test(value)) {\n            format.push(\"hh:mm:ss\");\n        }\n        return {\n            ...ast,\n            value: formatValue(internalDate.value, { format: format.join(\" \"), locale: DEFAULT_LOCALE }),\n        };\n    }\n    else {\n        return { ...ast, value: ast.value.replace(/\\\\\"/g, `\"\"`) };\n    }\n}\n\nfunction addConditionalFormatting(dxfs, conditionalFormats) {\n    // Conditional Formats\n    const cfNodes = [];\n    for (const cf of conditionalFormats) {\n        // Special case for each type of rule: might be better to extract that logic in dedicated functions\n        switch (cf.rule.type) {\n            case \"CellIsRule\":\n                cfNodes.push(addCellIsRule(cf, cf.rule, dxfs));\n                break;\n            case \"ColorScaleRule\":\n                cfNodes.push(addColorScaleRule(cf, cf.rule));\n                break;\n            case \"IconSetRule\":\n                cfNodes.push(addIconSetRule(cf, cf.rule));\n                break;\n            default:\n                // @ts-ignore Typescript knows it will never happen at compile time\n                console.warn(`Conditional formatting ${cf.rule.type} not implemented`);\n                break;\n        }\n    }\n    return cfNodes;\n}\n// ----------------------\n//         RULES\n// ----------------------\nfunction addCellIsRule(cf, rule, dxfs) {\n    const ruleAttributes = commonCfAttributes(cf);\n    const operator = convertOperator(rule.operator);\n    ruleAttributes.push(...cellRuleTypeAttributes(rule), [\"operator\", operator]);\n    const formulas = cellRuleFormula(cf.ranges, rule).map((formula) => escapeXml /*xml*/ `<formula>${formula}</formula>`);\n    const dxf = {\n        font: {\n            color: { rgb: rule.style.textColor },\n            bold: rule.style.bold,\n            italic: rule.style.italic,\n            strike: rule.style.strikethrough,\n            underline: rule.style.underline,\n        },\n    };\n    if (rule.style.fillColor) {\n        dxf.fill = { fgColor: { rgb: rule.style.fillColor } };\n    }\n    ruleAttributes.push([\"dxfId\", pushElement(dxf, dxfs)]);\n    return escapeXml /*xml*/ `\n    <conditionalFormatting sqref=\"${cf.ranges.join(\" \")}\">\n      <cfRule ${formatAttributes(ruleAttributes)}>\n        ${joinXmlNodes(formulas)}\n      </cfRule>\n    </conditionalFormatting>\n  `;\n}\nfunction cellRuleFormula(ranges, rule) {\n    const firstCell = ranges[0].split(\":\")[0];\n    const values = rule.values;\n    switch (rule.operator) {\n        case \"ContainsText\":\n            return [`NOT(ISERROR(SEARCH(\"${values[0]}\",${firstCell})))`];\n        case \"NotContains\":\n            return [`ISERROR(SEARCH(\"${values[0]}\",${firstCell}))`];\n        case \"BeginsWith\":\n            return [`LEFT(${firstCell},LEN(\"${values[0]}\"))=\"${values[0]}\"`];\n        case \"EndsWith\":\n            return [`RIGHT(${firstCell},LEN(\"${values[0]}\"))=\"${values[0]}\"`];\n        case \"IsEmpty\":\n            return [`LEN(TRIM(${firstCell}))=0`];\n        case \"IsNotEmpty\":\n            return [`LEN(TRIM(${firstCell}))>0`];\n        case \"Equal\":\n        case \"NotEqual\":\n        case \"GreaterThan\":\n        case \"GreaterThanOrEqual\":\n        case \"LessThan\":\n        case \"LessThanOrEqual\":\n            return [values[0]];\n        case \"Between\":\n        case \"NotBetween\":\n            return [values[0], values[1]];\n    }\n}\nfunction cellRuleTypeAttributes(rule) {\n    const operator = convertOperator(rule.operator);\n    switch (rule.operator) {\n        case \"ContainsText\":\n        case \"NotContains\":\n        case \"BeginsWith\":\n        case \"EndsWith\":\n            return [\n                [\"type\", operator],\n                [\"text\", rule.values[0]],\n            ];\n        case \"IsEmpty\":\n        case \"IsNotEmpty\":\n            return [[\"type\", operator]];\n        case \"Equal\":\n        case \"NotEqual\":\n        case \"GreaterThan\":\n        case \"GreaterThanOrEqual\":\n        case \"LessThan\":\n        case \"LessThanOrEqual\":\n        case \"Between\":\n        case \"NotBetween\":\n            return [[\"type\", \"cellIs\"]];\n    }\n}\nfunction addColorScaleRule(cf, rule) {\n    const ruleAttributes = commonCfAttributes(cf);\n    ruleAttributes.push([\"type\", \"colorScale\"]);\n    /** mimic our flow:\n     * for a given ColorScale CF, each range of the \"ranges set\" has its own behaviour.\n     */\n    const conditionalFormats = [];\n    for (const range of cf.ranges) {\n        const cfValueObject = [];\n        const colors = [];\n        let canExport = true;\n        for (let position of [\"minimum\", \"midpoint\", \"maximum\"]) {\n            const threshold = rule[position];\n            if (!threshold) {\n                // pass midpoint if not defined\n                continue;\n            }\n            if (threshold.type === \"formula\") {\n                canExport = false;\n                continue;\n            }\n            cfValueObject.push(thresholdAttributes(threshold, position));\n            colors.push([[\"rgb\", toXlsxHexColor(colorNumberString(threshold.color))]]);\n        }\n        if (!canExport) {\n            console.warn(\"Conditional formats with formula rules are not supported at the moment. The rule is therefore skipped.\");\n            continue;\n        }\n        const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ `<cfvo ${formatAttributes(attrs)}/>`);\n        const cfColorNodes = colors.map((attrs) => escapeXml /*xml*/ `<color ${formatAttributes(attrs)}/>`);\n        conditionalFormats.push(escapeXml /*xml*/ `\n      <conditionalFormatting sqref=\"${range}\">\n        <cfRule ${formatAttributes(ruleAttributes)}>\n          <colorScale>\n            ${joinXmlNodes(cfValueObjectNodes)}\n            ${joinXmlNodes(cfColorNodes)}\n          </colorScale>\n        </cfRule>\n      </conditionalFormatting>\n    `);\n    }\n    return joinXmlNodes(conditionalFormats);\n}\nfunction addIconSetRule(cf, rule) {\n    const ruleAttributes = commonCfAttributes(cf);\n    ruleAttributes.push([\"type\", \"iconSet\"]);\n    /** mimic our flow:\n     * for a given IconSet CF, each range of the \"ranges set\" has its own behaviour.\n     */\n    const conditionalFormats = [];\n    for (const range of cf.ranges) {\n        const cfValueObject = [\n            // It looks like they always want 3 cfvo and they add a dummy entry\n            [\n                [\"type\", \"percent\"],\n                [\"val\", 0],\n            ],\n        ];\n        let canExport = true;\n        for (let position of [\"lowerInflectionPoint\", \"upperInflectionPoint\"]) {\n            if (rule[position].type === \"formula\") {\n                canExport = false;\n                continue;\n            }\n            const threshold = rule[position];\n            cfValueObject.push([\n                ...thresholdAttributes(threshold, position),\n                [\"gte\", threshold.operator === \"ge\" ? \"1\" : \"0\"],\n            ]);\n        }\n        if (!canExport) {\n            console.warn(\"Conditional formats with formula rules are not supported at the moment. The rule is therefore skipped.\");\n            continue;\n        }\n        const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ `<cfvo ${formatAttributes(attrs)} />`);\n        conditionalFormats.push(escapeXml /*xml*/ `\n      <conditionalFormatting sqref=\"${range}\">\n        <cfRule ${formatAttributes(ruleAttributes)}>\n          <iconSet iconSet=\"${getIconSet(rule.icons)}\">\n            ${joinXmlNodes(cfValueObjectNodes)}\n          </iconSet>\n        </cfRule>\n      </conditionalFormatting>\n    `);\n    }\n    return joinXmlNodes(conditionalFormats);\n}\n// ----------------------\n//         MISC\n// ----------------------\nfunction commonCfAttributes(cf) {\n    return [\n        [\"priority\", 1],\n        [\"stopIfTrue\", cf.stopIfTrue ? 1 : 0],\n    ];\n}\nfunction getIconSet(iconSet) {\n    return XLSX_ICONSET_MAP[Object.keys(XLSX_ICONSET_MAP).find((key) => iconSet.upper.toLowerCase().startsWith(key)) ||\n        \"dots\"];\n}\nfunction thresholdAttributes(threshold, position) {\n    const type = getExcelThresholdType(threshold.type, position);\n    const attrs = [[\"type\", type]];\n    if (type !== \"min\" && type !== \"max\") {\n        // what if the formula is not correct\n        // references cannot be relative :/\n        let val = threshold.value;\n        if (type === \"formula\") {\n            try {\n                // Relative references are not supported in formula\n                val = adaptFormulaToExcel(threshold.value);\n            }\n            catch (error) {\n                val = threshold.value;\n            }\n        }\n        attrs.push([\"val\", val]); // value is undefined only for type=\"value\")\n    }\n    return attrs;\n}\n/**\n * This function adapts our Threshold types to their Excel equivalents.\n *\n * if type === \"value\" ,then we must replace it by min or max according to the position\n * if type === \"number\", then it becomes num\n * if type === \"percentage\", it becomes \"percent\"\n * rest of the time, the type is unchanged\n */\nfunction getExcelThresholdType(type, position) {\n    switch (type) {\n        case \"value\":\n            return position === \"minimum\" ? \"min\" : \"max\";\n        case \"number\":\n            return \"num\";\n        case \"percentage\":\n            return \"percent\";\n        default:\n            return type;\n    }\n}\n\nfunction createDrawing(drawingRelIds, sheet, figures) {\n    const namespaces = [\n        [\"xmlns:xdr\", NAMESPACE.drawing],\n        [\"xmlns:r\", RELATIONSHIP_NSR],\n        [\"xmlns:a\", DRAWING_NS_A],\n        [\"xmlns:c\", DRAWING_NS_C],\n    ];\n    const figuresNodes = [];\n    for (const [figureIndex, figure] of Object.entries(figures)) {\n        switch (figure?.tag) {\n            case \"chart\":\n                figuresNodes.push(createChartDrawing(figure, sheet, drawingRelIds[figureIndex]));\n                break;\n            case \"image\":\n                figuresNodes.push(createImageDrawing(figure, sheet, drawingRelIds[figureIndex]));\n                break;\n        }\n    }\n    const xml = escapeXml /*xml*/ `\n    <xdr:wsDr ${formatAttributes(namespaces)}>\n      ${joinXmlNodes(figuresNodes)}\n    </xdr:wsDr>\n  `;\n    return parseXML(xml);\n}\n/**\n *  Returns the coordinates of topLeft (from) and BottomRight (to) of the chart in English Metric Units (EMU)\n */\nfunction convertFigureData(figure, sheet) {\n    const { x, y, height, width } = figure;\n    const cols = Object.values(sheet.cols);\n    const rows = Object.values(sheet.rows);\n    const { index: colFrom, offset: offsetColFrom } = figureCoordinates(cols, x);\n    const { index: colTo, offset: offsetColTo } = figureCoordinates(cols, x + width);\n    const { index: rowFrom, offset: offsetRowFrom } = figureCoordinates(rows, y);\n    const { index: rowTo, offset: offsetRowTo } = figureCoordinates(rows, y + height);\n    return {\n        from: {\n            col: colFrom,\n            colOff: offsetColFrom,\n            row: rowFrom,\n            rowOff: offsetRowFrom,\n        },\n        to: {\n            col: colTo,\n            colOff: offsetColTo,\n            row: rowTo,\n            rowOff: offsetRowTo,\n        },\n    };\n}\n/** Returns figure coordinates in EMU for a specific header dimension\n *  See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement\n */\nfunction figureCoordinates(headers, position) {\n    let currentPosition = 0;\n    for (const [headerIndex, header] of headers.entries()) {\n        if (currentPosition <= position && position < currentPosition + header.size) {\n            return {\n                index: headerIndex,\n                offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH),\n            };\n        }\n        else if (headerIndex < headers.length - 1) {\n            currentPosition += header.size;\n        }\n    }\n    return {\n        index: headers.length - 1,\n        offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH),\n    };\n}\nfunction createChartDrawing(figure, sheet, chartRelId) {\n    // position\n    const { from, to } = convertFigureData(figure, sheet);\n    const chartId = convertChartId(figure.id);\n    const cNvPrAttrs = [\n        [\"id\", chartId],\n        [\"name\", `Chart ${chartId}`],\n        [\"title\", \"Chart\"],\n    ];\n    return escapeXml /*xml*/ `\n    <xdr:twoCellAnchor>\n      <xdr:from>\n        <xdr:col>${from.col}</xdr:col>\n        <xdr:colOff>${from.colOff}</xdr:colOff>\n        <xdr:row>${from.row}</xdr:row>\n        <xdr:rowOff>${from.rowOff}</xdr:rowOff>\n      </xdr:from>\n      <xdr:to>\n        <xdr:col>${to.col}</xdr:col>\n        <xdr:colOff>${to.colOff}</xdr:colOff>\n        <xdr:row>${to.row}</xdr:row>\n        <xdr:rowOff>${to.rowOff}</xdr:rowOff>\n      </xdr:to>\n      <xdr:graphicFrame>\n        <xdr:nvGraphicFramePr>\n          <xdr:cNvPr ${formatAttributes(cNvPrAttrs)} />\n          <xdr:cNvGraphicFramePr />\n        </xdr:nvGraphicFramePr>\n        <xdr:xfrm>\n          <a:off x=\"0\" y=\"0\"/>\n          <a:ext cx=\"0\" cy=\"0\"/>\n        </xdr:xfrm>\n        <a:graphic>\n          <a:graphicData uri=\"${DRAWING_NS_C}\">\n            <c:chart r:id=\"${chartRelId}\" />\n          </a:graphicData>\n        </a:graphic>\n      </xdr:graphicFrame>\n      <xdr:clientData fLocksWithSheet=\"0\"/>\n    </xdr:twoCellAnchor>\n  `;\n}\nfunction createImageDrawing(figure, sheet, imageRelId) {\n    // position\n    const { from, to } = convertFigureData(figure, sheet);\n    const imageId = convertImageId(figure.id);\n    const cNvPrAttrs = [\n        [\"id\", imageId],\n        [\"name\", `Image ${imageId}`],\n        [\"title\", \"Image\"],\n    ];\n    const cx = convertDotValueToEMU(figure.width);\n    const cy = convertDotValueToEMU(figure.height);\n    return escapeXml /*xml*/ `\n    <xdr:twoCellAnchor editAs=\"oneCell\">\n      <xdr:from>\n        <xdr:col>${from.col}</xdr:col>\n        <xdr:colOff>${from.colOff}</xdr:colOff>\n        <xdr:row>${from.row}</xdr:row>\n        <xdr:rowOff>${from.rowOff}</xdr:rowOff>\n      </xdr:from>\n      <xdr:to>\n        <xdr:col>${to.col}</xdr:col>\n        <xdr:colOff>${to.colOff}</xdr:colOff>\n        <xdr:row>${to.row}</xdr:row>\n        <xdr:rowOff>${to.rowOff}</xdr:rowOff>\n      </xdr:to>\n      <xdr:pic>\n        <xdr:nvPicPr>\n          <xdr:cNvPr ${formatAttributes(cNvPrAttrs)}/>\n          <xdr:cNvPicPr preferRelativeResize=\"0\"/>\n        </xdr:nvPicPr>\n        <xdr:blipFill>\n          <a:blip cstate=\"print\" r:embed=\"${imageRelId}\"/>\n          <a:stretch>\n            <a:fillRect/>\n          </a:stretch>\n        </xdr:blipFill>\n        <xdr:spPr>\n          <a:xfrm>\n            <a:ext cx=\"${cx}\" cy=\"${cy}\" />\n          </a:xfrm>\n          <a:prstGeom prst=\"rect\">\n            <a:avLst/>\n          </a:prstGeom>\n          <a:noFill/>\n        </xdr:spPr>\n      </xdr:pic>\n      <xdr:clientData fLocksWithSheet=\"0\"/>\n    </xdr:twoCellAnchor>\n  `;\n}\n\nfunction addNumberFormats(numFmts) {\n    const numFmtNodes = [];\n    for (let [index, numFmt] of Object.entries(numFmts)) {\n        const numFmtAttrs = [\n            [\"numFmtId\", parseInt(index) + FIRST_NUMFMT_ID],\n            [\"formatCode\", numFmt.format],\n        ];\n        numFmtNodes.push(escapeXml /*xml*/ `\n      <numFmt ${formatAttributes(numFmtAttrs)}/>\n    `);\n    }\n    return escapeXml /*xml*/ `\n    <numFmts count=\"${numFmts.length}\">\n      ${joinXmlNodes(numFmtNodes)}\n    </numFmts>\n  `;\n}\nfunction addFont(font) {\n    if (isObjectEmptyRecursive(font)) {\n        return escapeXml /*xml*/ ``;\n    }\n    return escapeXml /*xml*/ `\n    <font>\n      ${font.bold ? escapeXml /*xml*/ `<b />` : \"\"}\n      ${font.italic ? escapeXml /*xml*/ `<i />` : \"\"}\n      ${font.underline ? escapeXml /*xml*/ `<u />` : \"\"}\n      ${font.strike ? escapeXml /*xml*/ `<strike />` : \"\"}\n      ${font.size ? escapeXml /*xml*/ `<sz val=\"${font.size}\" />` : \"\"}\n      ${font.color && font.color.rgb\n        ? escapeXml /*xml*/ `<color rgb=\"${toXlsxHexColor(font.color.rgb)}\" />`\n        : \"\"}\n      ${font.name ? escapeXml /*xml*/ `<name val=\"${font.name}\" />` : \"\"}\n    </font>\n  `;\n}\nfunction addFonts(fonts) {\n    return escapeXml /*xml*/ `\n    <fonts count=\"${fonts.length}\">\n      ${joinXmlNodes(Object.values(fonts).map(addFont))}\n    </fonts>\n  `;\n}\nfunction addFills(fills) {\n    const fillNodes = [];\n    for (let fill of Object.values(fills)) {\n        if (fill.reservedAttribute !== undefined) {\n            fillNodes.push(escapeXml /*xml*/ `\n        <fill>\n          <patternFill patternType=\"${fill.reservedAttribute}\" />\n        </fill>\n      `);\n        }\n        else {\n            fillNodes.push(escapeXml /*xml*/ `\n        <fill>\n          <patternFill patternType=\"solid\">\n            <fgColor rgb=\"${toXlsxHexColor(fill.fgColor.rgb)}\" />\n            <bgColor indexed=\"64\" />\n          </patternFill>\n        </fill>\n      `);\n        }\n    }\n    return escapeXml /*xml*/ `\n    <fills count=\"${fills.length}\">\n    ${joinXmlNodes(fillNodes)}\n    </fills>\n  `;\n}\nfunction addBorders(borders) {\n    const borderNodes = [];\n    for (let border of Object.values(borders)) {\n        borderNodes.push(escapeXml /*xml*/ `\n      <border>\n        <left ${formatBorderAttribute(border[\"left\"])}>\n          ${addBorderColor(border[\"left\"])}\n        </left>\n        <right ${formatBorderAttribute(border[\"right\"])}>\n          ${addBorderColor(border[\"right\"])}\n        </right>\n        <top ${formatBorderAttribute(border[\"top\"])}>\n          ${addBorderColor(border[\"top\"])}\n        </top>\n        <bottom ${formatBorderAttribute(border[\"bottom\"])}>\n          ${addBorderColor(border[\"bottom\"])}\n        </bottom>\n        <diagonal ${formatBorderAttribute(border[\"diagonal\"])}>\n          ${addBorderColor(border[\"diagonal\"])}\n        </diagonal>\n      </border>\n    `);\n    }\n    return escapeXml /*xml*/ `\n    <borders count=\"${borders.length}\">\n      ${joinXmlNodes(borderNodes)}\n    </borders>\n  `;\n}\nfunction formatBorderAttribute(description) {\n    if (!description) {\n        return escapeXml ``;\n    }\n    return formatAttributes([[\"style\", description.style]]);\n}\nfunction addBorderColor(description) {\n    if (!description) {\n        return escapeXml ``;\n    }\n    return escapeXml /*xml*/ `\n    <color ${formatAttributes([[\"rgb\", toXlsxHexColor(description.color.rgb)]])}/>\n  `;\n}\nfunction addStyles(styles) {\n    const styleNodes = [];\n    for (let style of styles) {\n        const attributes = [\n            [\"numFmtId\", style.numFmtId],\n            [\"fillId\", style.fillId],\n            [\"fontId\", style.fontId],\n            [\"borderId\", style.borderId],\n        ];\n        // Note: the apply${substyleName} does not seem to be required\n        const alignAttrs = [];\n        if (style.alignment && style.alignment.vertical) {\n            alignAttrs.push([\"vertical\", style.alignment.vertical]);\n        }\n        if (style.alignment && style.alignment.horizontal) {\n            alignAttrs.push([\"horizontal\", style.alignment.horizontal]);\n        }\n        if (style.alignment && style.alignment.wrapText) {\n            alignAttrs.push([\"wrapText\", \"1\"]);\n        }\n        if (alignAttrs.length > 0) {\n            attributes.push([\"applyAlignment\", \"1\"]); // for Libre Office\n            styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}>${escapeXml /*xml*/ `<alignment ${formatAttributes(alignAttrs)} />`}</xf> `);\n        }\n        else {\n            styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);\n        }\n    }\n    return escapeXml /*xml*/ `\n    <cellXfs count=\"${styles.length}\">\n      ${joinXmlNodes(styleNodes)}\n    </cellXfs>\n  `;\n}\n/**\n * DXFS : Differential Formatting Records - Conditional formats\n */\nfunction addCellWiseConditionalFormatting(dxfs // cell-wise CF\n) {\n    const dxfNodes = [];\n    for (const dxf of dxfs) {\n        let fontNode = escapeXml ``;\n        if (dxf.font) {\n            fontNode = addFont(dxf.font);\n        }\n        let fillNode = escapeXml ``;\n        if (dxf.fill) {\n            fillNode = escapeXml /*xml*/ `\n        <fill>\n          <patternFill>\n            <bgColor rgb=\"${toXlsxHexColor(dxf.fill.fgColor.rgb)}\" />\n          </patternFill>\n        </fill>\n      `;\n        }\n        dxfNodes.push(escapeXml /*xml*/ `\n      <dxf>\n        ${fontNode}\n        ${fillNode}\n      </dxf>\n    `);\n    }\n    return escapeXml /*xml*/ `\n    <dxfs count=\"${dxfs.length}\">\n      ${joinXmlNodes(dxfNodes)}\n    </dxfs>\n  `;\n}\n\nfunction createTable(table, tableId, sheetData) {\n    const tableAttributes = [\n        [\"id\", tableId],\n        [\"name\", `Table${tableId}`],\n        [\"displayName\", `Table${tableId}`],\n        [\"ref\", table.range],\n        [\"headerRowCount\", table.config.numberOfHeaders],\n        [\"totalsRowCount\", table.config.totalRow ? 1 : 0],\n        [\"xmlns\", NAMESPACE.table],\n        [\"xmlns:xr\", NAMESPACE.revision],\n        [\"xmlns:xr3\", NAMESPACE.revision3],\n        [\"xmlns:mc\", NAMESPACE.markupCompatibility],\n    ];\n    const xml = escapeXml /*xml*/ `\n    <table ${formatAttributes(tableAttributes)}>\n      ${table.config.hasFilters ? addAutoFilter(table) : \"\"}\n      ${addTableColumns(table, sheetData)}\n      ${addTableStyle(table)}\n    </table>\n    `;\n    return parseXML(xml);\n}\nfunction addAutoFilter(table) {\n    const autoFilterAttributes = [[\"ref\", table.range]];\n    return escapeXml /*xml*/ `\n  <autoFilter ${formatAttributes(autoFilterAttributes)}>\n    ${joinXmlNodes(addFilterColumns(table))}\n  </autoFilter>\n  `;\n}\nfunction addFilterColumns(table) {\n    const columns = [];\n    for (const filter of table.filters) {\n        const colXml = escapeXml /*xml*/ `\n      <filterColumn ${formatAttributes([[\"colId\", filter.colId]])}>\n        ${addFilter(filter)}\n      </filterColumn>\n      `;\n        columns.push(colXml);\n    }\n    return columns;\n}\nfunction addFilter(filter) {\n    const filterValues = filter.displayedValues.map((val) => escapeXml /*xml*/ `<filter ${formatAttributes([[\"val\", val]])}/>`);\n    const filterAttributes = filter.displayBlanks ? [[\"blank\", 1]] : [];\n    return escapeXml /*xml*/ `\n  <filters ${formatAttributes(filterAttributes)}>\n      ${joinXmlNodes(filterValues)}\n  </filters>\n`;\n}\nfunction addTableColumns(table, sheetData) {\n    const tableZone = toZone(table.range);\n    const columns = [];\n    for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {\n        const colHeaderXc = toXC(tableZone.left + i, tableZone.top);\n        const colName = sheetData.cells[colHeaderXc]?.content || `col${i}`;\n        const colAttributes = [\n            [\"id\", i + 1], // id cannot be 0\n            [\"name\", colName],\n        ];\n        if (table.config.totalRow) {\n            // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag\n            // `<totalsRowFormula>` for the formula of the total. But those doesn't seem to be mandatory for Excel.\n            const colTotalXc = toXC(tableZone.left + i, tableZone.bottom);\n            const colTotalContent = sheetData.cells[colTotalXc]?.content;\n            if (colTotalContent?.startsWith(\"=\")) {\n                colAttributes.push([\"totalsRowFunction\", \"custom\"]);\n            }\n        }\n        columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);\n    }\n    return escapeXml /*xml*/ `\n        <tableColumns ${formatAttributes([[\"count\", columns.length]])}>\n            ${joinXmlNodes(columns)}\n        </tableColumns>\n    `;\n}\nfunction addTableStyle(table) {\n    const tableStyleAttrs = [\n        [\"name\", table.config.styleId],\n        [\"showFirstColumn\", table.config.firstColumn ? 1 : 0],\n        [\"showLastColumn\", table.config.lastColumn ? 1 : 0],\n        [\"showRowStripes\", table.config.bandedRows ? 1 : 0],\n        [\"showColumnStripes\", table.config.bandedColumns ? 1 : 0],\n    ];\n    return escapeXml /*xml*/ `<tableStyleInfo ${formatAttributes(tableStyleAttrs)}/>`;\n}\n\nfunction addColumns(cols) {\n    if (!Object.values(cols).length) {\n        return escapeXml ``;\n    }\n    const colNodes = [];\n    for (let [id, col] of Object.entries(cols)) {\n        // Always force our own col width\n        const attributes = [\n            [\"min\", parseInt(id) + 1],\n            [\"max\", parseInt(id) + 1],\n            [\"width\", convertWidthToExcel(col.size || DEFAULT_CELL_WIDTH)],\n            [\"customWidth\", 1],\n            [\"hidden\", col.isHidden ? 1 : 0],\n        ];\n        if (col.outlineLevel) {\n            attributes.push([\"outlineLevel\", col.outlineLevel]);\n        }\n        if (col.collapsed) {\n            attributes.push([\"collapsed\", 1]);\n        }\n        colNodes.push(escapeXml /*xml*/ `\n      <col ${formatAttributes(attributes)}/>\n    `);\n    }\n    return escapeXml /*xml*/ `\n    <cols>\n      ${joinXmlNodes(colNodes)}\n    </cols>\n  `;\n}\nfunction addRows(construct, data, sheet) {\n    const rowNodes = [];\n    for (let r = 0; r < sheet.rowNumber; r++) {\n        const rowAttrs = [[\"r\", r + 1]];\n        const row = sheet.rows[r] || {};\n        if (row.size && row.size !== DEFAULT_CELL_HEIGHT) {\n            rowAttrs.push([\"ht\", convertHeightToExcel(row.size)], [\"customHeight\", 1]);\n        }\n        if (row.isHidden) {\n            rowAttrs.push([\"hidden\", 1]);\n        }\n        if (row.outlineLevel) {\n            rowAttrs.push([\"outlineLevel\", row.outlineLevel]);\n        }\n        if (row.collapsed) {\n            rowAttrs.push([\"collapsed\", 1]);\n        }\n        const cellNodes = [];\n        for (let c = 0; c < sheet.colNumber; c++) {\n            const xc = toXC(c, r);\n            const cell = sheet.cells[xc];\n            if (cell) {\n                const attributes = [[\"r\", xc]];\n                // style\n                const id = normalizeStyle(construct, extractStyle(cell, data));\n                // don't add style if default\n                if (id) {\n                    attributes.push([\"s\", id]);\n                }\n                let additionalAttrs = [];\n                let cellNode = escapeXml ``;\n                // Either formula or static value inside the cell\n                if (cell.isFormula) {\n                    const res = addFormula(cell);\n                    if (!res) {\n                        continue;\n                    }\n                    ({ attrs: additionalAttrs, node: cellNode } = res);\n                }\n                else if (cell.content && isMarkdownLink(cell.content)) {\n                    const { label } = parseMarkdownLink(cell.content);\n                    ({ attrs: additionalAttrs, node: cellNode } = addContent(label, construct.sharedStrings));\n                }\n                else if (cell.content && cell.content !== \"\") {\n                    const isTableHeader = isCellTableHeader(c, r, sheet);\n                    const isTableTotal = isCellTableTotal(c, r, sheet);\n                    const isPlainText = !!(cell.format && isTextFormat(data.formats[cell.format]));\n                    ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isTableTotal || isPlainText));\n                }\n                attributes.push(...additionalAttrs);\n                // prettier-ignore\n                cellNodes.push(escapeXml /*xml*/ `<c ${formatAttributes(attributes)}>\n  ${cellNode}\n</c>`);\n            }\n        }\n        if (cellNodes.length ||\n            row.size !== DEFAULT_CELL_HEIGHT ||\n            row.isHidden ||\n            row.outlineLevel ||\n            row.collapsed) {\n            rowNodes.push(escapeXml /*xml*/ `\n        <row ${formatAttributes(rowAttrs)}>\n          ${joinXmlNodes(cellNodes)}\n        </row>\n      `);\n        }\n    }\n    return escapeXml /*xml*/ `\n    <sheetData>\n      ${joinXmlNodes(rowNodes)}\n    </sheetData>\n  `;\n}\nfunction isCellTableHeader(col, row, sheet) {\n    return sheet.tables.some((table) => {\n        const zone = toZone(table.range);\n        const headerZone = { ...zone, bottom: zone.top };\n        return isInside(col, row, headerZone);\n    });\n}\nfunction isCellTableTotal(col, row, sheet) {\n    return sheet.tables.some((table) => {\n        if (!table.config.totalRow) {\n            return false;\n        }\n        const zone = toZone(table.range);\n        const totalZone = { ...zone, top: zone.bottom };\n        return isInside(col, row, totalZone);\n    });\n}\nfunction addHyperlinks(construct, data, sheetIndex) {\n    const sheet = data.sheets[sheetIndex];\n    const cells = sheet.cells;\n    const linkNodes = [];\n    for (const xc in cells) {\n        const content = cells[xc]?.content;\n        if (content && isMarkdownLink(content)) {\n            const { label, url } = parseMarkdownLink(content);\n            if (isSheetUrl(url)) {\n                const sheetId = parseSheetUrl(url);\n                const sheet = data.sheets.find((sheet) => sheet.id === sheetId);\n                const position = sheet ? `${sheet.name}!A1` : CellErrorType.InvalidReference;\n                const hyperlinkAttributes = [\n                    [\"display\", label],\n                    [\"location\", position],\n                    [\"ref\", xc],\n                ];\n                linkNodes.push(escapeXml /*xml*/ `\n          <hyperlink ${formatAttributes(hyperlinkAttributes)}/>\n        `);\n            }\n            else {\n                const linkRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {\n                    target: withHttps(url),\n                    type: XLSX_RELATION_TYPE.hyperlink,\n                    targetMode: \"External\",\n                });\n                const hyperlinkAttributes = [\n                    [\"r:id\", linkRelId],\n                    [\"ref\", xc],\n                ];\n                linkNodes.push(escapeXml /*xml*/ `\n          <hyperlink ${formatAttributes(hyperlinkAttributes)}/>\n        `);\n            }\n        }\n    }\n    if (!linkNodes.length) {\n        return escapeXml ``;\n    }\n    return escapeXml /*xml*/ `\n    <hyperlinks>\n      ${joinXmlNodes(linkNodes)}\n    </hyperlinks>\n  `;\n}\nfunction addMerges(merges) {\n    if (merges.length) {\n        const mergeNodes = merges.map((merge) => escapeXml /*xml*/ `<mergeCell ref=\"${merge}\" />`);\n        return escapeXml /*xml*/ `\n      <mergeCells count=\"${merges.length}\">\n        ${joinXmlNodes(mergeNodes)}\n      </mergeCells>\n    `;\n    }\n    else\n        return escapeXml ``;\n}\nfunction addSheetViews(sheet) {\n    const panes = sheet.panes;\n    let splitPanes = escapeXml /*xml*/ ``;\n    if (panes && (panes.xSplit || panes.ySplit)) {\n        const xc = toXC(panes.xSplit, panes.ySplit);\n        //workbookViewId should be defined in the workbook file but it seems like Excel has a default behaviour.\n        const xSplit = panes.xSplit ? escapeXml `xSplit=\"${panes.xSplit}\"` : \"\";\n        const ySplit = panes.ySplit ? escapeXml `ySplit=\"${panes.ySplit}\"` : \"\";\n        const topRight = panes.xSplit ? escapeXml `<selection pane=\"topRight\"/>` : \"\";\n        const bottomLeft = panes.ySplit ? escapeXml `<selection pane=\"bottomLeft\"/>` : \"\";\n        const bottomRight = panes.xSplit && panes.ySplit ? escapeXml `<selection pane=\"bottomRight\"/>` : \"\";\n        splitPanes = escapeXml /*xml*/ `\n    <pane\n      ${xSplit}\n      ${ySplit}\n      topLeftCell=\"${xc}\"\n      activePane=\"${panes.xSplit ? (panes.ySplit ? \"bottomRight\" : \"topRight\") : \"bottomLeft\"}\"\n      state=\"frozen\"/>\n      ${topRight}\n      ${bottomLeft}\n      ${bottomRight}\n    `;\n    }\n    const sheetViewAttrs = [\n        [\"showGridLines\", sheet.areGridLinesVisible ? 1 : 0],\n        [\"workbookViewId\", 0],\n    ];\n    let sheetView = escapeXml /*xml*/ `\n      <sheetViews>\n        <sheetView ${formatAttributes(sheetViewAttrs)}>\n          ${splitPanes}\n        </sheetView>\n      </sheetViews>\n    `;\n    return sheetView;\n}\nfunction addSheetProperties(sheet) {\n    if (!sheet.color) {\n        return \"\";\n    }\n    let sheetView = escapeXml /*xml*/ `\n      <sheetPr>\n        <tabColor ${formatAttributes([[\"rgb\", toXlsxHexColor(sheet.color)]])} />\n      </sheetPr>\n    `;\n    return sheetView;\n}\n\n/**\n * Return the spreadsheet data in the Office Open XML file format.\n * See ECMA-376 standard.\n * https://www.ecma-international.org/publications-and-standards/standards/ecma-376/\n */\nfunction getXLSX(data) {\n    data = fixLengthySheetNames(data);\n    data = purgeSingleRowTables(data);\n    const files = [];\n    const construct = getDefaultXLSXStructure(data);\n    files.push(createWorkbook(data, construct));\n    files.push(...createWorksheets(data, construct));\n    files.push(createStylesSheet(construct));\n    files.push(createSharedStrings(construct.sharedStrings));\n    files.push(...createRelsFiles(construct.relsFiles));\n    files.push(createContentTypes(files));\n    files.push(createRelRoot());\n    return {\n        name: `my_spreadsheet.xlsx`,\n        files,\n    };\n}\nfunction createWorkbook(data, construct) {\n    const namespaces = [\n        [\"xmlns\", NAMESPACE[\"workbook\"]],\n        [\"xmlns:r\", RELATIONSHIP_NSR],\n    ];\n    const sheetNodes = [];\n    for (const [index, sheet] of Object.entries(data.sheets)) {\n        const attributes = [\n            [\"state\", sheet.isVisible ? \"visible\" : \"hidden\"],\n            [\"name\", sheet.name],\n            [\"sheetId\", parseInt(index) + 1],\n            [\"r:id\", `rId${parseInt(index) + 1}`],\n        ];\n        sheetNodes.push(escapeXml /*xml*/ `\n      <sheet ${formatAttributes(attributes)} />\n    `);\n        addRelsToFile(construct.relsFiles, \"xl/_rels/workbook.xml.rels\", {\n            type: XLSX_RELATION_TYPE.sheet,\n            target: `worksheets/sheet${index}.xml`,\n        });\n    }\n    const xml = escapeXml /*xml*/ `\n    <workbook ${formatAttributes(namespaces)}>\n      <sheets>\n        ${joinXmlNodes(sheetNodes)}\n      </sheets>\n    </workbook>\n  `;\n    return createXMLFile(parseXML(xml), \"xl/workbook.xml\", \"workbook\");\n}\nfunction createWorksheets(data, construct) {\n    const files = [];\n    let currentTableIndex = 1;\n    for (const [sheetIndex, sheet] of Object.entries(data.sheets)) {\n        const namespaces = [\n            [\"xmlns\", NAMESPACE[\"worksheet\"]],\n            [\"xmlns:r\", RELATIONSHIP_NSR],\n        ];\n        const sheetFormatAttributes = [\n            [\"defaultRowHeight\", convertHeightToExcel(DEFAULT_CELL_HEIGHT)],\n            [\"defaultColWidth\", convertWidthToExcel(DEFAULT_CELL_WIDTH)],\n        ];\n        const tablesNode = createTablesForSheet(sheet, sheetIndex, currentTableIndex, construct, files);\n        currentTableIndex += sheet.tables.length;\n        // Figures and Charts\n        let drawingNode = escapeXml ``;\n        const drawingRelIds = [];\n        for (const chart of sheet.charts) {\n            const xlsxChartId = convertChartId(chart.id);\n            const chartRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {\n                target: `../charts/chart${xlsxChartId}.xml`,\n                type: XLSX_RELATION_TYPE.chart,\n            });\n            drawingRelIds.push(chartRelId);\n            files.push(createXMLFile(createChart(chart, sheetIndex, data), `xl/charts/chart${xlsxChartId}.xml`, \"chart\"));\n        }\n        for (const image of sheet.images) {\n            const mimeType = image.data.mimetype;\n            if (mimeType === undefined)\n                continue;\n            const extension = IMAGE_MIMETYPE_TO_EXTENSION_MAPPING[mimeType];\n            // only support exporting images with mimetypes specified in the mapping\n            if (extension === undefined)\n                continue;\n            const xlsxImageId = convertImageId(image.id);\n            let imageFileName = `image${xlsxImageId}.${extension}`;\n            const imageRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {\n                target: `../media/${imageFileName}`,\n                type: XLSX_RELATION_TYPE.image,\n            });\n            drawingRelIds.push(imageRelId);\n            files.push({\n                path: `xl/media/${imageFileName}`,\n                imageSrc: image.data.path,\n            });\n        }\n        const drawings = [...sheet.charts, ...sheet.images];\n        if (drawings.length) {\n            const drawingRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {\n                target: `../drawings/drawing${sheetIndex}.xml`,\n                type: XLSX_RELATION_TYPE.drawing,\n            });\n            files.push(createXMLFile(createDrawing(drawingRelIds, sheet, drawings), `xl/drawings/drawing${sheetIndex}.xml`, \"drawing\"));\n            drawingNode = escapeXml /*xml*/ `<drawing r:id=\"${drawingRelId}\" />`;\n        }\n        const sheetXml = escapeXml /*xml*/ `\n      <worksheet ${formatAttributes(namespaces)}>\n        ${addSheetProperties(sheet)}\n        ${addSheetViews(sheet)}\n        <sheetFormatPr ${formatAttributes(sheetFormatAttributes)} />\n        ${addColumns(sheet.cols)}\n        ${addRows(construct, data, sheet)}\n        ${addMerges(sheet.merges)}\n        ${joinXmlNodes(addConditionalFormatting(construct.dxfs, sheet.conditionalFormats))}\n        ${addHyperlinks(construct, data, sheetIndex)}\n        ${drawingNode}\n        ${tablesNode}\n      </worksheet>\n    `;\n        files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, \"sheet\"));\n    }\n    addRelsToFile(construct.relsFiles, \"xl/_rels/workbook.xml.rels\", {\n        type: XLSX_RELATION_TYPE.sharedStrings,\n        target: \"sharedStrings.xml\",\n    });\n    addRelsToFile(construct.relsFiles, \"xl/_rels/workbook.xml.rels\", {\n        type: XLSX_RELATION_TYPE.styles,\n        target: \"styles.xml\",\n    });\n    return files;\n}\n/**\n * Create xlsx files for each tables contained in the given sheet, and add them to the XLSXStructure ans XLSXExportFiles.\n *\n * Return an XML string that should be added in the sheet to link these table to the sheet.\n */\nfunction createTablesForSheet(sheetData, sheetId, startingTableId, construct, files) {\n    let currentTableId = startingTableId;\n    if (!sheetData.tables.length)\n        return new XMLString(\"\");\n    const sheetRelFile = `xl/worksheets/_rels/sheet${sheetId}.xml.rels`;\n    const tableParts = [];\n    for (const table of sheetData.tables) {\n        const tableRelId = addRelsToFile(construct.relsFiles, sheetRelFile, {\n            target: `../tables/table${currentTableId}.xml`,\n            type: XLSX_RELATION_TYPE.table,\n        });\n        files.push(createXMLFile(createTable(table, currentTableId, sheetData), `xl/tables/table${currentTableId}.xml`, \"table\"));\n        tableParts.push(escapeXml /*xml*/ `<tablePart r:id=\"${tableRelId}\" />`);\n        currentTableId++;\n    }\n    return escapeXml /*xml*/ `\n    <tableParts count=\"${sheetData.tables.length}\">\n      ${joinXmlNodes(tableParts)}\n    </tableParts>\n`;\n}\nfunction createStylesSheet(construct) {\n    const namespaces = [\n        [\"xmlns\", NAMESPACE[\"styleSheet\"]],\n        [\"xmlns:r\", RELATIONSHIP_NSR],\n    ];\n    const styleXml = escapeXml /*xml*/ `\n    <styleSheet ${formatAttributes(namespaces)}>\n      ${addNumberFormats(construct.numFmts)}\n      ${addFonts(construct.fonts)}\n      ${addFills(construct.fills)}\n      ${addBorders(construct.borders)}\n      ${addStyles(construct.styles)}\n      ${addCellWiseConditionalFormatting(construct.dxfs)}\n    </styleSheet>\n  `;\n    return createXMLFile(parseXML(styleXml), \"xl/styles.xml\", \"styles\");\n}\nfunction createSharedStrings(strings) {\n    const namespaces = [\n        [\"xmlns\", NAMESPACE[\"sst\"]],\n        [\"count\", strings.length],\n        [\"uniqueCount\", strings.length],\n    ];\n    const stringNodes = strings.map((string) => escapeXml /*xml*/ `<si><t>${string}</t></si>`);\n    const xml = escapeXml /*xml*/ `\n    <sst ${formatAttributes(namespaces)}>\n      ${joinXmlNodes(stringNodes)}\n    </sst>\n  `;\n    return createXMLFile(parseXML(xml), \"xl/sharedStrings.xml\", \"sharedStrings\");\n}\nfunction createRelsFiles(relsFiles) {\n    const XMLRelsFiles = [];\n    for (const relFile of relsFiles) {\n        const relationNodes = [];\n        for (const rel of relFile.rels) {\n            const attributes = [\n                [\"Id\", rel.id],\n                [\"Target\", rel.target],\n                [\"Type\", rel.type],\n            ];\n            if (rel.targetMode) {\n                attributes.push([\"TargetMode\", rel.targetMode]);\n            }\n            relationNodes.push(escapeXml /*xml*/ `\n        <Relationship ${formatAttributes(attributes)} />\n      `);\n        }\n        const xml = escapeXml /*xml*/ `\n      <Relationships xmlns=\"${NAMESPACE[\"Relationships\"]}\">\n        ${joinXmlNodes(relationNodes)}\n      </Relationships>\n    `;\n        XMLRelsFiles.push(createXMLFile(parseXML(xml), relFile.path));\n    }\n    return XMLRelsFiles;\n}\nfunction createContentTypes(files) {\n    const overrideNodes = [];\n    // hard-code supported image mimetypes\n    const imageDefaultNodes = Object.entries(IMAGE_MIMETYPE_TO_EXTENSION_MAPPING).map(([mimetype, extension]) => createDefaultXMLElement(extension, mimetype));\n    for (const file of files) {\n        if (\"contentType\" in file && file.contentType) {\n            overrideNodes.push(createOverride(\"/\" + file.path, CONTENT_TYPES[file.contentType]));\n        }\n    }\n    const relsAttributes = [\n        [\"Extension\", \"rels\"],\n        [\"ContentType\", \"application/vnd.openxmlformats-package.relationships+xml\"],\n    ];\n    const xmlAttributes = [\n        [\"Extension\", \"xml\"],\n        [\"ContentType\", \"application/xml\"],\n    ];\n    const xml = escapeXml /*xml*/ `\n    <Types xmlns=\"${NAMESPACE[\"Types\"]}\">\n      ${joinXmlNodes(Object.values(imageDefaultNodes))}\n      <Default ${formatAttributes(relsAttributes)} />\n      <Default ${formatAttributes(xmlAttributes)} />\n      ${joinXmlNodes(overrideNodes)}\n    </Types>\n  `;\n    return createXMLFile(parseXML(xml), \"[Content_Types].xml\");\n}\nfunction createRelRoot() {\n    const attributes = [\n        [\"Id\", \"rId1\"],\n        [\"Type\", XLSX_RELATION_TYPE.document],\n        [\"Target\", \"xl/workbook.xml\"],\n    ];\n    const xml = escapeXml /*xml*/ `\n    <Relationships xmlns=\"${NAMESPACE[\"Relationships\"]}\">\n      <Relationship ${formatAttributes(attributes)} />\n    </Relationships>\n  `;\n    return createXMLFile(parseXML(xml), \"_rels/.rels\");\n}\n/**\n * Excel sheet names are maximum 31 characters while o-spreadsheet do not have this limit.\n * This method converts the sheet names to be within the 31 characters limit.\n * The cells/charts referencing this sheet will be updated accordingly.\n */\nfunction fixLengthySheetNames(data) {\n    const nameMapping = {};\n    const newNames = new Set();\n    for (const sheet of data.sheets) {\n        let newName = sheet.name.slice(0, 31);\n        let i = 1;\n        while (newNames.has(newName)) {\n            newName = newName.slice(0, 31 - String(i).length) + i++;\n        }\n        newNames.add(newName);\n        if (newName !== sheet.name) {\n            nameMapping[sheet.name] = newName;\n            sheet.name = newName;\n        }\n    }\n    if (!Object.keys(nameMapping).length) {\n        return data;\n    }\n    const sheetWithNewNames = Object.keys(nameMapping).sort((a, b) => b.length - a.length);\n    let stringifiedData = JSON.stringify(data);\n    for (const sheetName of sheetWithNewNames) {\n        const regex = new RegExp(`'?${escapeRegExp(sheetName)}'?!`, \"g\");\n        stringifiedData = stringifiedData.replaceAll(regex, (match) => {\n            const newName = nameMapping[sheetName];\n            return match.replace(sheetName, newName);\n        });\n    }\n    return JSON.parse(stringifiedData);\n}\n/** Excel files do not support tables with a single row the defined range\n * Since those tables are not really useful (no filtering/limited styling)\n * This function filters out all tables with a single row.\n */\nfunction purgeSingleRowTables(data) {\n    for (const sheet of data.sheets) {\n        sheet.tables = sheet.tables.filter((table) => zoneToDimension(toZone(table.range)).numberOfRows > 1);\n    }\n    return data;\n}\n\nvar Status;\n(function (Status) {\n    Status[Status[\"Ready\"] = 0] = \"Ready\";\n    Status[Status[\"Running\"] = 1] = \"Running\";\n    Status[Status[\"RunningCore\"] = 2] = \"RunningCore\";\n    Status[Status[\"Finalizing\"] = 3] = \"Finalizing\";\n})(Status || (Status = {}));\nclass Model extends EventBus {\n    corePlugins = [];\n    featurePlugins = [];\n    statefulUIPlugins = [];\n    coreViewsPlugins = [];\n    range;\n    session;\n    /**\n     * In a collaborative context, some commands can be replayed, we have to ensure\n     * that these commands are not replayed on the UI plugins.\n     */\n    isReplayingCommand = false;\n    /**\n     * A plugin can draw some contents on the canvas. But even better: it can do\n     * so multiple times.  The order of the render calls will determine a list of\n     * \"layers\" (i.e., earlier calls will be obviously drawn below later calls).\n     * This list simply keeps the renderers+layer information so the drawing code\n     * can just iterate on it\n     */\n    renderers = {};\n    /**\n     * Internal status of the model. Important for command handling coordination\n     */\n    status = 0 /* Status.Ready */;\n    /**\n     * The config object contains some configuration flag and callbacks\n     */\n    config;\n    corePluginConfig;\n    uiPluginConfig;\n    state;\n    selection;\n    /**\n     * Getters are the main way the rest of the UI read data from the model. Also,\n     * it is shared between all plugins, so they can also communicate with each\n     * other.\n     */\n    getters;\n    /**\n     * Getters that are accessible from the core plugins. It's a subset of `getters`,\n     * without the UI getters\n     */\n    coreGetters;\n    uuidGenerator;\n    handlers = [];\n    uiHandlers = [];\n    coreHandlers = [];\n    constructor(data = {}, config = {}, stateUpdateMessages = [], uuidGenerator = new UuidGenerator(), verboseImport = false) {\n        const start = performance.now();\n        console.debug(\"##### Model creation #####\");\n        super();\n        setDefaultTranslationMethod();\n        stateUpdateMessages = repairInitialMessages(data, stateUpdateMessages);\n        const workbookData = load(data, verboseImport);\n        this.state = new StateObserver();\n        this.uuidGenerator = uuidGenerator;\n        this.config = this.setupConfig(config);\n        this.session = this.setupSession(workbookData.revisionId);\n        this.coreGetters = {};\n        this.range = new RangeAdapter(this.coreGetters);\n        this.coreGetters.getRangeString = this.range.getRangeString.bind(this.range);\n        this.coreGetters.getRangeFromSheetXC = this.range.getRangeFromSheetXC.bind(this.range);\n        this.coreGetters.createAdaptedRanges = this.range.createAdaptedRanges.bind(this.range);\n        this.coreGetters.getRangeDataFromXc = this.range.getRangeDataFromXc.bind(this.range);\n        this.coreGetters.getRangeDataFromZone = this.range.getRangeDataFromZone.bind(this.range);\n        this.coreGetters.getRangeFromRangeData = this.range.getRangeFromRangeData.bind(this.range);\n        this.coreGetters.getRangeFromZone = this.range.getRangeFromZone.bind(this.range);\n        this.coreGetters.recomputeRanges = this.range.recomputeRanges.bind(this.range);\n        this.coreGetters.isRangeValid = this.range.isRangeValid.bind(this.range);\n        this.coreGetters.extendRange = this.range.extendRange.bind(this.range);\n        this.coreGetters.getRangesUnion = this.range.getRangesUnion.bind(this.range);\n        this.coreGetters.removeRangesSheetPrefix = this.range.removeRangesSheetPrefix.bind(this.range);\n        this.getters = {\n            isReadonly: () => this.config.mode === \"readonly\" || this.config.mode === \"dashboard\",\n            isDashboard: () => this.config.mode === \"dashboard\",\n        };\n        // Initiate stream processor\n        this.selection = new SelectionStreamProcessorImpl(this.getters);\n        this.coreHandlers.push(this.range);\n        this.handlers.push(this.range);\n        this.corePluginConfig = this.setupCorePluginConfig();\n        this.uiPluginConfig = this.setupUiPluginConfig();\n        // registering plugins\n        for (let Plugin of corePluginRegistry.getAll()) {\n            this.setupCorePlugin(Plugin, workbookData);\n        }\n        Object.assign(this.getters, this.coreGetters);\n        this.session.loadInitialMessages(stateUpdateMessages);\n        for (let Plugin of coreViewsPluginRegistry.getAll()) {\n            const plugin = this.setupUiPlugin(Plugin);\n            this.coreViewsPlugins.push(plugin);\n            this.handlers.push(plugin);\n            this.uiHandlers.push(plugin);\n            this.coreHandlers.push(plugin);\n        }\n        for (let Plugin of statefulUIPluginRegistry.getAll()) {\n            const plugin = this.setupUiPlugin(Plugin);\n            this.statefulUIPlugins.push(plugin);\n            this.handlers.push(plugin);\n            this.uiHandlers.push(plugin);\n        }\n        for (let Plugin of featurePluginRegistry.getAll()) {\n            const plugin = this.setupUiPlugin(Plugin);\n            this.featurePlugins.push(plugin);\n            this.handlers.push(plugin);\n            this.uiHandlers.push(plugin);\n        }\n        this.uuidGenerator.setIsFastStrategy(false);\n        // starting plugins\n        this.dispatch(\"START\");\n        // Model should be the last permanent subscriber in the list since he should render\n        // after all changes have been applied to the other subscribers (plugins)\n        this.selection.observe(this, {\n            handleEvent: () => this.trigger(\"update\"),\n        });\n        // This should be done after construction of LocalHistory due to order of\n        // events\n        this.setupSessionEvents();\n        this.joinSession();\n        if (config.snapshotRequested) {\n            const startSnapshot = performance.now();\n            console.debug(\"Snapshot requested\");\n            this.session.snapshot(this.exportData());\n            this.garbageCollectExternalResources();\n            console.debug(\"Snapshot taken in\", performance.now() - startSnapshot, \"ms\");\n        }\n        // mark all models as \"raw\", so they will not be turned into reactive objects\n        // by owl, since we do not rely on reactivity\n        markRaw(this);\n        console.debug(\"Model created in\", performance.now() - start, \"ms\");\n        console.debug(\"######\");\n    }\n    joinSession() {\n        this.session.join(this.config.client);\n    }\n    async leaveSession() {\n        const snapshot = this.getters.isReadonly() ? undefined : lazy(() => this.exportData());\n        await this.session.leave(snapshot);\n    }\n    setupUiPlugin(Plugin) {\n        const plugin = new Plugin(this.uiPluginConfig);\n        for (let name of Plugin.getters) {\n            if (!(name in plugin)) {\n                throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);\n            }\n            if (name in this.getters) {\n                throw new Error(`Getter \"${name}\" is already defined.`);\n            }\n            this.getters[name] = plugin[name].bind(plugin);\n        }\n        for (const layer of Plugin.layers) {\n            if (!this.renderers[layer]) {\n                this.renderers[layer] = [];\n            }\n            this.renderers[layer].push(plugin);\n        }\n        return plugin;\n    }\n    /**\n     * Initialize and properly configure a plugin.\n     *\n     * This method is private for now, but if the need arise, there is no deep\n     * reason why the model could not add dynamically a plugin while it is running.\n     */\n    setupCorePlugin(Plugin, data) {\n        const plugin = new Plugin(this.corePluginConfig);\n        for (let name of Plugin.getters) {\n            if (!(name in plugin)) {\n                throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);\n            }\n            if (name in this.coreGetters) {\n                throw new Error(`Getter \"${name}\" is already defined.`);\n            }\n            this.coreGetters[name] = plugin[name].bind(plugin);\n        }\n        plugin.import(data);\n        this.corePlugins.push(plugin);\n        this.coreHandlers.push(plugin);\n        this.handlers.push(plugin);\n    }\n    onRemoteRevisionReceived({ commands }) {\n        for (let command of commands) {\n            const previousStatus = this.status;\n            this.status = 2 /* Status.RunningCore */;\n            this.dispatchToHandlers(this.statefulUIPlugins, command);\n            this.status = previousStatus;\n        }\n        this.finalize();\n    }\n    setupSession(revisionId) {\n        const session = new Session(buildRevisionLog({\n            initialRevisionId: revisionId,\n            recordChanges: this.state.recordChanges.bind(this.state),\n            dispatch: (command) => {\n                const result = this.checkDispatchAllowed(command);\n                if (!result.isSuccessful) {\n                    return;\n                }\n                this.isReplayingCommand = true;\n                this.dispatchToHandlers(this.coreHandlers, command);\n                this.isReplayingCommand = false;\n            },\n        }), this.config.transportService, revisionId);\n        return session;\n    }\n    setupSessionEvents() {\n        this.session.on(\"remote-revision-received\", this, this.onRemoteRevisionReceived);\n        this.session.on(\"revision-undone\", this, ({ commands }) => {\n            this.dispatchFromCorePlugin(\"UNDO\", { commands });\n            this.finalize();\n        });\n        this.session.on(\"revision-redone\", this, ({ commands }) => {\n            this.dispatchFromCorePlugin(\"REDO\", { commands });\n            this.finalize();\n        });\n        // How could we improve communication between the session and UI?\n        // It feels weird to have the model piping specific session events to its own bus.\n        this.session.on(\"unexpected-revision-id\", this, () => this.trigger(\"unexpected-revision-id\"));\n        this.session.on(\"collaborative-event-received\", this, () => {\n            this.trigger(\"update\");\n        });\n    }\n    setupConfig(config) {\n        const client = config.client || {\n            id: this.uuidGenerator.uuidv4(),\n            name: _t(\"Anonymous\").toString(),\n        };\n        const transportService = config.transportService || new LocalTransportService();\n        return {\n            ...config,\n            mode: config.mode || \"normal\",\n            custom: config.custom || {},\n            external: this.setupExternalConfig(config.external || {}),\n            transportService,\n            client,\n            moveClient: () => { },\n            snapshotRequested: false,\n            notifyUI: (payload) => this.trigger(\"notify-ui\", payload),\n            raiseBlockingErrorUI: (text) => this.trigger(\"raise-error-ui\", { text }),\n            customColors: config.customColors || [],\n        };\n    }\n    setupExternalConfig(external) {\n        const loadLocales = external.loadLocales || (() => Promise.resolve(DEFAULT_LOCALES));\n        return {\n            ...external,\n            loadLocales,\n        };\n    }\n    setupCorePluginConfig() {\n        return {\n            getters: this.coreGetters,\n            stateObserver: this.state,\n            range: this.range,\n            dispatch: this.dispatchFromCorePlugin,\n            canDispatch: this.canDispatch,\n            uuidGenerator: this.uuidGenerator,\n            custom: this.config.custom,\n            external: this.config.external,\n        };\n    }\n    setupUiPluginConfig() {\n        return {\n            getters: this.getters,\n            stateObserver: this.state,\n            dispatch: this.dispatch,\n            canDispatch: this.canDispatch,\n            selection: this.selection,\n            moveClient: this.session.move.bind(this.session),\n            custom: this.config.custom,\n            uiActions: this.config,\n            session: this.session,\n            defaultCurrency: this.config.defaultCurrency,\n            customColors: this.config.customColors || [],\n        };\n    }\n    // ---------------------------------------------------------------------------\n    // Command Handling\n    // ---------------------------------------------------------------------------\n    /**\n     * Check if the given command is allowed by all the plugins and the history.\n     */\n    checkDispatchAllowed(command) {\n        const results = isCoreCommand(command)\n            ? this.checkDispatchAllowedCoreCommand(command)\n            : this.checkDispatchAllowedLocalCommand(command);\n        if (results.some((r) => r !== \"Success\" /* CommandResult.Success */)) {\n            return new DispatchResult(results.flat());\n        }\n        return DispatchResult.Success;\n    }\n    checkDispatchAllowedCoreCommand(command) {\n        const results = this.corePlugins.map((handler) => handler.allowDispatch(command));\n        results.push(this.range.allowDispatch(command));\n        return results;\n    }\n    checkDispatchAllowedLocalCommand(command) {\n        const results = this.uiHandlers.map((handler) => handler.allowDispatch(command));\n        return results;\n    }\n    finalize() {\n        this.status = 3 /* Status.Finalizing */;\n        for (const h of this.handlers) {\n            h.finalize();\n        }\n        this.status = 0 /* Status.Ready */;\n        this.trigger(\"command-finalized\");\n    }\n    /**\n     * Check if a command can be dispatched, and returns a DispatchResult object with the possible\n     * reasons the dispatch failed.\n     */\n    canDispatch = (type, payload) => {\n        return this.checkDispatchAllowed(createCommand(type, payload));\n    };\n    /**\n     * The dispatch method is the only entry point to manipulate data in the model.\n     * This is through this method that commands are dispatched most of the time\n     * recursively until no plugin want to react anymore.\n     *\n     * CoreCommands dispatched from this function are saved in the history.\n     *\n     * Small technical detail: it is defined as an arrow function.  There are two\n     * reasons for this:\n     * 1. this means that the dispatch method can be \"detached\" from the model,\n     *    which is done when it is put in the environment (see the Spreadsheet\n     *    component)\n     * 2. This allows us to define its type by using the interface CommandDispatcher\n     */\n    dispatch = (type, payload) => {\n        const command = createCommand(type, payload);\n        let status = this.status;\n        if (this.getters.isReadonly() && !canExecuteInReadonly(command)) {\n            return new DispatchResult(\"Readonly\" /* CommandResult.Readonly */);\n        }\n        if (!this.session.canApplyOptimisticUpdate()) {\n            return new DispatchResult(\"WaitingSessionConfirmation\" /* CommandResult.WaitingSessionConfirmation */);\n        }\n        switch (status) {\n            case 0 /* Status.Ready */:\n                const result = this.checkDispatchAllowed(command);\n                if (!result.isSuccessful) {\n                    this.trigger(\"update\");\n                    return result;\n                }\n                this.status = 1 /* Status.Running */;\n                const { changes, commands } = this.state.recordChanges(() => {\n                    const start = performance.now();\n                    if (isCoreCommand(command)) {\n                        this.state.addCommand(command);\n                    }\n                    this.dispatchToHandlers(this.handlers, command);\n                    this.finalize();\n                    const time = performance.now() - start;\n                    if (time > 5) {\n                        console.debug(type, time, \"ms\");\n                    }\n                });\n                this.session.save(command, commands, changes);\n                this.status = 0 /* Status.Ready */;\n                this.trigger(\"update\");\n                break;\n            case 1 /* Status.Running */:\n                if (isCoreCommand(command)) {\n                    const dispatchResult = this.checkDispatchAllowed(command);\n                    if (!dispatchResult.isSuccessful) {\n                        return dispatchResult;\n                    }\n                    this.state.addCommand(command);\n                }\n                this.dispatchToHandlers(this.handlers, command);\n                break;\n            case 3 /* Status.Finalizing */:\n                throw new Error(\"Cannot dispatch commands in the finalize state\");\n            case 2 /* Status.RunningCore */:\n                if (isCoreCommand(command)) {\n                    throw new Error(`A UI plugin cannot dispatch ${type} while handling a core command`);\n                }\n                this.dispatchToHandlers(this.handlers, command);\n        }\n        return DispatchResult.Success;\n    };\n    /**\n     * Dispatch a command from a Core Plugin (or the History).\n     * A command dispatched from this function is not added to the history.\n     */\n    dispatchFromCorePlugin = (type, payload) => {\n        const command = createCommand(type, payload);\n        const previousStatus = this.status;\n        this.status = 2 /* Status.RunningCore */;\n        const handlers = this.isReplayingCommand ? this.coreHandlers : this.handlers;\n        this.dispatchToHandlers(handlers, command);\n        this.status = previousStatus;\n        return DispatchResult.Success;\n    };\n    /**\n     * Dispatch the given command to the given handlers.\n     * It will call `beforeHandle` and `handle`\n     */\n    dispatchToHandlers(handlers, command) {\n        const isCommandCore = isCoreCommand(command);\n        for (const handler of handlers) {\n            if (!isCommandCore && handler instanceof CorePlugin) {\n                continue;\n            }\n            handler.beforeHandle(command);\n        }\n        for (const handler of handlers) {\n            if (!isCommandCore && handler instanceof CorePlugin) {\n                continue;\n            }\n            handler.handle(command);\n        }\n        this.trigger(\"command-dispatched\", command);\n    }\n    // ---------------------------------------------------------------------------\n    // Grid Rendering\n    // ---------------------------------------------------------------------------\n    /**\n     * When the Grid component is ready (= mounted), it has a reference to its\n     * canvas and need to draw the grid on it.  This is then done by calling this\n     * method, which will dispatch the call to all registered plugins.\n     *\n     * Note that nothing prevent multiple grid components from calling this method\n     * each, or one grid component calling it multiple times with a different\n     * context. This is probably the way we should do if we want to be able to\n     * freeze a part of the grid (so, we would need to render different zones)\n     */\n    drawLayer(context, layer) {\n        const renderers = this.renderers[layer];\n        if (!renderers) {\n            return;\n        }\n        for (const renderer of renderers) {\n            context.ctx.save();\n            renderer.drawLayer(context, layer);\n            context.ctx.restore();\n        }\n    }\n    // ---------------------------------------------------------------------------\n    // Data Export\n    // ---------------------------------------------------------------------------\n    /**\n     * As the name of this method strongly implies, it is useful when we need to\n     * export date out of the model.\n     */\n    exportData() {\n        let data = createEmptyWorkbookData();\n        for (let handler of this.handlers) {\n            if (handler instanceof CorePlugin) {\n                handler.export(data);\n            }\n        }\n        data.revisionId = this.session.getRevisionId() || DEFAULT_REVISION_ID;\n        data = deepCopy(data);\n        return data;\n    }\n    updateMode(mode) {\n        // @ts-ignore For testing purposes only\n        this.config.mode = mode;\n        this.trigger(\"update\");\n    }\n    /**\n     * Exports the current model data into a list of serialized XML files\n     * to be zipped together as an *.xlsx file.\n     *\n     * We need to trigger a cell revaluation  on every sheet and ensure that even\n     * async functions are evaluated.\n     * This prove to be necessary if the client did not trigger that evaluation in the first place\n     * (e.g. open a document with several sheet and click on download before visiting each sheet)\n     */\n    exportXLSX() {\n        this.dispatch(\"EVALUATE_CELLS\");\n        let data = createEmptyExcelWorkbookData();\n        for (let handler of this.handlers) {\n            if (handler instanceof BasePlugin) {\n                handler.exportForExcel(data);\n            }\n        }\n        data = deepCopy(data);\n        return getXLSX(data);\n    }\n    garbageCollectExternalResources() {\n        for (const plugin of this.corePlugins) {\n            plugin.garbageCollectExternalResources();\n        }\n    }\n}\nfunction createCommand(type, payload = {}) {\n    const command = deepCopy(payload);\n    command.type = type;\n    return command;\n}\n\n/**\n * We export here all entities that needs to be accessed publicly by Odoo.\n *\n * Note that the __info__ key is actually completed by the build process (see\n * the rollup.config.js file)\n */\nconst __info__ = {};\nconst SPREADSHEET_DIMENSIONS = {\n    MIN_ROW_HEIGHT,\n    MIN_COL_WIDTH,\n    HEADER_HEIGHT,\n    HEADER_WIDTH,\n    TOPBAR_HEIGHT,\n    BOTTOMBAR_HEIGHT,\n    DEFAULT_CELL_WIDTH,\n    DEFAULT_CELL_HEIGHT,\n    SCROLLBAR_WIDTH,\n};\nconst registries = {\n    autoCompleteProviders,\n    autofillModifiersRegistry,\n    autofillRulesRegistry,\n    cellMenuRegistry,\n    colMenuRegistry,\n    errorTypes,\n    linkMenuRegistry,\n    functionRegistry,\n    featurePluginRegistry,\n    iconsOnCellRegistry,\n    statefulUIPluginRegistry,\n    coreViewsPluginRegistry,\n    corePluginRegistry,\n    rowMenuRegistry,\n    sidePanelRegistry,\n    figureRegistry,\n    chartSidePanelComponentRegistry,\n    chartComponentRegistry,\n    chartRegistry,\n    chartSubtypeRegistry,\n    topbarMenuRegistry,\n    topbarComponentRegistry,\n    clickableCellRegistry,\n    otRegistry,\n    inverseCommandRegistry,\n    urlRegistry,\n    cellPopoverRegistry,\n    numberFormatMenuRegistry,\n    repeatLocalCommandTransformRegistry,\n    repeatCommandTransformRegistry,\n    clipboardHandlersRegistries,\n    pivotRegistry,\n    pivotTimeAdapterRegistry,\n    pivotSidePanelRegistry,\n    pivotNormalizationValueRegistry,\n    supportedPivotPositionalFormulaRegistry,\n    pivotToFunctionValueRegistry,\n    migrationStepRegistry,\n};\nconst helpers = {\n    arg,\n    isEvaluationError,\n    toBoolean,\n    toJsDate,\n    toNumber,\n    toString,\n    toNormalizedPivotValue,\n    toXC,\n    toZone,\n    toUnboundedZone,\n    toCartesian,\n    numberToLetters,\n    lettersToNumber,\n    UuidGenerator,\n    formatValue,\n    createCurrencyFormat,\n    ColorGenerator,\n    computeTextWidth,\n    createEmptyWorkbookData,\n    createEmptySheet,\n    createEmptyExcelSheet,\n    getDefaultChartJsRuntime,\n    chartFontColor,\n    getChartAxisTitleRuntime,\n    getChartAxisType,\n    getTrendDatasetForBarChart,\n    getTrendDatasetForLineChart,\n    getFillingMode,\n    rgbaToHex,\n    colorToRGBA,\n    positionToZone,\n    isDefined,\n    isMatrix,\n    lazy,\n    genericRepeat,\n    createAction,\n    createActions,\n    transformRangeData,\n    deepEquals,\n    overlap,\n    union,\n    isInside,\n    deepCopy,\n    expandZoneOnInsertion,\n    reduceZoneOnDeletion,\n    unquote,\n    getMaxObjectId,\n    getFunctionsFromTokens,\n    getFirstPivotFunction,\n    getNumberOfPivotFunctions,\n    parseDimension,\n    isDateOrDatetimeField,\n    makeFieldProposal,\n    insertTokenAfterArgSeparator,\n    insertTokenAfterLeftParenthesis,\n    mergeContiguousZones,\n    getPivotHighlights,\n    pivotTimeAdapter,\n    UNDO_REDO_PIVOT_COMMANDS,\n    createPivotFormula,\n    areDomainArgsFieldsValid,\n    splitReference,\n    formatTickValue,\n    sanitizeSheetName,\n};\nconst links = {\n    isMarkdownLink,\n    parseMarkdownLink,\n    markdownLink,\n    openLink,\n    urlRepresentation,\n};\nconst components = {\n    Checkbox,\n    Section,\n    RoundColorPicker,\n    ChartDataSeries,\n    ChartErrorSection,\n    ChartLabelRange,\n    ChartTitle,\n    ChartPanel,\n    ChartFigure,\n    ChartJsComponent,\n    Grid,\n    GridOverlay,\n    ScorecardChart,\n    LineConfigPanel,\n    BarConfigPanel,\n    PieChartDesignPanel,\n    GenericChartConfigPanel,\n    ChartWithAxisDesignPanel,\n    GaugeChartConfigPanel,\n    GaugeChartDesignPanel,\n    ScorecardChartConfigPanel,\n    ScorecardChartDesignPanel,\n    ChartTypePicker,\n    FigureComponent,\n    Menu,\n    Popover,\n    SelectionInput,\n    ValidationMessages,\n    AddDimensionButton,\n    PivotDimensionGranularity,\n    PivotDimensionOrder,\n    PivotDimension,\n    PivotLayoutConfigurator,\n    EditableName,\n    PivotDeferUpdate,\n    PivotTitleSection,\n    CogWheelMenu,\n    TextInput,\n    SidePanelCollapsible,\n};\nconst hooks = {\n    useDragAndDropListItems,\n    useHighlights,\n    useHighlightsOnHover,\n};\nconst stores = {\n    useStoreProvider,\n    DependencyContainer,\n    CellPopoverStore,\n    ComposerFocusStore,\n    CellComposerStore,\n    FindAndReplaceStore,\n    HighlightStore,\n    HoveredCellStore,\n    ModelStore,\n    NotificationStore,\n    RendererStore,\n    SelectionInputStore,\n    SpreadsheetStore,\n    useStore,\n    useLocalStore,\n    SidePanelStore,\n    PivotSidePanelStore,\n    PivotMeasureDisplayPanelStore,\n};\nfunction addFunction(functionName, functionDescription) {\n    functionRegistry.add(functionName, functionDescription);\n    return {\n        addFunction: (fName, fDescription) => addFunction(fName, fDescription),\n    };\n}\nconst constants = {\n    DEFAULT_LOCALE,\n    HIGHLIGHT_COLOR,\n    PIVOT_TABLE_CONFIG,\n    TREND_LINE_XAXIS_ID,\n    CHART_AXIS_CHOICES,\n    ChartTerms,\n};\n\nexport { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };\n\n\n__info__.version = \"18.0.8\";\n__info__.date = \"2024-12-19T07:55:19.099Z\";\n__info__.hash = \"7cf34a618\";\n//# sourceMappingURL=o_spreadsheet.js.map\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { registries, tokenColors, helpers } from \"@odoo/o-spreadsheet\";\n\nconst { insertTokenAfterLeftParenthesis } = helpers;\n\n// copy-pasted list of options from the `account_type` selection field.\nconst ACCOUNT_TYPES = [\n    [\"asset_receivable\", _t(\"Receivable\")],\n    [\"asset_cash\", _t(\"Bank and Cash\")],\n    [\"asset_current\", _t(\"Current Assets\")],\n    [\"asset_non_current\", _t(\"Non-current Assets\")],\n    [\"asset_prepayments\", _t(\"Prepayments\")],\n    [\"asset_fixed\", _t(\"Fixed Assets\")],\n    [\"liability_payable\", _t(\"Payable\")],\n    [\"liability_credit_card\", _t(\"Credit Card\")],\n    [\"liability_current\", _t(\"Current Liabilities\")],\n    [\"liability_non_current\", _t(\"Non-current Liabilities\")],\n    [\"equity\", _t(\"Equity\")],\n    [\"equity_unaffected\", _t(\"Current Year Earnings\")],\n    [\"income\", _t(\"Income\")],\n    [\"income_other\", _t(\"Other Income\")],\n    [\"expense\", _t(\"Expenses\")],\n    [\"expense_depreciation\", _t(\"Depreciation\")],\n    [\"expense_direct_cost\", _t(\"Cost of Revenue\")],\n    [\"off_balance\", _t(\"Off-Balance Sheet\")],\n];\n\nregistries.autoCompleteProviders.add(\"account_group_types\", {\n    sequence: 50,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        const functionContext = tokenAtCursor.functionContext;\n        if (\n            functionContext?.parent.toUpperCase() === \"ODOO.ACCOUNT.GROUP\" &&\n            functionContext.argPosition === 0\n        ) {\n            return ACCOUNT_TYPES.map(([technicalName, displayName]) => {\n                const text = `\"${technicalName}\"`;\n                return {\n                    text,\n                    description: displayName,\n                    htmlContent: [{ value: text, color: tokenColors.STRING }],\n                    fuzzySearchKey: technicalName + displayName,\n                };\n            });\n        }\n        return;\n    },\n    selectProposal: insertTokenAfterLeftParenthesis,\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { EvaluationError } from \"@odoo/o-spreadsheet\";\nconst { functionRegistry } = spreadsheet.registries;\nconst { arg, toBoolean, toString, toNumber, toJsDate } = spreadsheet.helpers;\n\nconst QuarterRegexp = /^q([1-4])\\/(\\d{4})$/i;\nconst MonthRegexp = /^0?([1-9]|1[0-2])\\/(\\d{4})$/i;\n\n/**\n * @typedef {Object} YearDateRange\n * @property {\"year\"} rangeType\n * @property {number} year\n */\n\n/**\n * @typedef {Object} QuarterDateRange\n * @property {\"quarter\"} rangeType\n * @property {number} year\n * @property {number} quarter\n */\n\n/**\n * @typedef {Object} MonthDateRange\n * @property {\"month\"} rangeType\n * @property {number} year\n * @property {number} month\n */\n\n/**\n * @typedef {Object} DayDateRange\n * @property {\"day\"} rangeType\n * @property {number} year\n * @property {number} month\n * @property {number} day\n */\n\n/**\n * @typedef {YearDateRange | QuarterDateRange | MonthDateRange | DayDateRange} DateRange\n */\n\n/**\n * @param {object | undefined} dateRange\n * @returns {QuarterDateRange | undefined}\n */\nfunction parseAccountingQuarter(dateRange) {\n    const found = toString(dateRange?.value).trim().match(QuarterRegexp);\n    return found\n        ? {\n              rangeType: \"quarter\",\n              year: Number(found[2]),\n              quarter: Number(found[1]),\n          }\n        : undefined;\n}\n\n/**\n * @param {object | undefined} dateRange\n * @returns {MonthDateRange | undefined}\n */\nfunction parseAccountingMonth(dateRange, locale) {\n    if (\n        typeof dateRange?.value === \"number\" &&\n        dateRange.format?.includes(\"m\") &&\n        !dateRange.format?.includes(\"d\")\n    ) {\n        const date = toJsDate(dateRange.value, locale);\n        return {\n            rangeType: \"month\",\n            year: date.getFullYear(),\n            month: date.getMonth() + 1,\n        };\n    }\n    const found = toString(dateRange?.value).trim().match(MonthRegexp);\n    return found\n        ? {\n              rangeType: \"month\",\n              year: Number(found[2]),\n              month: Number(found[1]),\n          }\n        : undefined;\n}\n\n/**\n * @param {object | undefined} dateRange\n * @returns {YearDateRange | undefined}\n */\nfunction parseAccountingYear(dateRange, locale) {\n    const dateNumber = toNumber(dateRange?.value, locale);\n    // This allows a bit of flexibility for the user if they were to input a\n    // numeric value instead of a year.\n    // Users won't need to fetch accounting info for year 3000 before a long time\n    // And the numeric value 3000 corresponds to 18th march 1908, so it's not an\n    //issue to prevent them from fetching accounting data prior to that date.\n    if (dateNumber < 3000) {\n        return { rangeType: \"year\", year: dateNumber };\n    }\n    return undefined;\n}\n\n/**\n * @param {object | undefined} dateRange\n * @returns {DayDateRange}\n */\nfunction parseAccountingDay(dateRange, locale) {\n    const dateNumber = toNumber(dateRange?.value, locale);\n    return {\n        rangeType: \"day\",\n        year: functionRegistry.get(\"YEAR\").compute.bind({ locale })(dateNumber),\n        month: functionRegistry.get(\"MONTH\").compute.bind({ locale })(dateNumber),\n        day: functionRegistry.get(\"DAY\").compute.bind({ locale })(dateNumber),\n    };\n}\n\n/**\n * @param {object | undefined} dateRange\n * @returns {DateRange}\n */\nexport function parseAccountingDate(dateRange, locale) {\n    try {\n        return (\n            parseAccountingQuarter(dateRange) ||\n            parseAccountingMonth(dateRange, locale) ||\n            parseAccountingYear(dateRange, locale) ||\n            parseAccountingDay(dateRange, locale)\n        );\n    } catch {\n        throw new EvaluationError(\n            sprintf(\n                _t(\n                    `'%s' is not a valid period. Supported formats are \"21/12/2022\", \"Q1/2022\", \"12/2022\", and \"2022\".`\n                ),\n                dateRange?.value\n            )\n        );\n    }\n}\n\nconst ODOO_FIN_ARGS = () => [\n    arg(\"account_codes (string)\", _t(\"The prefix of the accounts.\")),\n    arg(\n        \"date_range (string, date)\",\n        _t(`The date range. Supported formats are \"21/12/2022\", \"Q1/2022\", \"12/2022\", and \"2022\".`)\n    ),\n    arg(\"offset (number, default=0)\", _t(\"Year offset applied to date_range.\")),\n    arg(\"company_id (number, optional)\", _t(\"The company to target (Advanced).\")),\n    arg(\n        \"include_unposted (boolean, default=FALSE)\",\n        _t(\"Set to TRUE to include unposted entries.\")\n    ),\n];\n\nfunctionRegistry.add(\"ODOO.CREDIT\", {\n    description: _t(\"Get the total credit for the specified account(s) and period.\"),\n    args: ODOO_FIN_ARGS(),\n    category: \"Odoo\",\n    returns: [\"NUMBER\"],\n    compute: function (\n        accountCodes,\n        dateRange,\n        offset = { value: 0 },\n        companyId = { value: null },\n        includeUnposted = { value: false }\n    ) {\n        const _accountCodes = toString(accountCodes)\n            .split(\",\")\n            .map((code) => code.trim())\n            .sort();\n        const _offset = toNumber(offset, this.locale);\n        const _dateRange = parseAccountingDate(dateRange, this.locale);\n        const _companyId = companyId?.value;\n        const _includeUnposted = toBoolean(includeUnposted);\n        return {\n            value: this.getters.getAccountPrefixCredit(\n                _accountCodes,\n                _dateRange,\n                _offset,\n                _companyId,\n                _includeUnposted\n            ),\n            format: this.getters.getCompanyCurrencyFormat(_companyId) || \"#,##0.00\",\n        };\n    },\n});\n\nfunctionRegistry.add(\"ODOO.DEBIT\", {\n    description: _t(\"Get the total debit for the specified account(s) and period.\"),\n    args: ODOO_FIN_ARGS(),\n    category: \"Odoo\",\n    returns: [\"NUMBER\"],\n    compute: function (\n        accountCodes,\n        dateRange,\n        offset = { value: 0 },\n        companyId = { value: null },\n        includeUnposted = { value: false }\n    ) {\n        const _accountCodes = toString(accountCodes)\n            .split(\",\")\n            .map((code) => code.trim())\n            .sort();\n        const _offset = toNumber(offset, this.locale);\n        const _dateRange = parseAccountingDate(dateRange, this.locale);\n        const _companyId = companyId?.value;\n        const _includeUnposted = toBoolean(includeUnposted);\n        return {\n            value: this.getters.getAccountPrefixDebit(\n                _accountCodes,\n                _dateRange,\n                _offset,\n                _companyId,\n                _includeUnposted\n            ),\n            format: this.getters.getCompanyCurrencyFormat(_companyId) || \"#,##0.00\",\n        };\n    },\n});\n\nfunctionRegistry.add(\"ODOO.BALANCE\", {\n    description: _t(\"Get the total balance for the specified account(s) and period.\"),\n    args: ODOO_FIN_ARGS(),\n    category: \"Odoo\",\n    returns: [\"NUMBER\"],\n    compute: function (\n        accountCodes,\n        dateRange,\n        offset = { value: 0 },\n        companyId = { value: null },\n        includeUnposted = { value: false }\n    ) {\n        const _accountCodes = toString(accountCodes)\n            .split(\",\")\n            .map((code) => code.trim())\n            .sort();\n        const _offset = toNumber(offset, this.locale);\n        const _dateRange = parseAccountingDate(dateRange, this.locale);\n        const _companyId = companyId?.value;\n        const _includeUnposted = toBoolean(includeUnposted);\n        const value =\n            this.getters.getAccountPrefixDebit(\n                _accountCodes,\n                _dateRange,\n                _offset,\n                _companyId,\n                _includeUnposted\n            ) -\n            this.getters.getAccountPrefixCredit(\n                _accountCodes,\n                _dateRange,\n                _offset,\n                _companyId,\n                _includeUnposted\n            );\n        return { value, format: this.getters.getCompanyCurrencyFormat(_companyId) || \"#,##0.00\" };\n    },\n});\n\nfunctionRegistry.add(\"ODOO.FISCALYEAR.START\", {\n    description: _t(\"Returns the starting date of the fiscal year encompassing the provided date.\"),\n    args: [\n        arg(\"day (date)\", _t(\"The day from which to extract the fiscal year start.\")),\n        arg(\"company_id (number, optional)\", _t(\"The company.\")),\n    ],\n    category: \"Odoo\",\n    returns: [\"NUMBER\"],\n    compute: function (date, companyId = { value: null }) {\n        const startDate = this.getters.getFiscalStartDate(\n            toJsDate(date, this.locale),\n            companyId.value === null ? null : toNumber(companyId, this.locale)\n        );\n        return {\n            value: toNumber(startDate, this.locale),\n            format: this.locale.dateFormat,\n        };\n    },\n});\n\nfunctionRegistry.add(\"ODOO.FISCALYEAR.END\", {\n    description: _t(\"Returns the ending date of the fiscal year encompassing the provided date.\"),\n    args: [\n        arg(\"day (date)\", _t(\"The day from which to extract the fiscal year end.\")),\n        arg(\"company_id (number, optional)\", _t(\"The company.\")),\n    ],\n    category: \"Odoo\",\n    returns: [\"NUMBER\"],\n    compute: function (date, companyId = { value: null }) {\n        const endDate = this.getters.getFiscalEndDate(\n            toJsDate(date, this.locale),\n            companyId.value === null ? null : toNumber(companyId, this.locale)\n        );\n        return {\n            value: toNumber(endDate, this.locale),\n            format: this.locale.dateFormat,\n        };\n    },\n});\n\nfunctionRegistry.add(\"ODOO.ACCOUNT.GROUP\", {\n    description: _t(\"Returns the account ids of a given group.\"),\n    args: [arg(\"type (string)\", _t(\"The account type (income, expense, asset_current,...).\"))],\n    category: \"Odoo\",\n    returns: [\"NUMBER\"],\n    compute: function (accountType) {\n        const accountTypes = this.getters.getAccountGroupCodes(toString(accountType));\n        return accountTypes.join(\",\");\n    },\n});\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { AccountingPlugin } from \"./plugins/accounting_plugin\";\nimport { getFirstAccountFunction, getNumberOfAccountFormulas } from \"./utils\";\nimport { parseAccountingDate } from \"./accounting_functions\";\nimport { camelToSnakeObject } from \"@spreadsheet/helpers/helpers\";\n\nconst { cellMenuRegistry, featurePluginRegistry } = spreadsheet.registries;\nconst { astToFormula } = spreadsheet;\nconst { isEvaluationError, toString, toBoolean } = spreadsheet.helpers;\n\nfeaturePluginRegistry.add(\"odooAccountingAggregates\", AccountingPlugin);\n\ncellMenuRegistry.add(\"move_lines_see_records\", {\n    name: _t(\"See records\"),\n    sequence: 176,\n    async execute(env) {\n        const position = env.model.getters.getActivePosition();\n        const sheetId = position.sheetId;\n        const cell = env.model.getters.getCell(position);\n        const { args } = getFirstAccountFunction(cell.compiledFormula.tokens);\n        let [codes, date_range, offset, companyId, includeUnposted] = args\n            .map(astToFormula)\n            .map((arg) => env.model.getters.evaluateFormulaResult(sheetId, arg));\n        codes = toString(codes?.value).split(\",\");\n        const locale = env.model.getters.getLocale();\n        const dateRange = parseAccountingDate(date_range, locale);\n        offset = parseInt(offset?.value) || 0;\n        dateRange.year += offset || 0;\n        companyId = parseInt(companyId?.value) || null;\n        try {\n            includeUnposted = toBoolean(includeUnposted.value);\n        } catch {\n            includeUnposted = false;\n        }\n\n        const action = await env.services.orm.call(\n            \"account.account\",\n            \"spreadsheet_move_line_action\",\n            [camelToSnakeObject({ dateRange, companyId, codes, includeUnposted })]\n        );\n        await env.services.action.doAction(action);\n    },\n    isVisible: (env) => {\n        const position = env.model.getters.getActivePosition();\n        const evaluatedCell = env.model.getters.getEvaluatedCell(position);\n        const cell = env.model.getters.getCell(position);\n        return (\n            !isEvaluationError(evaluatedCell.value) &&\n            evaluatedCell.value !== \"\" &&\n            cell &&\n            cell.isFormula &&\n            getNumberOfAccountFormulas(cell.compiledFormula.tokens) === 1\n        );\n    },\n});\n", "/** @odoo-module */\n// @ts-check\n\nimport { EvaluationError } from \"@odoo/o-spreadsheet\";\nimport { OdooUIPlugin } from \"@spreadsheet/plugins\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { deepCopy } from \"@web/core/utils/objects\";\nimport { camelToSnakeObject, toServerDateString } from \"@spreadsheet/helpers/helpers\";\n\n/**\n * @typedef {import(\"../accounting_functions\").DateRange} DateRange\n */\n\nexport class AccountingPlugin extends OdooUIPlugin {\n    static getters = /** @type {const} */ ([\n        \"getAccountPrefixCredit\",\n        \"getAccountPrefixDebit\",\n        \"getAccountGroupCodes\",\n        \"getFiscalStartDate\",\n        \"getFiscalEndDate\",\n    ]);\n    constructor(config) {\n        super(config);\n        /** @type {import(\"@spreadsheet/data_sources/server_data\").ServerData} */\n        this._serverData = config.custom.odooDataProvider?.serverData;\n    }\n\n    get serverData() {\n        if (!this._serverData) {\n            throw new Error(\n                \"'serverData' is not defined, please make sure a 'OdooDataProvider' instance is provided to the model.\"\n            );\n        }\n        return this._serverData;\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    /**\n     * Gets the total balance for given account code prefix\n     * @param {string[]} codes prefixes of the accounts' codes\n     * @param {DateRange} dateRange start date of the period to look\n     * @param {number} offset end  date of the period to look\n     * @param {number | null} companyId specific company to target\n     * @param {boolean} includeUnposted wether or not select unposted entries\n     * @returns {number}\n     */\n    getAccountPrefixCredit(codes, dateRange, offset, companyId, includeUnposted) {\n        const data = this._fetchAccountData(codes, dateRange, offset, companyId, includeUnposted);\n        return data.credit;\n    }\n\n    /**\n     * Gets the total balance for a given account code prefix\n     * @param {string[]} codes prefixes of the accounts codes\n     * @param {DateRange} dateRange start date of the period to look\n     * @param {number} offset end  date of the period to look\n     * @param {number | null} companyId specific company to target\n     * @param {boolean} includeUnposted wether or not select unposted entries\n     * @returns {number}\n     */\n    getAccountPrefixDebit(codes, dateRange, offset, companyId, includeUnposted) {\n        const data = this._fetchAccountData(codes, dateRange, offset, companyId, includeUnposted);\n        return data.debit;\n    }\n\n    /**\n     * @param {Date} date Date included in the fiscal year\n     * @param {number | null} companyId specific company to target\n     * @returns {string | undefined}\n     */\n    getFiscalStartDate(date, companyId) {\n        return this._fetchCompanyData(date, companyId).start;\n    }\n\n    /**\n     * @param {Date} date Date included in the fiscal year\n     * @param {number | undefined} companyId specific company to target\n     * @returns {string | undefined}\n     */\n    getFiscalEndDate(date, companyId) {\n        return this._fetchCompanyData(date, companyId).end;\n    }\n\n    /**\n     * @param {string} accountType\n     * @returns {string[]}\n     */\n    getAccountGroupCodes(accountType) {\n        return this.serverData.batch.get(\"account.account\", \"get_account_group\", accountType);\n    }\n\n    /**\n     * Fetch the account information (credit/debit) for a given account code\n     * @private\n     * @param {string[]} codes prefix of the accounts' codes\n     * @param {DateRange} dateRange start date of the period to look\n     * @param {number} offset end  date of the period to look\n     * @param {number | null} companyId specific companyId to target\n     * @param {boolean} includeUnposted wether or not select unposted entries\n     * @returns {{ debit: number, credit: number }}\n     */\n    _fetchAccountData(codes, dateRange, offset, companyId, includeUnposted) {\n        dateRange = deepCopy(dateRange);\n        dateRange.year += offset;\n        // Excel dates start at 1899-12-30, we should not support date ranges\n        // that do not cover dates prior to it.\n        // Unfortunately, this check needs to be done right before the server\n        // call as a date to low (year <= 1) can raise an error server side.\n        if (dateRange.year < 1900) {\n            throw new EvaluationError(_t(\"%s is not a valid year.\", dateRange.year));\n        }\n        return this.serverData.batch.get(\n            \"account.account\",\n            \"spreadsheet_fetch_debit_credit\",\n            camelToSnakeObject({ dateRange, codes, companyId, includeUnposted })\n        );\n    }\n\n    /**\n     * Fetch the start and end date of the fiscal year enclosing a given date\n     * Defaults on the current user company if not provided\n     * @private\n     * @param {Date} date\n     * @param {number | null} companyId\n     * @returns {{start: string, end: string}}\n     */\n    _fetchCompanyData(date, companyId) {\n        const result = this.serverData.batch.get(\"res.company\", \"get_fiscal_dates\", {\n            date: toServerDateString(date),\n            company_id: companyId,\n        });\n        if (result === false) {\n            throw new EvaluationError(_t(\"The company fiscal year could not be found.\"));\n        }\n        return result;\n    }\n}\n", "/** @odoo-module **/\n// @ts-check\n\nimport { helpers } from \"@odoo/o-spreadsheet\";\n\nconst { getFunctionsFromTokens } = helpers;\n\n/**\n * @typedef {import(\"@odoo/o-spreadsheet\").Token} Token\n * @typedef  {import(\"@spreadsheet/helpers/odoo_functions_helpers\").OdooFunctionDescription} OdooFunctionDescription\n */\n\n/**\n * @param {Token[]} tokens\n * @returns {number}\n */\nexport function getNumberOfAccountFormulas(tokens) {\n    return getFunctionsFromTokens(tokens, [\"ODOO.BALANCE\", \"ODOO.CREDIT\", \"ODOO.DEBIT\"]).length;\n}\n\n/**\n * Get the first Account function description of the given formula.\n *\n * @param {Token[]} tokens\n * @returns {OdooFunctionDescription | undefined}\n */\nexport function getFirstAccountFunction(tokens) {\n    return getFunctionsFromTokens(tokens, [\"ODOO.BALANCE\", \"ODOO.CREDIT\", \"ODOO.DEBIT\"])[0];\n}\n", "/** @odoo-module */\n\n/**\n * @typedef {import(\"@web/webclient/actions/action_service\").ActionOptions} ActionOptions\n */\n\n/**\n * @param {*} env\n * @param {string} actionXmlId\n * @param {Object} actionDescription\n * @param {ActionOptions} options\n */\nexport async function navigateTo(env, actionXmlId, actionDescription, options) {\n    const actionService = env.services.action;\n    let navigateActionDescription;\n    const { views, view_mode, domain, context, name, res_model, res_id } = actionDescription;\n    try {\n        navigateActionDescription = await actionService.loadAction(actionXmlId, context);\n        const filteredViews = views.map(\n            ([v, viewType]) =>\n                navigateActionDescription.views.find(([, type]) => viewType === type) || [\n                    v,\n                    viewType,\n                ]\n        );\n\n        navigateActionDescription = {\n            ...navigateActionDescription,\n            context,\n            domain,\n            name,\n            res_model,\n            res_id,\n            view_mode,\n            target: \"current\",\n            views: filteredViews,\n        };\n    } catch {\n        navigateActionDescription = {\n            type: \"ir.actions.act_window\",\n            name,\n            res_model,\n            res_id,\n            views,\n            target: \"current\",\n            domain,\n            context,\n            view_mode,\n        };\n    } finally {\n        await actionService.doAction(\n            // clear empty keys\n            JSON.parse(JSON.stringify(navigateActionDescription)),\n            options\n        );\n    }\n}\n", "/** @odoo-module */\n\nimport { useSpreadsheetNotificationStore } from \"@spreadsheet/hooks\";\nimport { Spreadsheet, Model } from \"@odoo/o-spreadsheet\";\nimport { Component } from \"@odoo/owl\";\n\n/**\n * Component wrapping the <Spreadsheet> component from o-spreadsheet\n * to add user interactions extensions from odoo such as notifications,\n * error dialogs, etc.\n */\nexport class SpreadsheetComponent extends Component {\n    static template = \"spreadsheet.SpreadsheetComponent\";\n    static components = { Spreadsheet };\n    static props = {\n        model: Model,\n    };\n\n    get model() {\n        return this.props.model;\n    }\n    setup() {\n        useSpreadsheetNotificationStore();\n    }\n}\n", "/** @odoo-module */\n\nimport { download } from \"@web/core/network/download\";\nimport { registry } from \"@web/core/registry\";\nimport { createSpreadsheetModel, waitForDataLoaded } from \"@spreadsheet/helpers/model\";\n\n/**\n * @param {import(\"@web/env\").OdooEnv} env\n * @param {object} action\n */\nasync function downloadSpreadsheet(env, action) {\n    let { name, data, stateUpdateMessages, xlsxData } = action.params;\n    if (!xlsxData) {\n        const model = await createSpreadsheetModel({ env, data, revisions: stateUpdateMessages });\n        await waitForDataLoaded(model);\n        xlsxData = model.exportXLSX();\n    }\n    await download({\n        url: \"/spreadsheet/xlsx\",\n        data: {\n            zip_name: `${name}.xlsx`,\n            files: JSON.stringify(xlsxData.files),\n        },\n    });\n}\n\nregistry\n    .category(\"actions\")\n    .add(\"action_download_spreadsheet\", downloadSpreadsheet, { force: true });\n", "/** @odoo-module */\n\nimport { OdooViewsDataSource } from \"@spreadsheet/data_sources/odoo_views_data_source\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { GraphModel as ChartModel } from \"@web/views/graph/graph_model\";\n\nexport class ChartDataSource extends OdooViewsDataSource {\n    /**\n     * @override\n     * @param {Object} services Services (see DataSource)\n     */\n    constructor(services, params) {\n        super(services, params);\n    }\n\n    /**\n     * @protected\n     */\n    async _load() {\n        await super._load();\n        const metaData = {\n            fieldAttrs: {},\n            ...this._metaData,\n        };\n        this._model = new ChartModel(\n            {\n                _t,\n            },\n            metaData,\n            {\n                orm: this._orm,\n            }\n        );\n        await this._model.load(this._searchParams);\n    }\n\n    getData() {\n        if (!this.isReady()) {\n            this.load();\n            return { datasets: [], labels: [] };\n        }\n        if (!this._isValid) {\n            return { datasets: [], labels: [] };\n        }\n        return this._model.data;\n    }\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nconst { chartComponentRegistry } = spreadsheet.registries;\nconst { ChartJsComponent } = spreadsheet.components;\n\nchartComponentRegistry.add(\"odoo_bar\", ChartJsComponent);\nchartComponentRegistry.add(\"odoo_line\", ChartJsComponent);\nchartComponentRegistry.add(\"odoo_pie\", ChartJsComponent);\n\nimport { OdooChartCorePlugin } from \"./plugins/odoo_chart_core_plugin\";\nimport { ChartOdooMenuPlugin } from \"./plugins/chart_odoo_menu_plugin\";\nimport { OdooChartUIPlugin } from \"./plugins/odoo_chart_ui_plugin\";\n\nexport { OdooChartCorePlugin, ChartOdooMenuPlugin, OdooChartUIPlugin };\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { OdooChart } from \"./odoo_chart\";\n\nconst { chartRegistry } = spreadsheet.registries;\n\nconst {\n    getDefaultChartJsRuntime,\n    getChartAxisTitleRuntime,\n    chartFontColor,\n    ColorGenerator,\n    getTrendDatasetForBarChart,\n    formatValue,\n    formatTickValue,\n} = spreadsheet.helpers;\n\nconst { TREND_LINE_XAXIS_ID } = spreadsheet.constants;\n\nexport class OdooBarChart extends OdooChart {\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.verticalAxisPosition = definition.verticalAxisPosition;\n        this.stacked = definition.stacked;\n        this.axesDesign = definition.axesDesign;\n        this.trend = definition.trend;\n    }\n\n    getDefinition() {\n        return {\n            ...super.getDefinition(),\n            verticalAxisPosition: this.verticalAxisPosition,\n            stacked: this.stacked,\n            axesDesign: this.axesDesign,\n            trend: this.trend,\n        };\n    }\n}\n\nchartRegistry.add(\"odoo_bar\", {\n    match: (type) => type === \"odoo_bar\",\n    createChart: (definition, sheetId, getters) => new OdooBarChart(definition, sheetId, getters),\n    getChartRuntime: createOdooChartRuntime,\n    validateChartDefinition: (validator, definition) =>\n        OdooBarChart.validateChartDefinition(validator, definition),\n    transformDefinition: (definition) => OdooBarChart.transformDefinition(definition),\n    getChartDefinitionFromContextCreation: () => OdooBarChart.getDefinitionFromContextCreation(),\n    name: _t(\"Bar\"),\n});\n\nfunction createOdooChartRuntime(chart, getters) {\n    const background = chart.background || \"#FFFFFF\";\n    const { datasets, labels } = chart.dataSource.getData();\n    const locale = getters.getLocale();\n    const chartJsConfig = getBarConfiguration(chart, labels, locale);\n    chartJsConfig.options = {\n        ...chartJsConfig.options,\n        ...getters.getChartDatasetActionCallbacks(chart),\n    };\n    const colors = new ColorGenerator(datasets.length);\n    const trendDatasets = [];\n    for (const { label, data } of datasets) {\n        const color = colors.next();\n        const dataset = {\n            label,\n            data,\n            borderColor: \"#FFFFFF\",\n            borderWidth: 1,\n            backgroundColor: color,\n        };\n        chartJsConfig.data.datasets.push(dataset);\n\n        const trend = chart.getDefinition().trend;\n        if (!trend?.display || chart.horizontal) {\n            continue;\n        }\n\n        const trendDataset = getTrendDatasetForBarChart(trend, dataset);\n        if (trendDataset) {\n            trendDatasets.push(trendDataset);\n        }\n    }\n\n    if (trendDatasets.length) {\n        /* We add a second x axis here to draw the trend lines, with the labels length being\n         * set so that the second axis points match the classical x axis\n         */\n        const maxLength = Math.max(\n            ...trendDatasets.map((trendDataset) => trendDataset.data.length)\n        );\n        chartJsConfig.options.scales[TREND_LINE_XAXIS_ID] = {\n            ...chartJsConfig.options.scales.x,\n            labels: Array(maxLength).fill(\"\"),\n            offset: false,\n            display: false,\n        };\n        /* These datasets must be inserted after the original\n         * datasets to ensure the way we distinguish the originals and trendLine datasets after\n         */\n        trendDatasets.forEach((x) => chartJsConfig.data.datasets.push(x));\n\n        const originalTooltipTitle = chartJsConfig.options.plugins.tooltip.callbacks.title;\n        chartJsConfig.options.plugins.tooltip.callbacks.title = function (tooltipItems) {\n            if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {\n                return originalTooltipTitle?.(tooltipItems);\n            }\n            return \"\";\n        };\n    }\n    return { background, chartJsConfig };\n}\n\nfunction getBarConfiguration(chart, labels, locale) {\n    const color = chartFontColor(chart.background);\n    const config = getDefaultChartJsRuntime(chart, labels, color, { locale });\n    config.type = chart.type.replace(\"odoo_\", \"\");\n    const legend = {\n        ...config.options.legend,\n        display: chart.legendPosition !== \"none\",\n        labels: { color },\n    };\n    legend.position = chart.legendPosition;\n    config.options.plugins = config.options.plugins || {};\n    config.options.plugins.legend = legend;\n    config.options.layout = {\n        padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },\n    };\n    config.options.scales = {\n        x: {\n            ticks: {\n                // x axis configuration\n                maxRotation: 60,\n                minRotation: 15,\n                padding: 5,\n                labelOffset: 2,\n                color,\n            },\n            title: getChartAxisTitleRuntime(chart.axesDesign?.x),\n        },\n        y: {\n            position: chart.verticalAxisPosition,\n            ticks: {\n                color,\n                callback: (value) =>\n                    formatValue(value, {\n                        locale,\n                        format: Math.abs(value) >= 1000 ? \"#,##\" : undefined,\n                    }),\n            },\n            beginAtZero: true, // the origin of the y axis is always zero\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y),\n        },\n    };\n    if (chart.stacked) {\n        config.options.scales.x.stacked = true;\n        config.options.scales.y.stacked = true;\n    }\n\n    config.options.plugins.chartShowValuesPlugin = {\n        showValues: chart.showValues,\n        background: chart.background,\n        horizontal: chart.horizontal,\n        callback: formatTickValue({ locale }),\n    };\n\n    return config;\n}\n", "/** @odoo-module */\n\nimport { AbstractChart, CommandResult } from \"@odoo/o-spreadsheet\";\nimport { ChartDataSource } from \"../data_source/chart_data_source\";\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n *\n * @typedef MetaData\n * @property {Array<Object>} domains\n * @property {Array<string>} groupBy\n * @property {string} measure\n * @property {string} mode\n * @property {string} [order]\n * @property {string} resModel\n * @property {boolean} stacked\n *\n * @typedef OdooChartDefinition\n * @property {string} type\n * @property {MetaData} metaData\n * @property {SearchParams} searchParams\n * @property {string} title\n * @property {string} background\n * @property {string} legendPosition\n * @property {boolean} cumulative\n *\n * @typedef OdooChartDefinitionDataSource\n * @property {MetaData} metaData\n * @property {SearchParams} searchParams\n *\n */\n\nexport class OdooChart extends AbstractChart {\n    /**\n     * @param {OdooChartDefinition} definition\n     * @param {string} sheetId\n     * @param {Object} getters\n     */\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.type = definition.type;\n        this.metaData = {\n            ...definition.metaData,\n            mode: this.type.replace(\"odoo_\", \"\"),\n            cumulated: definition.cumulative,\n            // if a chart is cumulated, the first data point should take into\n            // account past data, even if a domain on a specific period is applied\n            cumulatedStart: definition.cumulative,\n        };\n        this.searchParams = definition.searchParams;\n        this.legendPosition = definition.legendPosition;\n        this.background = definition.background;\n        this.dataSource = undefined;\n        this.actionXmlId = definition.actionXmlId;\n        this.showValues = definition.showValues;\n    }\n\n    static transformDefinition(definition) {\n        return definition;\n    }\n\n    static validateChartDefinition(validator, definition) {\n        return CommandResult.Success;\n    }\n\n    static getDefinitionFromContextCreation() {\n        throw new Error(\"It's not possible to convert an Odoo chart to a native chart\");\n    }\n\n    /**\n     * @returns {OdooChartDefinitionDataSource}\n     */\n    getDefinitionForDataSource() {\n        return {\n            metaData: this.metaData,\n            searchParams: this.searchParams,\n        };\n    }\n\n    /**\n     * @returns {OdooChartDefinition}\n     */\n    getDefinition() {\n        return {\n            //@ts-ignore Defined in the parent class\n            title: this.title,\n            background: this.background,\n            legendPosition: this.legendPosition,\n            metaData: this.metaData,\n            searchParams: this.searchParams,\n            type: this.type,\n            actionXmlId: this.actionXmlId,\n            showValues: this.showValues,\n        };\n    }\n\n    getDefinitionForExcel() {\n        // Export not supported\n        return undefined;\n    }\n\n    /**\n     * @returns {OdooChart}\n     */\n    updateRanges() {\n        // No range on this graph\n        return this;\n    }\n\n    /**\n     * @returns {OdooChart}\n     */\n    copyForSheetId() {\n        return this;\n    }\n\n    /**\n     * @returns {OdooChart}\n     */\n    copyInSheetId() {\n        return this;\n    }\n\n    getContextCreation() {\n        return {};\n    }\n\n    getSheetIdsUsedInChartRanges() {\n        return [];\n    }\n\n    setDataSource(dataSource) {\n        if (dataSource instanceof ChartDataSource) {\n            this.dataSource = dataSource;\n        } else {\n            throw new Error(\"Only ChartDataSources can be added.\");\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { OdooChart } from \"./odoo_chart\";\n\nconst { chartRegistry } = spreadsheet.registries;\n\nconst {\n    getDefaultChartJsRuntime,\n    getChartAxisTitleRuntime,\n    chartFontColor,\n    ColorGenerator,\n    getFillingMode,\n    colorToRGBA,\n    rgbaToHex,\n    getTrendDatasetForLineChart,\n    getChartAxisType,\n    formatValue,\n    formatTickValue,\n} = spreadsheet.helpers;\n\nconst { TREND_LINE_XAXIS_ID } = spreadsheet.constants;\n\nconst LINE_FILL_TRANSPARENCY = 0.4;\n\nexport class OdooLineChart extends OdooChart {\n    constructor(definition, sheetId, getters) {\n        super(definition, sheetId, getters);\n        this.verticalAxisPosition = definition.verticalAxisPosition;\n        this.stacked = definition.stacked;\n        this.cumulative = definition.cumulative;\n        this.axesDesign = definition.axesDesign;\n        this.fillArea = definition.fillArea;\n        this.trend = definition.trend;\n    }\n\n    getDefinition() {\n        return {\n            ...super.getDefinition(),\n            verticalAxisPosition: this.verticalAxisPosition,\n            stacked: this.stacked,\n            cumulative: this.cumulative,\n            axesDesign: this.axesDesign,\n            fillArea: this.fillArea,\n            trend: this.trend,\n        };\n    }\n}\n\nchartRegistry.add(\"odoo_line\", {\n    match: (type) => type === \"odoo_line\",\n    createChart: (definition, sheetId, getters) => new OdooLineChart(definition, sheetId, getters),\n    getChartRuntime: createOdooChartRuntime,\n    validateChartDefinition: (validator, definition) =>\n        OdooLineChart.validateChartDefinition(validator, definition),\n    transformDefinition: (definition) => OdooLineChart.transformDefinition(definition),\n    getChartDefinitionFromContextCreation: () => OdooLineChart.getDefinitionFromContextCreation(),\n    name: _t(\"Line\"),\n});\n\nfunction createOdooChartRuntime(chart, getters) {\n    const background = chart.background || \"#FFFFFF\";\n    const { datasets, labels } = chart.dataSource.getData();\n    const locale = getters.getLocale();\n    const chartJsConfig = getLineConfiguration(chart, labels, locale);\n    const colors = new ColorGenerator(datasets.length);\n\n    let maxLength = 0;\n    const trendDatasets = [];\n    const axisType = getChartAxisType(chart, getters);\n\n    for (const index in datasets) {\n        let { label, data, cumulatedStart } = datasets[index];\n\n        const color = colors.next();\n        let backgroundColor = color;\n        if (chart.fillArea) {\n            const backgroundRGBA = colorToRGBA(color);\n            // use the transparency of Odoo to keep consistency\n            backgroundRGBA.a = LINE_FILL_TRANSPARENCY;\n            backgroundColor = rgbaToHex(backgroundRGBA);\n        }\n        if (chart.cumulative) {\n            let accumulator = cumulatedStart;\n            data = data.map((value) => {\n                accumulator += value;\n                return accumulator;\n            });\n        }\n\n        const dataset = {\n            label,\n            data,\n            lineTension: 0,\n            borderColor: color,\n            backgroundColor,\n            pointBackgroundColor: color,\n            fill: chart.fillArea ? getFillingMode(parseInt(index), chart.stacked) : false,\n        };\n        chartJsConfig.data.datasets.push(dataset);\n\n        const trend = chart.getDefinition().trend;\n        if (!trend?.display) {\n            continue;\n        }\n\n        const trendDataset = getTrendDatasetForLineChart(trend, dataset, axisType, locale);\n        if (trendDataset) {\n            maxLength = Math.max(maxLength, trendDataset.data.length);\n            trendDatasets.push(trendDataset);\n        }\n    }\n\n    if (trendDatasets.length) {\n        /* We add a second x axis here to draw the trend lines, with the labels length being\n         * set so that the second axis points match the classical x axis\n         */\n        chartJsConfig.options.scales[TREND_LINE_XAXIS_ID] = {\n            ...chartJsConfig.options.scales.x,\n            type: \"category\",\n            labels: Array(maxLength).fill(\"\"),\n            offset: false,\n            display: false,\n        };\n        /* These datasets must be inserted after the original datasets to ensure the way we\n         * distinguish the originals and trendLine datasets after\n         */\n        trendDatasets.forEach((x) => chartJsConfig.data.datasets.push(x));\n\n        const originalTooltipTitle = chartJsConfig.options.plugins.tooltip.callbacks.title;\n        chartJsConfig.options.plugins.tooltip.callbacks.title = function (tooltipItems) {\n            if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {\n                return originalTooltipTitle?.(tooltipItems);\n            }\n            return \"\";\n        };\n    }\n    return { background, chartJsConfig };\n}\n\nfunction getLineConfiguration(chart, labels, locale) {\n    const fontColor = chartFontColor(chart.background);\n    const config = getDefaultChartJsRuntime(chart, labels, fontColor, { locale });\n    config.type = chart.type.replace(\"odoo_\", \"\");\n    const legend = {\n        ...config.options.legend,\n        display: chart.legendPosition !== \"none\",\n    };\n    legend.position = chart.legendPosition;\n    config.options.plugins = config.options.plugins || {};\n    config.options.plugins.legend = legend;\n    config.options.layout = {\n        padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },\n    };\n    config.options.scales = {\n        x: {\n            ticks: {\n                // x axis configuration\n                maxRotation: 60,\n                minRotation: 15,\n                padding: 5,\n                labelOffset: 2,\n                color: fontColor,\n            },\n            title: getChartAxisTitleRuntime(chart.axesDesign?.x),\n        },\n        y: {\n            position: chart.verticalAxisPosition,\n            ticks: {\n                color: fontColor,\n                callback: (value) =>\n                    formatValue(value, {\n                        locale,\n                        format: Math.abs(value) >= 1000 ? \"#,##\" : undefined,\n                    }),\n            },\n            title: getChartAxisTitleRuntime(chart.axesDesign?.y),\n        },\n    };\n    if (chart.stacked) {\n        config.options.scales.y.stacked = true;\n    }\n\n    config.options.plugins.chartShowValuesPlugin = {\n        showValues: chart.showValues,\n        background: chart.background,\n        callback: formatTickValue({ locale }),\n    };\n    return config;\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { OdooChart } from \"./odoo_chart\";\n\nconst { chartRegistry } = spreadsheet.registries;\n\nconst { getDefaultChartJsRuntime, chartFontColor, ColorGenerator, formatTickValue } =\n    spreadsheet.helpers;\n\nchartRegistry.add(\"odoo_pie\", {\n    match: (type) => type === \"odoo_pie\",\n    createChart: (definition, sheetId, getters) => new OdooChart(definition, sheetId, getters),\n    getChartRuntime: createOdooChartRuntime,\n    validateChartDefinition: (validator, definition) =>\n        OdooChart.validateChartDefinition(validator, definition),\n    transformDefinition: (definition) => OdooChart.transformDefinition(definition),\n    getChartDefinitionFromContextCreation: () => OdooChart.getDefinitionFromContextCreation(),\n    name: _t(\"Pie\"),\n});\n\nfunction createOdooChartRuntime(chart, getters) {\n    const background = chart.background || \"#FFFFFF\";\n    const { datasets, labels } = chart.dataSource.getData();\n    const locale = getters.getLocale();\n    const chartJsConfig = getPieConfiguration(chart, labels, locale);\n    chartJsConfig.options = {\n        ...chartJsConfig.options,\n        ...getters.getChartDatasetActionCallbacks(chart),\n    };\n    const dataSetsLength = Math.max(0, ...datasets.map((ds) => ds?.data?.length ?? 0));\n    const colors = new ColorGenerator(dataSetsLength);\n    for (const { label, data } of datasets) {\n        const backgroundColor = getPieColors(colors, datasets);\n        const dataset = {\n            label,\n            data,\n            borderColor: \"#FFFFFF\",\n            backgroundColor,\n            hoverOffset: 30,\n        };\n        chartJsConfig.data.datasets.push(dataset);\n    }\n    return { background, chartJsConfig };\n}\n\nfunction getPieConfiguration(chart, labels, locale) {\n    const color = chartFontColor(chart.background);\n    const config = getDefaultChartJsRuntime(chart, labels, color, { locale });\n    config.type = chart.type.replace(\"odoo_\", \"\");\n    const legend = {\n        ...config.options.legend,\n        display: chart.legendPosition !== \"none\",\n        labels: { color },\n    };\n    legend.position = chart.legendPosition;\n    config.options.plugins = config.options.plugins || {};\n    config.options.plugins.legend = legend;\n    config.options.layout = {\n        padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },\n    };\n    config.options.plugins.tooltip = {\n        callbacks: {\n            title: function (tooltipItem) {\n                return tooltipItem.label;\n            },\n        },\n    };\n\n    config.options.plugins.chartShowValuesPlugin = {\n        showValues: chart.showValues,\n        callback: formatTickValue({ locale }),\n    };\n    return config;\n}\n\nfunction getPieColors(colors, dataSetsValues) {\n    const pieColors = [];\n    const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));\n    for (let i = 0; i <= maxLength; i++) {\n        pieColors.push(colors.next());\n    }\n\n    return pieColors;\n}\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(spreadsheet.components.FigureComponent.prototype, {\n    setup() {\n        super.setup();\n        this.menuService = useService(\"menu\");\n        this.actionService = useService(\"action\");\n        this.notificationService = useService(\"notification\");\n    },\n    async navigateToOdooMenu() {\n        const menu = this.env.model.getters.getChartOdooMenu(this.props.figure.id);\n        if (!menu) {\n            throw new Error(`Cannot find any menu associated with the chart`);\n        }\n        if (!menu.actionID) {\n            this.notificationService.add(\n                _t(\n                    \"The menu linked to this chart doesn't have an corresponding action. Please link the chart to another menu.\"\n                ),\n                { type: \"danger\" }\n            );\n            return;\n        }\n        await this.actionService.doAction(menu.actionID);\n    },\n    get hasOdooMenu() {\n        return this.env.model.getters.getChartOdooMenu(this.props.figure.id) !== undefined;\n    },\n    async onClick() {\n        if (this.env.isDashboard() && this.hasOdooMenu) {\n            this.navigateToOdooMenu();\n        }\n    },\n});\n", "/** @odoo-module */\n\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\nimport { coreTypes, helpers } from \"@odoo/o-spreadsheet\";\nimport { omit } from \"@web/core/utils/objects\";\nconst { deepEquals } = helpers;\n\n/** Plugin that link charts with Odoo menus. It can contain either the Id of the odoo menu, or its xml id. */\nexport class ChartOdooMenuPlugin extends OdooCorePlugin {\n    static getters = /** @type {const} */ ([\"getChartOdooMenu\"]);\n    constructor(config) {\n        super(config);\n        this.odooMenuReference = {};\n    }\n\n    /**\n     * Handle a spreadsheet command\n     * @param {Object} cmd Command\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"LINK_ODOO_MENU_TO_CHART\":\n                this.history.update(\"odooMenuReference\", cmd.chartId, cmd.odooMenuId);\n                break;\n            case \"DELETE_FIGURE\":\n                this.history.update(\"odooMenuReference\", cmd.id, undefined);\n                break;\n            case \"DUPLICATE_SHEET\":\n                this.updateOnDuplicateSheet(cmd.sheetId, cmd.sheetIdTo);\n                break;\n        }\n    }\n\n    updateOnDuplicateSheet(sheetIdFrom, sheetIdTo) {\n        for (const oldChartId of this.getters.getChartIds(sheetIdFrom)) {\n            if (!this.odooMenuReference[oldChartId]) {\n                continue;\n            }\n            const oldChartDefinition = this.getters.getChartDefinition(oldChartId);\n            const oldFigure = this.getters.getFigure(sheetIdFrom, oldChartId);\n            const newChartId = this.getters.getChartIds(sheetIdTo).find((newChartId) => {\n                const newChartDefinition = this.getters.getChartDefinition(newChartId);\n                const newFigure = this.getters.getFigure(sheetIdTo, newChartId);\n                return (\n                    deepEquals(oldChartDefinition, newChartDefinition) &&\n                    deepEquals(omit(newFigure, \"id\"), omit(oldFigure, \"id\")) // compare size and position\n                );\n            });\n\n            if (newChartId) {\n                this.history.update(\n                    \"odooMenuReference\",\n                    newChartId,\n                    this.odooMenuReference[oldChartId]\n                );\n            }\n        }\n    }\n\n    /**\n     * Get odoo menu linked to the chart\n     *\n     * @param {string} chartId\n     * @returns {object | undefined}\n     */\n    getChartOdooMenu(chartId) {\n        const menuId = this.odooMenuReference[chartId];\n        return menuId ? this.getters.getIrMenu(menuId) : undefined;\n    }\n\n    import(data) {\n        if (data.chartOdooMenusReferences) {\n            this.odooMenuReference = data.chartOdooMenusReferences;\n        }\n    }\n\n    export(data) {\n        data.chartOdooMenusReferences = this.odooMenuReference;\n    }\n}\n\ncoreTypes.add(\"LINK_ODOO_MENU_TO_CHART\");\n", "/** @odoo-module */\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { checkFilterFieldMatching } from \"@spreadsheet/global_filters/helpers\";\nimport { CommandResult } from \"../../o_spreadsheet/cancelled_reason\";\nimport { Domain } from \"@web/core/domain\";\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Chart\n * @property {Object} fieldMatching\n *\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n */\n\nconst CHART_PLACEHOLDER_DISPLAY_NAME = {\n    odoo_bar: _t(\"Odoo Bar Chart\"),\n    odoo_line: _t(\"Odoo Line Chart\"),\n    odoo_pie: _t(\"Odoo Pie Chart\"),\n};\n\nexport class OdooChartCorePlugin extends OdooCorePlugin {\n    static getters = /** @type {const} */ ([\n        \"getOdooChartIds\",\n        \"getChartFieldMatch\",\n        \"getOdooChartDisplayName\",\n        \"getOdooChartFieldMatching\",\n    ]);\n\n    constructor(config) {\n        super(config);\n\n        /** @type {Object.<string, Chart>} */\n        this.charts = {};\n\n        globalFiltersFieldMatchers[\"chart\"] = {\n            getIds: () => this.getters.getOdooChartIds(),\n            getDisplayName: (chartId) => this.getters.getOdooChartDisplayName(chartId),\n            getFieldMatching: (chartId, filterId) =>\n                this.getOdooChartFieldMatching(chartId, filterId),\n            getModel: (chartId) =>\n                this.getters.getChart(chartId).getDefinitionForDataSource().metaData.resModel,\n        };\n    }\n\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n                if (cmd.chart) {\n                    return checkFilterFieldMatching(cmd.chart);\n                }\n        }\n        return CommandResult.Success;\n    }\n\n    /**\n     * Handle a spreadsheet command\n     *\n     * @param {Object} cmd Command\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_CHART\": {\n                switch (cmd.definition.type) {\n                    case \"odoo_pie\":\n                    case \"odoo_bar\":\n                    case \"odoo_line\":\n                        this._addOdooChart(cmd.id);\n                        break;\n                }\n                break;\n            }\n            case \"DELETE_FIGURE\": {\n                const charts = { ...this.charts };\n                delete charts[cmd.id];\n                this.history.update(\"charts\", charts);\n                break;\n            }\n            case \"REMOVE_GLOBAL_FILTER\":\n                this._onFilterDeletion(cmd.id);\n                break;\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n                if (cmd.chart) {\n                    this._setOdooChartFieldMatching(cmd.filter.id, cmd.chart);\n                }\n                break;\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    /**\n     * Get all the odoo chart ids\n     * @returns {Array<string>}\n     */\n    getOdooChartIds() {\n        return Object.keys(this.charts);\n    }\n\n    /**\n     * @param {string} chartId\n     * @returns {string}\n     */\n    getChartFieldMatch(chartId) {\n        return this.charts[chartId].fieldMatching;\n    }\n\n    /**\n     *\n     * @param {string} chartId\n     * @returns {string}\n     */\n    getOdooChartDisplayName(chartId) {\n        const { title, type } = this.getters.getChart(chartId);\n        const name = title.text || CHART_PLACEHOLDER_DISPLAY_NAME[type];\n        return `(#${this.getOdooChartIds().indexOf(chartId) + 1}) ${name}`;\n    }\n\n    /**\n     * Import the charts\n     *\n     * @param {Object} data\n     */\n    import(data) {\n        for (const sheet of data.sheets) {\n            if (sheet.figures) {\n                for (const figure of sheet.figures) {\n                    if (figure.tag === \"chart\" && figure.data.type.startsWith(\"odoo_\")) {\n                        this._addOdooChart(figure.id, figure.data.fieldMatching);\n                    }\n                }\n            }\n        }\n    }\n    /**\n     * Export the chart\n     *\n     * @param {Object} data\n     */\n    export(data) {\n        for (const sheet of data.sheets) {\n            if (sheet.figures) {\n                for (const figure of sheet.figures) {\n                    if (figure.tag === \"chart\" && figure.data.type.startsWith(\"odoo_\")) {\n                        figure.data.fieldMatching = this.getChartFieldMatch(figure.id);\n                        figure.data.searchParams.domain = new Domain(\n                            figure.data.searchParams.domain\n                        ).toJson();\n                    }\n                }\n            }\n        }\n    }\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n\n    /**\n     * Get the current odooChartFieldMatching of a chart\n     *\n     * @param {string} chartId\n     * @param {string} filterId\n     */\n    getOdooChartFieldMatching(chartId, filterId) {\n        return this.charts[chartId].fieldMatching[filterId];\n    }\n\n    /**\n     * Sets the current odooChartFieldMatching of a chart\n     *\n     * @param {string} filterId\n     * @param {Record<string,FieldMatching>} chartFieldMatches\n     */\n    _setOdooChartFieldMatching(filterId, chartFieldMatches) {\n        const charts = { ...this.charts };\n        for (const [chartId, fieldMatch] of Object.entries(chartFieldMatches)) {\n            charts[chartId].fieldMatching[filterId] = fieldMatch;\n        }\n        this.history.update(\"charts\", charts);\n    }\n\n    _onFilterDeletion(filterId) {\n        const charts = { ...this.charts };\n        for (const chartId in charts) {\n            this.history.update(\"charts\", chartId, \"fieldMatching\", filterId, undefined);\n        }\n    }\n\n    /**\n     * @param {string} chartId\n     * @param {Object} fieldMatching\n     */\n    _addOdooChart(chartId, fieldMatching = undefined) {\n        const charts = { ...this.charts };\n        if (!fieldMatching) {\n            const model = this.getters.getChartDefinition(chartId).metaData.resModel;\n            fieldMatching = this.getters.getFieldMatchingForModel(model);\n        }\n        charts[chartId] = {\n            fieldMatching,\n        };\n        this.history.update(\"charts\", charts);\n    }\n}\n", "/** @odoo-module */\n\nimport { Domain } from \"@web/core/domain\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { ChartDataSource } from \"../data_source/chart_data_source\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { OdooUIPlugin } from \"@spreadsheet/plugins\";\nimport { navigateTo } from \"../../actions/helpers\";\n\nexport class OdooChartUIPlugin extends OdooUIPlugin {\n    static getters = /** @type {const} */ ([\n        \"getChartDataSource\",\n        \"getChartDatasetActionCallbacks\",\n    ]);\n\n    shouldChartUpdateReloadDataSource = false;\n\n    constructor(config) {\n        super(config);\n\n        this.custom = config.custom;\n\n        /** @type {Record<string, ChartDataSource>} */\n        this.charts = {};\n\n        globalFiltersFieldMatchers[\"chart\"] = {\n            ...globalFiltersFieldMatchers[\"chart\"],\n            getTag: async (chartId) => {\n                const model = await this.getChartDataSource(chartId).getModelLabel();\n                return sprintf(_t(\"Chart - %s\"), model);\n            },\n            waitForReady: () => this._getOdooChartsWaitForReady(),\n            getFields: (chartId) => this.getChartDataSource(chartId).getFields(),\n        };\n    }\n\n    beforeHandle(cmd) {\n        switch (cmd.type) {\n            case \"START\":\n                for (const chartId of this.getters.getOdooChartIds()) {\n                    this._setupChartDataSource(chartId);\n                }\n\n                // make sure the domains are correctly set before\n                // any evaluation\n                this._addDomains();\n                break;\n            case \"UPDATE_CHART\": {\n                switch (cmd.definition.type) {\n                    case \"odoo_pie\":\n                    case \"odoo_bar\":\n                    case \"odoo_line\": {\n                        const dataSource = this.getChartDataSource(cmd.id);\n                        const chart = this.getters.getChart(cmd.id);\n                        if (\n                            cmd.definition.type !== chart.type ||\n                            dataSource.getInitialDomainString() !==\n                                new Domain(cmd.definition.searchParams.domain).toString()\n                        ) {\n                            this.shouldChartUpdateReloadDataSource = true;\n                        }\n                        break;\n                    }\n                }\n                break;\n            }\n        }\n    }\n\n    /**\n     * Handle a spreadsheet command\n     *\n     * @param {Object} cmd Command\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"CREATE_CHART\": {\n                switch (cmd.definition.type) {\n                    case \"odoo_pie\":\n                    case \"odoo_bar\":\n                    case \"odoo_line\":\n                        this._setupChartDataSource(cmd.id);\n                        break;\n                }\n                break;\n            }\n            case \"UPDATE_CHART\": {\n                switch (cmd.definition.type) {\n                    case \"odoo_pie\":\n                    case \"odoo_bar\":\n                    case \"odoo_line\": {\n                        if (this.shouldChartUpdateReloadDataSource) {\n                            this._resetChartDataSource(cmd.id);\n                            this.shouldChartUpdateReloadDataSource = false;\n                        }\n                        this._setChartDataSource(cmd.id);\n                        break;\n                    }\n                }\n                break;\n            }\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n            case \"REMOVE_GLOBAL_FILTER\":\n            case \"SET_GLOBAL_FILTER_VALUE\":\n            case \"CLEAR_GLOBAL_FILTER_VALUE\":\n                this._addDomains();\n                break;\n            case \"UNDO\":\n            case \"REDO\": {\n                if (\n                    cmd.commands.find((command) =>\n                        [\n                            \"ADD_GLOBAL_FILTER\",\n                            \"EDIT_GLOBAL_FILTER\",\n                            \"REMOVE_GLOBAL_FILTER\",\n                        ].includes(command.type)\n                    )\n                ) {\n                    this._addDomains();\n                }\n\n                const domainEditionCommands = cmd.commands.filter(\n                    (cmd) => cmd.type === \"UPDATE_CHART\" || cmd.type === \"CREATE_CHART\"\n                );\n                for (const cmd of domainEditionCommands) {\n                    if (!this.getters.getOdooChartIds().includes(cmd.id)) {\n                        continue;\n                    }\n                    const dataSource = this.getChartDataSource(cmd.id);\n                    if (\n                        dataSource.getInitialDomainString() !==\n                        new Domain(cmd.definition.searchParams.domain).toString()\n                    ) {\n                        this._resetChartDataSource(cmd.id);\n                    }\n                }\n                break;\n            }\n            case \"REFRESH_ALL_DATA_SOURCES\":\n                this._refreshOdooCharts();\n                break;\n        }\n    }\n\n    /**\n     * @param {string} chartId\n     * @returns {ChartDataSource|undefined}\n     */\n    getChartDataSource(chartId) {\n        const dataSourceId = this._getOdooChartDataSourceId(chartId);\n        return this.charts[dataSourceId];\n    }\n\n    /**\n     * Get the callback used for onClick and onHover in an Odoo Chart\n     */\n    getChartDatasetActionCallbacks(chart) {\n        const { datasets, labels } = chart.dataSource.getData();\n        const env = this.custom.env;\n        return {\n            onClick: async (event, items) => {\n                if (!items.length) {\n                    return;\n                }\n                if (!env) {\n                    return;\n                }\n                const { datasetIndex, index } = items[0];\n                const dataset = datasets[datasetIndex];\n                let name = labels[index];\n                if (dataset.label) {\n                    name += ` / ${dataset.label}`;\n                }\n                await navigateTo(\n                    env,\n                    chart.actionXmlId,\n                    {\n                        name,\n                        type: \"ir.actions.act_window\",\n                        res_model: chart.metaData.resModel,\n                        views: [\n                            [false, \"list\"],\n                            [false, \"form\"],\n                        ],\n                        domain: dataset.domains[index],\n                    },\n                    { viewType: \"list\" }\n                );\n            },\n            onHover: (event, items) => {\n                if (items.length > 0) {\n                    event.native.target.style.cursor = \"pointer\";\n                } else {\n                    event.native.target.style.cursor = \"\";\n                }\n            },\n        };\n    }\n\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n\n    /**\n     * Add an additional domain to a chart\n     *\n     * @private\n     *\n     * @param {string} chartId chart id\n     */\n    _addDomain(chartId) {\n        const domainList = [];\n        for (const [filterId, fieldMatch] of Object.entries(\n            this.getters.getChartFieldMatch(chartId)\n        )) {\n            domainList.push(this.getters.getGlobalFilterDomain(filterId, fieldMatch));\n        }\n        const domain = Domain.combine(domainList, \"AND\").toString();\n        this.getChartDataSource(chartId).addDomain(domain);\n    }\n\n    /**\n     * Add an additional domain to all chart\n     *\n     * @private\n     *\n     */\n    _addDomains() {\n        for (const chartId of this.getters.getOdooChartIds()) {\n            this._addDomain(chartId);\n        }\n    }\n\n    /**\n     * @param {string} chartId\n     * @param {string} dataSourceId\n     */\n    _setupChartDataSource(chartId) {\n        const dataSourceId = this._getOdooChartDataSourceId(chartId);\n        if (!(dataSourceId in this.charts)) {\n            this._resetChartDataSource(chartId);\n        }\n        this._setChartDataSource(chartId);\n    }\n\n    /**\n     * Sets the datasource on the corresponding chart\n     * @param {string} chartId\n     */\n    _resetChartDataSource(chartId) {\n        const definition = this.getters.getChart(chartId).getDefinitionForDataSource();\n        const dataSourceId = this._getOdooChartDataSourceId(chartId);\n        this.charts[dataSourceId] = new ChartDataSource(this.custom, definition);\n    }\n\n    /**\n     * Sets the datasource on the corresponding chart\n     * @param {string} chartId\n     */\n    _setChartDataSource(chartId) {\n        const chart = this.getters.getChart(chartId);\n        chart.setDataSource(this.getChartDataSource(chartId));\n    }\n\n    /**\n     *\n     * @return {Promise[]}\n     */\n    _getOdooChartsWaitForReady() {\n        return this.getters\n            .getOdooChartIds()\n            .map((chartId) => this.getChartDataSource(chartId).loadMetadata());\n    }\n\n    _getOdooChartDataSourceId(chartId) {\n        return `chart-${chartId}`;\n    }\n\n    /**\n     * Refresh the cache of a chart\n     * @param {string} chartId Id of the chart\n     */\n    _refreshOdooChart(chartId) {\n        this.getChartDataSource(chartId).load({ reload: true });\n    }\n\n    /**\n     * Refresh the cache of all the charts\n     */\n    _refreshOdooCharts() {\n        for (const chartId of this.getters.getOdooChartIds()) {\n            this._refreshOdooChart(chartId);\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nconst { inverseCommandRegistry, otRegistry } = spreadsheet.registries;\n\nfunction identity(cmd) {\n    return [cmd];\n}\n\notRegistry.addTransformation(\n    \"DELETE_FIGURE\",\n    [\"LINK_ODOO_MENU_TO_CHART\"],\n    (toTransform, executed) => {\n        if (executed.id === toTransform.chartId) {\n            return undefined;\n        }\n        return toTransform;\n    }\n);\n\ninverseCommandRegistry.add(\"LINK_ODOO_MENU_TO_CHART\", identity);\n", "/** @odoo-module **/\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { CopyButton } from \"@web/core/copy_button/copy_button\";\nimport { waitForDataLoaded, freezeOdooData } from \"@spreadsheet/helpers/model\";\nimport { Model } from \"@odoo/o-spreadsheet\";\n\n/**\n * Share button to share a spreadsheet\n */\nexport class SpreadsheetShareButton extends Component {\n    static template = \"spreadsheet.ShareButton\";\n    static components = { Dropdown, DropdownItem, CopyButton };\n    static props = {\n        model: { type: Model, optional: true },\n        onSpreadsheetShared: Function,\n        togglerClass: { type: String, optional: true },\n    };\n\n    setup() {\n        this.copiedText = _t(\"Copied\");\n        this.state = useState({ url: undefined });\n    }\n\n    get togglerClass() {\n        return [\"btn\", this.props.togglerClass].join(\" \");\n    }\n\n    async onOpened() {\n        const model = this.props.model;\n        await waitForDataLoaded(model);\n        const data = await freezeOdooData(model);\n        if (!this.isChanged(data)) {\n            return;\n        }\n        const url = await this.props.onSpreadsheetShared(data, model.exportXLSX());\n        this.state.url = url;\n        setTimeout(async () => {\n            try {\n                await browser.navigator.clipboard.writeText(url);\n            } catch (error) {\n                browser.console.warn(error);\n            }\n        });\n    }\n\n    /**\n     * Check whether the locale/global filters/contents have changed\n     * compared to the last time of sharing (in the same session)\n     */\n    isChanged(data) {\n        const contentsChanged = data.revisionId !== this.lastRevisionId;\n        let globalFilterChanged = this.lastGlobalFilters === undefined;\n        const newCells = data.sheets[data.sheets.length - 1].cells;\n        if (this.lastGlobalFilters !== undefined) {\n            for (const key of Object.keys(newCells)) {\n                if (this.lastGlobalFilters[key]?.content !== newCells[key].content) {\n                    globalFilterChanged = true;\n                    break;\n                }\n            }\n        }\n        const localeChanged = data.settings.locale.code !== this.lastLocale;\n        if (!(localeChanged || globalFilterChanged || contentsChanged)) {\n            return false;\n        }\n\n        this.lastRevisionId = data.revisionId;\n        this.lastGlobalFilters = newCells;\n        this.lastLocale = data.settings.locale.code;\n        return true;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nconst { arg, toString, toJsDate, toNumber } = spreadsheet.helpers;\nconst { functionRegistry } = spreadsheet.registries;\n\nfunctionRegistry.add(\"ODOO.CURRENCY.RATE\", {\n    description: _t(\n        \"This function takes in two currency codes as arguments, and returns the exchange rate from the first currency to the second as float.\"\n    ),\n    category: \"Odoo\",\n    compute: function (currencyFrom, currencyTo, date, companyId) {\n        const from = toString(currencyFrom);\n        const to = toString(currencyTo);\n        const _date = date ? toJsDate(date, this.locale) : undefined;\n        const _companyId = companyId ? toNumber(companyId) : undefined;\n        return this.getters.getCurrencyRate(from, to, _date, _companyId);\n    },\n    args: [\n        arg(\"currency_from (string)\", _t(\"First currency code.\")),\n        arg(\"currency_to (string)\", _t(\"Second currency code.\")),\n        arg(\"date (date, optional)\", _t(\"Date of the rate.\")),\n        arg(\"company_id (number, optional)\", _t(\"The company to take the exchange rate from.\")),\n    ],\n    returns: [\"NUMBER\"],\n});\n", "/**\n * Return the currency cleaned from useless info and from the `code` field to be used to generate\n * a default currency format.\n *\n * @param {object} currency\n * @returns {object}\n */\nexport function createDefaultCurrency(currency) {\n    if (!currency) {\n        return undefined;\n    }\n    return {\n        symbol: currency.symbol,\n        position: currency.position,\n        decimalPlaces: currency.decimalPlaces,\n    };\n}\n", "/** @odoo-module */\n\nimport { EvaluationError, helpers, registries } from \"@odoo/o-spreadsheet\";\nimport { OdooUIPlugin } from \"@spreadsheet/plugins\";\nimport { toServerDateString } from \"@spreadsheet/helpers/helpers\";\nimport { _t } from \"@web/core/l10n/translation\";\nconst { featurePluginRegistry } = registries;\nconst { createCurrencyFormat } = helpers;\n\n/**\n * @typedef Currency\n * @property {string} name\n * @property {string} code\n * @property {string} symbol\n * @property {number} decimalPlaces\n * @property {\"before\" | \"after\"} position\n */\n\nexport class CurrencyPlugin extends OdooUIPlugin {\n    static getters = /** @type {const} */ ([\n        \"getCurrencyRate\",\n        \"computeFormatFromCurrency\",\n        \"getCompanyCurrencyFormat\",\n    ]);\n\n    constructor(config) {\n        super(config);\n        /** @type {string | undefined} */\n        this.currentCompanyCurrency = config.defaultCurrency;\n        /** @type {import(\"@spreadsheet/data_sources/server_data\").ServerData} */\n        this._serverData = config.custom.odooDataProvider?.serverData;\n    }\n\n    get serverData() {\n        if (!this._serverData) {\n            throw new Error(\n                \"'serverData' is not defined, please make sure a 'OdooDataProvider' instance is provided to the model.\"\n            );\n        }\n        return this._serverData;\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    /**\n     * Get the currency rate between the two given currencies\n     * @param {string} from Currency from\n     * @param {string} to Currency to\n     * @param {string | undefined} date\n     * @param {number | undefined} companyId\n     * @returns {number|string}\n     */\n    getCurrencyRate(from, to, date, companyId) {\n        const data = this.serverData.batch.get(\"res.currency.rate\", \"get_rates_for_spreadsheet\", {\n            from,\n            to,\n            date: date ? toServerDateString(date) : undefined,\n            company_id: companyId,\n        });\n        const rate = data !== undefined ? data.rate : undefined;\n        if (rate === false) {\n            throw new EvaluationError(_t(\"Currency rate unavailable.\"));\n        }\n        return rate;\n    }\n\n    /**\n     * @param {Currency | undefined} currency\n     * @returns {string | undefined}\n     */\n    computeFormatFromCurrency(currency) {\n        if (!currency) {\n            return undefined;\n        }\n        return createCurrencyFormat({\n            symbol: currency.symbol,\n            position: currency.position,\n            decimalPlaces: currency.decimalPlaces,\n        });\n    }\n\n    /**\n     * Returns the default display format of a the company currency\n     * @param {number} [companyId]\n     * @returns {string | undefined}\n     */\n    getCompanyCurrencyFormat(companyId) {\n        if (!companyId && this.currentCompanyCurrency) {\n            return this.computeFormatFromCurrency(this.currentCompanyCurrency);\n        }\n        const currency = this.serverData.get(\n            \"res.currency\",\n            \"get_company_currency_for_spreadsheet\",\n            [companyId]\n        );\n        if (currency === false) {\n            throw new EvaluationError(_t(\"Currency not available for this company.\"));\n        }\n        return this.computeFormatFromCurrency(currency);\n    }\n}\n\nfeaturePluginRegistry.add(\"odooCurrency\", CurrencyPlugin);\n", "/** @odoo-module */\n// @ts-check\n\nimport { LoadingDataError } from \"@spreadsheet/o_spreadsheet/errors\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { CellErrorType, EvaluationError } from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {import(\"./odoo_data_provider\").OdooDataProvider} OdooDataProvider\n * @typedef {import(\"./server_data\").ServerData} ServerData\n */\n\n/**\n * DataSource is an abstract class that contains the logic of fetching and\n * maintaining access to data that have to be loaded.\n *\n * A class which extends this class have to implement the `_load` method\n * * which should load the data it needs\n *\n * Subclass can implement concrete methods to have access to a\n * particular data.\n */\nexport class LoadableDataSource {\n    /**\n     * @param {Object} param0\n     * @param {OdooDataProvider} param0.odooDataProvider\n     */\n    constructor({ odooDataProvider }) {\n        /** @protected */\n        this.odooDataProvider = odooDataProvider;\n\n        /**\n         * Last time that this dataSource has been updated\n         */\n        this._lastUpdate = undefined;\n\n        this._concurrency = new KeepLast();\n        /**\n         * Promise to control the loading of data\n         */\n        this._loadPromise = undefined;\n        this._isFullyLoaded = false;\n        this._isValid = true;\n        this._loadError = undefined;\n        this._isModelValid = true;\n    }\n\n    get _orm() {\n        return this.odooDataProvider.orm;\n    }\n\n    get serverData() {\n        return this.odooDataProvider.serverData;\n    }\n\n    /**\n     * Load data in the model\n     * @param {object} [params] Params for fetching data\n     * @param {boolean} [params.reload=false] Force the reload of the data\n     *\n     * @returns {Promise} Resolved when data are fetched.\n     */\n    async load(params) {\n        if (params && params.reload) {\n            this.odooDataProvider.cancelPromise(this._loadPromise);\n            this._loadPromise = undefined;\n        }\n        if (!this._loadPromise) {\n            this._isFullyLoaded = false;\n            this._isValid = true;\n            this._loadError = undefined;\n            this._loadPromise = this._concurrency\n                .add(this._load())\n                .catch((e) => {\n                    this._isValid = false;\n                    if (e instanceof ModelNotFoundError) {\n                        this._isModelValid = false;\n                        this._loadError = Object.assign(\n                            new EvaluationError(\n                                _t(`The model \"%(model)s\" does not exist.`, { model: e.message })\n                            ),\n                            {\n                                cause: e,\n                            }\n                        );\n                        return;\n                    }\n                    this._loadError = Object.assign(\n                        new EvaluationError(e instanceof RPCError ? e.data.message : e.message),\n                        { cause: e }\n                    );\n                })\n                .finally(() => {\n                    this._lastUpdate = Date.now();\n                    this._isFullyLoaded = true;\n                });\n            await this.odooDataProvider.notifyWhenPromiseResolves(this._loadPromise);\n        }\n        return this._loadPromise;\n    }\n\n    get lastUpdate() {\n        return this._lastUpdate;\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    isReady() {\n        return this._isFullyLoaded;\n    }\n\n    isLoading() {\n        return !!this._loadPromise && !this.isReady();\n    }\n\n    isValid() {\n        return this.isReady() && this._isValid;\n    }\n\n    isModelValid() {\n        return this.isReady() && this._isModelValid;\n    }\n\n    assertIsValid({ throwOnError } = { throwOnError: true }) {\n        if (!this._isFullyLoaded) {\n            this.load();\n            if (throwOnError) {\n                throw LOADING_ERROR;\n            }\n            return LOADING_ERROR;\n        }\n        if (!this._isValid) {\n            if (throwOnError) {\n                throw this._loadError;\n            }\n            return { value: CellErrorType.GenericError, message: this._loadError.message };\n        }\n    }\n\n    /**\n     * Load the data in the model\n     *\n     * @abstract\n     * @protected\n     */\n    async _load() {}\n}\n\nexport const LOADING_ERROR = new LoadingDataError();\n\nexport class ModelNotFoundError extends Error {}\n\n/**\n * Perform a `fields_get` on the given model and return the fields.\n * If the model is not found, a `ModelNotFoundError` is thrown.\n *\n * @param {ServerData} serverData\n * @param {string} model\n * @returns {Promise<import(\"@spreadsheet\").OdooFields>}\n */\nexport async function getFields(serverData, model) {\n    try {\n        const fields = await serverData.fetch(model, \"fields_get\");\n        return fields;\n    } catch (e) {\n        if (e instanceof RPCError && e.code === 404) {\n            throw new ModelNotFoundError(model);\n        }\n        throw e;\n    }\n}\n", "import { EventBus } from \"@odoo/owl\";\nimport { ServerData } from \"./server_data\";\n\nexport class OdooDataProvider extends EventBus {\n    constructor(env) {\n        super();\n        this.orm = env.services.orm.silent;\n        this.serverData = new ServerData(this.orm, {\n            whenDataStartLoading: (promise) => this.notifyWhenPromiseResolves(promise),\n        });\n        this.pendingPromises = new Set();\n    }\n\n    cancelPromise(promise) {\n        this.pendingPromises.delete(promise);\n    }\n\n    /**\n     * @param {Promise<unknown>} promise\n     */\n    async notifyWhenPromiseResolves(promise) {\n        this.pendingPromises.add(promise);\n        await promise\n            .then(() => {\n                this.pendingPromises.delete(promise);\n                this.notify();\n            })\n            .catch(() => {\n                this.pendingPromises.delete(promise);\n                this.notify();\n            });\n    }\n\n    /**\n     * Notify that a data source has been updated. Could be useful to\n     * request a re-evaluation.\n     */\n    notify() {\n        if (this.pendingPromises.size) {\n            if (!this.nextTriggerTimeOutId) {\n                // evaluates at least every 10 seconds, even if there are pending promises\n                // to avoid blocking everything if there is a really long request\n                this.nextTriggerTimeOutId = setTimeout(() => {\n                    this.nextTriggerTimeOutId = undefined;\n                    if (this.pendingPromises.size) {\n                        this.trigger(\"data-source-updated\");\n                    }\n                }, 10000);\n            }\n            return;\n        }\n        this.trigger(\"data-source-updated\");\n    }\n}\n", "/** @odoo-module */\n// @ts-check\n\nimport { LOADING_ERROR, LoadableDataSource, getFields } from \"./data_source\";\nimport { Domain } from \"@web/core/domain\";\nimport { user } from \"@web/core/user\";\nimport { omit } from \"@web/core/utils/objects\";\n\n/**\n * @typedef {import(\"@spreadsheet\").OdooField} OdooField\n * @typedef {import(\"@spreadsheet\").OdooFields} OdooFields\n */\n\n/**\n * @typedef {Object} OdooModelMetaData\n * @property {string} resModel\n * @property {OdooFields} [fields]\n *\n * @typedef {Object} OdooModelSearchParams\n * @property {Object} context\n * @property {Array<string>} domain\n */\n\nexport class OdooViewsDataSource extends LoadableDataSource {\n    /**\n     * @override\n     * @param {Object} services\n     * @param {Object} params\n     * @param {OdooModelMetaData} params.metaData\n     * @param {Object} params.searchParams\n     */\n    constructor(services, params) {\n        super(services);\n        /** @type {OdooModelMetaData} */\n        this._metaData = JSON.parse(JSON.stringify(params.metaData));\n        /** @protected */\n        this._initialSearchParams = JSON.parse(JSON.stringify(params.searchParams));\n        const userContext = user.context;\n        this._initialSearchParams.context = omit(\n            this._initialSearchParams.context || {},\n            ...Object.keys(userContext)\n        );\n        /** @private */\n        this._customDomain = this._initialSearchParams.domain;\n        this._metaDataLoaded = false;\n    }\n\n    /**\n     * @protected\n     */\n    get _searchParams() {\n        return {\n            ...this._initialSearchParams,\n            domain: this.getComputedDomain(),\n        };\n    }\n\n    async loadMetadata() {\n        if (!this._metaData.fields) {\n            this._metaData.fields = await getFields(this.serverData, this._metaData.resModel);\n        }\n        this._metaDataLoaded = true;\n    }\n\n    /**\n     * Ensure that the metadata are loaded. If not, throw an error\n     */\n    _assertMetaDataLoaded() {\n        if (!this._isModelValid) {\n            throw this.loadError;\n        }\n        if (!this._metaDataLoaded) {\n            this.loadMetadata();\n            throw LOADING_ERROR;\n        }\n    }\n\n    /**\n     * @returns {OdooFields} List of fields\n     */\n    getFields() {\n        this._assertMetaDataLoaded();\n        return this._metaData.fields;\n    }\n\n    /**\n     * @param {string} field Field name\n     * @returns {OdooField | undefined} Field\n     */\n    getField(field) {\n        this._assertMetaDataLoaded();\n        return this._metaData.fields[field];\n    }\n\n    /**\n     * @protected\n     */\n    async _load() {\n        await this.loadMetadata();\n    }\n\n    isMetaDataLoaded() {\n        return this._metaData.fields !== undefined;\n    }\n\n    /**\n     * Get the computed domain of this source\n     * @returns {Array}\n     */\n    getComputedDomain() {\n        const userContext = user.context;\n        return new Domain(this._customDomain).toList({\n            ...this._initialSearchParams.context,\n            ...userContext,\n        });\n    }\n\n    /**\n     * Get the current domain as a string\n     * @returns { string }\n     */\n    getInitialDomainString() {\n        return new Domain(this._initialSearchParams.domain).toString();\n    }\n\n    /**\n     *\n     * @param {string} domain\n     */\n    addDomain(domain) {\n        const newDomain = Domain.and([this._initialSearchParams.domain, domain]).toString();\n        if (newDomain.toString() === new Domain(this._customDomain).toString()) {\n            return;\n        }\n        this._customDomain = newDomain;\n        if (this._loadPromise === undefined) {\n            // if the data source has never been loaded, there's no point\n            // at reloading it now.\n            return;\n        }\n        this.load({ reload: true });\n    }\n\n    /**\n     * @returns {Promise<string>} Display name of the model\n     */\n    async getModelLabel() {\n        const model = this._metaData.resModel;\n        const result = await this.serverData.fetch(\"ir.model\", \"display_name_for\", [[model]]);\n        return (result[0] && result[0].display_name) || \"\";\n    }\n}\n", "/** @odoo-module */\n// @ts-check\n\nimport { EvaluationError } from \"@odoo/o-spreadsheet\";\nimport { LoadingDataError, isLoadingError } from \"../o_spreadsheet/errors\";\n\n/**\n * @param {T[]} array\n * @returns {T[]}\n * @template T\n */\nfunction removeDuplicates(array) {\n    return [...new Set(array.map((el) => JSON.stringify(el)))].map((el) => JSON.parse(el));\n}\n\nexport class Request {\n    /**\n     * @param {string} resModel\n     * @param {string} method\n     * @param {unknown[]} args\n     */\n    constructor(resModel, method, args) {\n        this.resModel = resModel;\n        this.method = method;\n        this.args = args;\n        this.key = `${resModel}/${method}(${JSON.stringify(args)})`;\n    }\n}\n\n/**\n * A batch request consists of multiple requests which are combined into a single RPC.\n *\n * The batch responsibility is to combine individual requests into a single RPC payload\n * and to split the response back for individual requests.\n *\n * The server method must have the following API:\n * - The input is a list of arguments. Each list item being the arguments of a single request.\n * - The output is a list of results, ordered according to the input list\n *\n *  ```\n *  [result1, result2] = self.env['my.model'].my_batched_method([request_1_args, request_2_args])\n *  ```\n */\nclass ListRequestBatch {\n    /**\n     * @param {string} resModel\n     * @param {string} method\n     * @param {Request[]} requests\n     */\n    constructor(resModel, method, requests = []) {\n        this.resModel = resModel;\n        this.method = method;\n        this.requests = requests;\n    }\n\n    get payload() {\n        const payload = removeDuplicates(this.requests.map((request) => request.args).flat());\n        return [payload];\n    }\n\n    /**\n     * @param {Request} request\n     */\n    add(request) {\n        if (request.resModel !== this.resModel || request.method !== this.method) {\n            throw new Error(\n                `Request ${request.resModel}/${request.method} cannot be added to the batch ${this.resModel}/${this.method}`\n            );\n        }\n        this.requests.push(request);\n    }\n\n    /**\n     * Split the batched RPC response into single request results\n     *\n     * @param {T[]} results\n     * @returns {Map<Request, T>}\n     * @template T\n     */\n    splitResponse(results) {\n        const split = new Map();\n        for (let i = 0; i < this.requests.length; i++) {\n            split.set(this.requests[i], results[i]);\n        }\n        return split;\n    }\n}\n\nexport class ServerData {\n    /**\n     * @param {import(\"@web/core/orm_service\").ORM} orm\n     * @param {object} params\n     * @param {(promise: Promise<any>) => void} [params.whenDataStartLoading]\n     */\n    constructor(orm, { whenDataStartLoading }) {\n        /** @type {import(\"@web/core/orm_service\").ORM} */\n        this.orm = orm;\n        /** @type {(promise: Promise<any>) => void} */\n        this.startLoadingCallback = whenDataStartLoading ?? (() => {});\n        /** @type {Record<string, unknown>}*/\n        this.cache = {};\n        /** @type {Record<string, Promise<unknown>>}*/\n        this.asyncCache = {};\n        this.batchEndpoints = {};\n    }\n\n    /**\n     * @returns {{get: (resModel:string, method: string, args: unknown) => any}}\n     */\n    get batch() {\n        return { get: (resModel, method, args) => this._getBatchItem(resModel, method, args) };\n    }\n\n    /**\n     * @private\n     * @param {string} resModel\n     * @param {string} method\n     * @param  {unknown} args\n     * @returns {any}\n     */\n    _getBatchItem(resModel, method, args) {\n        const request = new Request(resModel, method, [args]);\n        if (!(request.key in this.cache)) {\n            const error = new LoadingDataError();\n            this.cache[request.key] = error;\n            this._batch(request);\n            throw error;\n        }\n        return this._getOrThrowCachedResponse(request);\n    }\n\n    /**\n     * @param {string} resModel\n     * @param {string} method\n     * @param  {unknown[]} args\n     * @returns {any}}\n     */\n    get(resModel, method, args) {\n        const request = new Request(resModel, method, args);\n        if (!(request.key in this.cache)) {\n            const error = new LoadingDataError();\n            this.cache[request.key] = error;\n            const promise = this.orm\n                .call(resModel, method, args)\n                .then((result) => (this.cache[request.key] = result))\n                .catch(\n                    (error) =>\n                        (this.cache[request.key] = new EvaluationError(\n                            error.data?.message || error.message\n                        ))\n                );\n            this.startLoadingCallback(promise);\n            throw error;\n        }\n        return this._getOrThrowCachedResponse(request);\n    }\n\n    /**\n     * Returns the request result if cached or the associated promise\n     * @param {string} resModel\n     * @param {string} method\n     * @param  {unknown[]} [args]\n     * @returns {Promise<any>}\n     */\n    async fetch(resModel, method, args) {\n        const request = new Request(resModel, method, args);\n        if (!(request.key in this.asyncCache)) {\n            this.asyncCache[request.key] = this.orm.call(resModel, method, args);\n        }\n        return this.asyncCache[request.key];\n    }\n\n    /**\n     * @private\n     * @param {Request} request\n     * @returns {void}\n     */\n    _batch(request) {\n        const endpoint = this._getBatchEndPoint(request.resModel, request.method);\n        endpoint.call(request);\n    }\n\n    /**\n     * @private\n     * @param {Request} request\n     * @return {unknown}\n     */\n    _getOrThrowCachedResponse(request) {\n        const data = this.cache[request.key];\n        if (data instanceof Error || isLoadingError({ value: data })) {\n            throw data;\n        }\n        return data;\n    }\n\n    /**\n     * @private\n     * @param {string} resModel\n     * @param {string} method\n     */\n    _getBatchEndPoint(resModel, method) {\n        if (!this.batchEndpoints[resModel] || !this.batchEndpoints[resModel][method]) {\n            this.batchEndpoints[resModel] = {\n                ...this.batchEndpoints[resModel],\n                [method]: this._createBatchEndpoint(resModel, method),\n            };\n        }\n        return this.batchEndpoints[resModel][method];\n    }\n\n    /**\n     * @private\n     * @param {string} resModel\n     * @param {string} method\n     */\n    _createBatchEndpoint(resModel, method) {\n        return new BatchEndpoint(this.orm, resModel, method, {\n            whenDataStartLoading: (promise) => this.startLoadingCallback(promise),\n            successCallback: (request, result) => {\n                this.cache[request.key] = result;\n            },\n            failureCallback: (request, error) =>\n                (this.cache[request.key] = new EvaluationError(\n                    error.data?.message || error.message\n                )),\n        });\n    }\n}\n\n/**\n * Collect multiple requests into a single batch.\n */\nexport class BatchEndpoint {\n    /**\n     * @param {object} orm\n     * @param {string} resModel\n     * @param {string} method\n     * @param {object} callbacks\n     * @param {function} callbacks.successCallback\n     * @param {function} callbacks.failureCallback\n     * @param {(promise: Promise<any>) => void} callbacks.whenDataStartLoading\n     */\n    constructor(orm, resModel, method, { successCallback, failureCallback, whenDataStartLoading }) {\n        this.orm = orm;\n        this.resModel = resModel;\n        this.method = method;\n        this.successCallback = successCallback;\n        this.failureCallback = failureCallback;\n        this.batchStartsLoadingCallback = whenDataStartLoading;\n\n        this._isScheduled = false;\n        this._pendingBatch = new ListRequestBatch(resModel, method);\n    }\n\n    /**\n     * @param {Request} request\n     */\n    call(request) {\n        this._pendingBatch.add(request);\n        this._scheduleNextBatch();\n    }\n\n    /**\n     * @param {Map} batchResult\n     * @private\n     */\n    _notifyResults(batchResult) {\n        for (const [request, result] of batchResult) {\n            if (result instanceof Error) {\n                this.failureCallback(request, result);\n            } else {\n                this.successCallback(request, result);\n            }\n        }\n    }\n\n    /**\n     * @private\n     */\n    _scheduleNextBatch() {\n        if (this._isScheduled || this._pendingBatch.requests.length === 0) {\n            return;\n        }\n        this._isScheduled = true;\n        queueMicrotask(async () => {\n            this._isScheduled = false;\n            const batch = this._pendingBatch;\n            const { resModel, method } = batch;\n            this._pendingBatch = new ListRequestBatch(resModel, method);\n            const promise = this.orm\n                .call(resModel, method, batch.payload)\n                .then((result) => batch.splitResponse(result))\n                .catch(() => this._retryOneByOne(batch))\n                .then((batchResults) => this._notifyResults(batchResults));\n            this.batchStartsLoadingCallback(promise);\n        });\n    }\n\n    /**\n     * @private\n     * @param {ListRequestBatch} batch\n     * @returns {Promise<Map<Request, unknown>>}\n     */\n    async _retryOneByOne(batch) {\n        const mergedResults = new Map();\n        const { resModel, method } = batch;\n        const singleRequestBatches = batch.requests.map(\n            (request) => new ListRequestBatch(resModel, method, [request])\n        );\n        const proms = [];\n        for (const batch of singleRequestBatches) {\n            const request = batch.requests[0];\n            const prom = this.orm\n                .call(resModel, method, batch.payload)\n                .then((result) =>\n                    mergedResults.set(request, batch.splitResponse(result).get(request))\n                )\n                .catch((error) => mergedResults.set(request, error));\n            proms.push(prom);\n        }\n        await Promise.allSettled(proms);\n        return mergedResults;\n    }\n}\n", "/** @ts-check */\n\nimport { Component } from \"@odoo/owl\";\nimport { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport { serializeDate, deserializeDate } from \"@web/core/l10n/dates\";\n\nexport class DateFromToValue extends Component {\n    static template = \"spreadsheet.DateFromToValue\";\n    static components = { DateTimeInput };\n    static props = {\n        onFromToChanged: Function,\n        from: { type: String, optional: true },\n        to: { type: String, optional: true },\n    };\n\n    onDateFromChanged(dateFrom) {\n        this.props.onFromToChanged({\n            from: dateFrom ? serializeDate(dateFrom.startOf(\"day\")) : undefined,\n            to: this.props.to,\n        });\n    }\n\n    onDateToChanged(dateTo) {\n        this.props.onFromToChanged({\n            from: this.props.from,\n            to: dateTo ? serializeDate(dateTo.endOf(\"day\")) : undefined,\n        });\n    }\n\n    get dateFrom() {\n        return this.props.from && deserializeDate(this.props.from);\n    }\n\n    get dateTo() {\n        return this.props.to && deserializeDate(this.props.to);\n    }\n}\n", "/** @ts-check */\n\nimport { Component, onWillUpdateProps } from \"@odoo/owl\";\nimport { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport { monthsOptions } from \"@spreadsheet/assets_backend/constants\";\nimport { QUARTER_OPTIONS } from \"@web/search/utils/dates\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {Object} DateOption\n * @property {string} id\n * @property {string | import(\"@spreadsheet\").LazyTranslatedString} description\n */\n\nexport class DateFilterValue extends Component {\n    static template = \"spreadsheet_edition.DateFilterValue\";\n    static components = { DateTimeInput };\n    static props = {\n        // See @spreadsheet_edition/bundle/global_filters/filters_plugin.RangeType\n        onTimeRangeChanged: Function,\n        yearOffset: { type: Number, optional: true },\n        period: { type: String, optional: true },\n        disabledPeriods: { type: Array, optional: true },\n    };\n    setup() {\n        this._setStateFromProps(this.props);\n        this.dateOptions = this.getDateOptions(this.props);\n        onWillUpdateProps((nextProps) => {\n            this._setStateFromProps(nextProps);\n            this.dateOptions = this.getDateOptions(nextProps);\n        });\n    }\n    _setStateFromProps(props) {\n        this.period = props.period;\n        /** @type {number|undefined} */\n        this.yearOffset = props.yearOffset;\n        // date should be undefined if we don't have the yearOffset\n        /** @type {import(\"@web/core/l10n/dates\").DateTime|undefined} */\n        this.date =\n            this.yearOffset !== undefined\n                ? DateTime.local().plus({ year: this.yearOffset })\n                : undefined;\n    }\n\n    /**\n     * Returns a list of time options to choose from according to the requested\n     * type. Each option contains its (translated) description.\n     *\n     * @returns {Array<Object>}\n     */\n    getDateOptions(props) {\n        const quarterOptions = Object.values(QUARTER_OPTIONS);\n        const disabledPeriods = props.disabledPeriods || [];\n\n        const dateOptions = [];\n        if (!disabledPeriods.includes(\"quarter\")) {\n            dateOptions.push(...quarterOptions);\n        }\n        if (!disabledPeriods.includes(\"month\")) {\n            dateOptions.push(...monthsOptions);\n        }\n        return dateOptions;\n    }\n\n    isSelected(periodId) {\n        return this.period === periodId;\n    }\n\n    /**\n     * @param {Event & { target: HTMLSelectElement }} ev\n     */\n    onPeriodChanged(ev) {\n        this.period = ev.target.value;\n        this._updateFilter();\n    }\n\n    onYearChanged(date) {\n        this.date = date;\n        this.yearOffset = date.year - DateTime.now().year;\n        this._updateFilter();\n    }\n\n    _updateFilter() {\n        this.props.onTimeRangeChanged({\n            yearOffset: this.yearOffset || 0,\n            period: this.period,\n        });\n    }\n}\n", "/** @ts-check */\n\nimport { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class TextFilterValue extends Component {\n    static template = \"spreadsheet.TextFilterValue\";\n    static props = {\n        label: { type: String, optional: true },\n        onValueChanged: Function,\n        value: { type: String, optional: true },\n        options: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: { value: String, formattedValue: String },\n                optional: true,\n            },\n        },\n    };\n\n    translate(label) {\n        // the filter label is extracted from the spreadsheet json file.\n        return _t(label);\n    }\n}\n", "/** @ts-check */\n\nimport { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\nimport { RELATIVE_DATE_RANGE_TYPES } from \"@spreadsheet/helpers/constants\";\nimport { DateFilterValue } from \"../filter_date_value/filter_date_value\";\nimport { DateFromToValue } from \"../filter_date_from_to_value/filter_date_from_to_value\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { components } from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Domain } from \"@web/core/domain\";\nimport { user } from \"@web/core/user\";\nimport { TextFilterValue } from \"../filter_text_value/filter_text_value\";\nimport { getFields, ModelNotFoundError } from \"@spreadsheet/data_sources/data_source\";\n\nconst { ValidationMessages } = components;\n\nexport class FilterValue extends Component {\n    static template = \"spreadsheet_edition.FilterValue\";\n    static components = {\n        DateFilterValue,\n        DateFromToValue,\n        MultiRecordSelector,\n        TextFilterValue,\n        ValidationMessages,\n    };\n    static props = {\n        filter: Object,\n        model: Object,\n    };\n\n    setup() {\n        this.getters = this.props.model.getters;\n        this.relativeDateRangesTypes = RELATIVE_DATE_RANGE_TYPES;\n        this.nameService = useService(\"name\");\n        this.isValid = false;\n        onWillStart(async () => {\n            if (this.filter.type !== \"relation\") {\n                this.isValid = true;\n                return;\n            }\n            try {\n                const odooDataProvider = this.props.model.config.custom.odooDataProvider;\n                await getFields(odooDataProvider.serverData, this.filter.modelName);\n                this.isValid = true;\n            } catch (e) {\n                if (e instanceof ModelNotFoundError) {\n                    this.isValid = false;\n                } else {\n                    throw e;\n                }\n            }\n        });\n    }\n\n    get filter() {\n        return this.props.filter;\n    }\n\n    get filterValue() {\n        return this.getters.getGlobalFilterValue(this.filter.id);\n    }\n\n    get textAllowedValues() {\n        return this.getters.getTextFilterOptions(this.filter.id);\n    }\n\n    get relationalAllowedDomain() {\n        const domain = this.props.filter.domainOfAllowedValues;\n        if (domain) {\n            return new Domain(domain).toList(user.context);\n        }\n        return [];\n    }\n\n    get invalidModel() {\n        const model = this.filter.modelName;\n        return _t(\n            \"The model (%(model)s) of this global filter is not valid (it may have been renamed/deleted).\",\n            {\n                model,\n            }\n        );\n    }\n\n    onDateInput(id, value) {\n        this.props.model.dispatch(\"SET_GLOBAL_FILTER_VALUE\", { id, value });\n    }\n\n    onTextInput(id, value) {\n        this.props.model.dispatch(\"SET_GLOBAL_FILTER_VALUE\", { id, value });\n    }\n\n    async onTagSelected(id, resIds) {\n        if (!resIds.length) {\n            // force clear, even automatic default values\n            this.clear(id);\n        } else {\n            const displayNames = await this.nameService.loadDisplayNames(\n                this.filter.modelName,\n                resIds\n            );\n            this.props.model.dispatch(\"SET_GLOBAL_FILTER_VALUE\", {\n                id,\n                value: resIds,\n                displayNames: Object.values(displayNames),\n            });\n        }\n    }\n\n    translate(text) {\n        return _t(text);\n    }\n\n    clear(id) {\n        this.props.model.dispatch(\"CLEAR_GLOBAL_FILTER_VALUE\", { id });\n    }\n}\n", "/** @ts-check */\n\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { Domain } from \"@web/core/domain\";\n\nimport { CommandResult } from \"@spreadsheet/o_spreadsheet/cancelled_reason\";\nimport { RELATIVE_DATE_RANGE_TYPES } from \"@spreadsheet/helpers/constants\";\nimport { monthsOptions } from \"@spreadsheet/assets_backend/constants\";\nimport { QUARTER_OPTIONS } from \"@web/search/utils/dates\";\n\n/**\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n */\n\nconst monthsOptionsIds = monthsOptions.map((option) => option.id);\nconst quarterOptionsIds = Object.values(QUARTER_OPTIONS).map((option) => option.id);\n\n/**\n * Check if the value is valid for given filter.\n * @param {GlobalFilter | CmdGlobalFilter} filter\n * @param {any} value\n * @returns {boolean}\n */\nexport function checkFilterValueIsValid(filter, value) {\n    const { type } = filter;\n    if (value !== undefined) {\n        switch (type) {\n            case \"text\":\n                if (typeof value !== \"string\") {\n                    return false;\n                }\n                break;\n            case \"date\": {\n                return checkDateFilterValueIsValid(filter, value);\n            }\n            case \"relation\":\n                if (value === \"current_user\") {\n                    return true;\n                }\n                if (!Array.isArray(value)) {\n                    return false;\n                }\n                break;\n        }\n    }\n    return true;\n}\n\n/**\n * Check if the value is valid for given filter.\n * @param {DateGlobalFilter} filter\n * @param {any} value\n * @returns {boolean}\n */\nfunction checkDateFilterValueIsValid(filter, value) {\n    if (!value) {\n        return true;\n    }\n    switch (filter.rangeType) {\n        case \"fixedPeriod\": {\n            const period = value.period;\n            if (!filter.disabledPeriods || !filter.disabledPeriods.length) {\n                return true;\n            }\n            if (filter.disabledPeriods.includes(\"month\")) {\n                return value !== \"this_month\" && !monthsOptionsIds.includes(period);\n            }\n            if (filter.disabledPeriods.includes(\"quarter\")) {\n                return value !== \"this_quarter\" && !quarterOptionsIds.includes(period);\n            }\n            return true;\n        }\n        case \"relative\": {\n            const expectedValues = RELATIVE_DATE_RANGE_TYPES.map((val) => val.type);\n            expectedValues.push(\"this_month\", \"this_quarter\", \"this_year\");\n            return expectedValues.includes(value);\n        }\n        case \"from_to\":\n            return typeof value === \"object\";\n    }\n    return true;\n}\n\n/**\n *\n * @param {Record<string, FieldMatching>} fieldMatchings\n */\nexport function checkFilterFieldMatching(fieldMatchings) {\n    for (const fieldMatch of Object.values(fieldMatchings)) {\n        if (fieldMatch.offset && (!fieldMatch.chain || !fieldMatch.type)) {\n            return CommandResult.InvalidFieldMatch;\n        }\n    }\n\n    return CommandResult.Success;\n}\n\n/**\n * Get a date domain relative to the current date.\n * The domain will span the amount of time specified in rangeType and end the day before the current day.\n *\n *\n * @param {Object} now current time, as luxon time\n * @param {number} offset offset to add to the date\n * @param {import(\"@spreadsheet\").RelativePeriod} rangeType\n * @param {string} fieldName\n * @param {\"date\" | \"datetime\"} fieldType\n *\n * @returns {Domain|undefined}\n */\nexport function getRelativeDateDomain(now, offset, rangeType, fieldName, fieldType) {\n    const startOfNextDay = now.plus({ days: 1 }).startOf(\"day\");\n    let endDate = now.endOf(\"day\");\n    let startDate = endDate;\n    switch (rangeType) {\n        case \"year_to_date\": {\n            const offsetParam = { years: offset };\n            startDate = now.startOf(\"year\").plus(offsetParam);\n            endDate = now.endOf(\"day\").plus(offsetParam);\n            break;\n        }\n        case \"last_week\": {\n            const offsetParam = { days: 7 * offset };\n            endDate = endDate.plus(offsetParam);\n            startDate = startOfNextDay.minus({ days: 7 }).plus(offsetParam);\n            break;\n        }\n        case \"last_month\": {\n            const offsetParam = { days: 30 * offset };\n            endDate = endDate.plus(offsetParam);\n            startDate = startOfNextDay.minus({ days: 30 }).plus(offsetParam);\n            break;\n        }\n        case \"last_three_months\": {\n            const offsetParam = { days: 90 * offset };\n            endDate = endDate.plus(offsetParam);\n            startDate = startOfNextDay.minus({ days: 90 }).plus(offsetParam);\n            break;\n        }\n        case \"last_six_months\": {\n            const offsetParam = { days: 180 * offset };\n            endDate = endDate.plus(offsetParam);\n            startDate = startOfNextDay.minus({ days: 180 }).plus(offsetParam);\n            break;\n        }\n        case \"last_year\": {\n            const offsetParam = { days: 365 * offset };\n            endDate = endDate.plus(offsetParam);\n            startDate = startOfNextDay.minus({ days: 365 }).plus(offsetParam);\n            break;\n        }\n        case \"last_three_years\": {\n            const offsetParam = { days: 3 * 365 * offset };\n            endDate = endDate.plus(offsetParam);\n            startDate = startOfNextDay.minus({ days: 3 * 365 }).plus(offsetParam);\n            break;\n        }\n        default:\n            return undefined;\n    }\n\n    let leftBound, rightBound;\n    if (fieldType === \"date\") {\n        leftBound = serializeDate(startDate);\n        rightBound = serializeDate(endDate);\n    } else {\n        leftBound = serializeDateTime(startDate);\n        rightBound = serializeDateTime(endDate);\n    }\n\n    return new Domain([\"&\", [fieldName, \">=\", leftBound], [fieldName, \"<=\", rightBound]]);\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nimport { GlobalFiltersUIPlugin } from \"./plugins/global_filters_ui_plugin\";\nimport { GlobalFiltersCorePlugin } from \"./plugins/global_filters_core_plugin\";\nconst { inverseCommandRegistry } = spreadsheet.registries;\n\nfunction identity(cmd) {\n    return [cmd];\n}\n\nconst { coreTypes, invalidateEvaluationCommands, readonlyAllowedCommands } = spreadsheet;\n\ncoreTypes.add(\"ADD_GLOBAL_FILTER\");\ncoreTypes.add(\"EDIT_GLOBAL_FILTER\");\ncoreTypes.add(\"REMOVE_GLOBAL_FILTER\");\ncoreTypes.add(\"MOVE_GLOBAL_FILTER\");\n\ninvalidateEvaluationCommands.add(\"ADD_GLOBAL_FILTER\");\ninvalidateEvaluationCommands.add(\"EDIT_GLOBAL_FILTER\");\ninvalidateEvaluationCommands.add(\"REMOVE_GLOBAL_FILTER\");\ninvalidateEvaluationCommands.add(\"SET_GLOBAL_FILTER_VALUE\");\ninvalidateEvaluationCommands.add(\"CLEAR_GLOBAL_FILTER_VALUE\");\n\nreadonlyAllowedCommands.add(\"SET_GLOBAL_FILTER_VALUE\");\nreadonlyAllowedCommands.add(\"SET_MANY_GLOBAL_FILTER_VALUE\");\nreadonlyAllowedCommands.add(\"CLEAR_GLOBAL_FILTER_VALUE\");\nreadonlyAllowedCommands.add(\"UPDATE_OBJECT_DOMAINS\");\n\ninverseCommandRegistry\n    .add(\"EDIT_GLOBAL_FILTER\", identity)\n    .add(\"ADD_GLOBAL_FILTER\", (cmd) => {\n        return [\n            {\n                type: \"REMOVE_GLOBAL_FILTER\",\n                id: cmd.filter.id,\n            },\n        ];\n    })\n    .add(\"REMOVE_GLOBAL_FILTER\", (cmd) => {\n        return [\n            {\n                type: \"ADD_GLOBAL_FILTER\",\n                filter: {},\n            },\n        ];\n    })\n    .add(\"MOVE_GLOBAL_FILTER\", (cmd) => {\n        return [\n            {\n                type: \"MOVE_GLOBAL_FILTER\",\n                id: cmd.id,\n                delta: cmd.delta * -1,\n            },\n        ];\n    });\n\nexport { GlobalFiltersCorePlugin, GlobalFiltersUIPlugin };\n", "/** @ts-check */\n\nexport const globalFiltersFieldMatchers = {};\n\nimport { CommandResult } from \"@spreadsheet/o_spreadsheet/cancelled_reason\";\nimport { checkFilterValueIsValid } from \"@spreadsheet/global_filters/helpers\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\n\n/**\n * @typedef {import(\"@spreadsheet\").GlobalFilter} GlobalFilter\n * @typedef {import(\"@spreadsheet\").CmdGlobalFilter} CmdGlobalFilter\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n */\n\nexport class GlobalFiltersCorePlugin extends OdooCorePlugin {\n    static getters = /** @type {const} */ ([\n        \"getGlobalFilter\",\n        \"getGlobalFilters\",\n        \"getGlobalFilterDefaultValue\",\n        \"getGlobalFilterLabel\",\n        \"getFieldMatchingForModel\",\n    ]);\n    constructor(config) {\n        super(config);\n        /** @type {Array.<GlobalFilter>} */\n        this.globalFilters = [];\n    }\n\n    /**\n     * Check if the given command can be dispatched\n     *\n     * @param {import(\"@spreadsheet\").AllCoreCommand} cmd Command\n     */\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"EDIT_GLOBAL_FILTER\":\n                if (!this.getGlobalFilter(cmd.filter.id)) {\n                    return CommandResult.FilterNotFound;\n                } else if (this._isDuplicatedLabel(cmd.filter.id, cmd.filter.label)) {\n                    return CommandResult.DuplicatedFilterLabel;\n                }\n                if (!checkFilterValueIsValid(cmd.filter, cmd.filter.defaultValue)) {\n                    return CommandResult.InvalidValueTypeCombination;\n                }\n                break;\n            case \"REMOVE_GLOBAL_FILTER\":\n                if (!this.getGlobalFilter(cmd.id)) {\n                    return CommandResult.FilterNotFound;\n                }\n                break;\n            case \"ADD_GLOBAL_FILTER\":\n                if (this._isDuplicatedLabel(cmd.filter.id, cmd.filter.label)) {\n                    return CommandResult.DuplicatedFilterLabel;\n                }\n                if (!checkFilterValueIsValid(cmd.filter, cmd.filter.defaultValue)) {\n                    return CommandResult.InvalidValueTypeCombination;\n                }\n                break;\n            case \"MOVE_GLOBAL_FILTER\": {\n                const index = this.globalFilters.findIndex((filter) => filter.id === cmd.id);\n                if (index === -1) {\n                    return CommandResult.FilterNotFound;\n                }\n                const targetIndex = index + cmd.delta;\n                if (targetIndex < 0 || targetIndex >= this.globalFilters.length) {\n                    return CommandResult.InvalidFilterMove;\n                }\n                break;\n            }\n        }\n        return CommandResult.Success;\n    }\n\n    /**\n     * Handle a spreadsheet command\n     *\n     * @param {Object} cmd Command\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_GLOBAL_FILTER\": {\n                const filter = { ...cmd.filter };\n                if (filter.type === \"text\" && filter.rangeOfAllowedValues) {\n                    filter.rangeOfAllowedValues = this.getters.getRangeFromRangeData(\n                        filter.rangeOfAllowedValues\n                    );\n                }\n                this.history.update(\"globalFilters\", [...this.globalFilters, filter]);\n                break;\n            }\n            case \"EDIT_GLOBAL_FILTER\": {\n                this._editGlobalFilter(cmd.filter);\n                break;\n            }\n            case \"REMOVE_GLOBAL_FILTER\": {\n                const filters = this.globalFilters.filter((filter) => filter.id !== cmd.id);\n                this.history.update(\"globalFilters\", filters);\n                break;\n            }\n            case \"MOVE_GLOBAL_FILTER\":\n                this._onMoveFilter(cmd.id, cmd.delta);\n                break;\n        }\n    }\n\n    adaptRanges(applyChange) {\n        for (const filterIndex in this.globalFilters) {\n            const filter = this.globalFilters[filterIndex];\n            if (filter.type === \"text\" && filter.rangeOfAllowedValues) {\n                const change = applyChange(filter.rangeOfAllowedValues);\n                switch (change.changeType) {\n                    case \"REMOVE\": {\n                        this.history.update(\n                            \"globalFilters\",\n                            filterIndex,\n                            \"rangeOfAllowedValues\",\n                            undefined\n                        );\n                        break;\n                    }\n                    case \"RESIZE\":\n                    case \"MOVE\":\n                    case \"CHANGE\": {\n                        this.history.update(\n                            \"globalFilters\",\n                            filterIndex,\n                            \"rangeOfAllowedValues\",\n                            change.range\n                        );\n                    }\n                }\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------\n\n    /**\n     * Retrieve the global filter with the given id\n     *\n     * @param {string} id\n     * @returns {GlobalFilter|undefined} Global filter\n     */\n    getGlobalFilter(id) {\n        return this.globalFilters.find((filter) => filter.id === id);\n    }\n\n    /**\n     * Get the global filter with the given name\n     *\n     * @param {string} label Label\n     *\n     * @returns {GlobalFilter|undefined}\n     */\n    getGlobalFilterLabel(label) {\n        return this.globalFilters.find((filter) => _t(filter.label) === _t(label));\n    }\n\n    /**\n     * Retrieve all the global filters\n     *\n     * @returns {Array<GlobalFilter>} Array of Global filters\n     */\n    getGlobalFilters() {\n        return [...this.globalFilters];\n    }\n\n    /**\n     * Get the default value of a global filter\n     *\n     * @param {string} id Id of the filter\n     *\n     * @returns {string|Array<string>|Object}\n     */\n    getGlobalFilterDefaultValue(id) {\n        return this.getGlobalFilter(id).defaultValue;\n    }\n\n    /**\n     * Returns the field matching for a given model by copying the matchings of another DataSource that\n     * share the same model, including only the chain and type.\n     *\n     * @returns {Record<string, FieldMatching> | {}}\n     */\n    getFieldMatchingForModel(newModel) {\n        const globalFilters = this.getGlobalFilters();\n        if (globalFilters.length === 0) {\n            return {};\n        }\n\n        for (const matcher of Object.values(globalFiltersFieldMatchers)) {\n            for (const dataSourceId of matcher.getIds()) {\n                const model = matcher.getModel(dataSourceId);\n                if (model === newModel) {\n                    const fieldMatching = {};\n                    for (const filter of globalFilters) {\n                        const matchedField = matcher.getFieldMatching(dataSourceId, filter.id);\n                        if (matchedField) {\n                            fieldMatching[filter.id] = {\n                                chain: matchedField.chain,\n                                type: matchedField.type,\n                            };\n                        }\n                    }\n                    return fieldMatching;\n                }\n            }\n        }\n        return {};\n    }\n\n    // ---------------------------------------------------------------------\n    // Handlers\n    // ---------------------------------------------------------------------\n\n    /**\n     * Edit a global filter\n     *\n     * @param {CmdGlobalFilter} cmdFilter\n     */\n    _editGlobalFilter(cmdFilter) {\n        const rangeOfAllowedValues =\n            cmdFilter.type === \"text\" && cmdFilter.rangeOfAllowedValues\n                ? this.getters.getRangeFromRangeData(cmdFilter.rangeOfAllowedValues)\n                : undefined;\n        /** @type {GlobalFilter} */\n        const newFilter =\n            cmdFilter.type === \"text\" ? { ...cmdFilter, rangeOfAllowedValues } : { ...cmdFilter };\n        const id = newFilter.id;\n        const currentLabel = this.getGlobalFilter(id).label;\n        const index = this.globalFilters.findIndex((filter) => filter.id === id);\n        if (index === -1) {\n            return;\n        }\n        this.history.update(\"globalFilters\", index, newFilter);\n        const newLabel = this.getGlobalFilter(id).label;\n        if (currentLabel !== newLabel) {\n            this._updateFilterLabelInFormulas(currentLabel, newLabel);\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------\n\n    /**\n     * Import the filters\n     *\n     * @param {Object} data\n     */\n    import(data) {\n        for (const globalFilter of data.globalFilters || []) {\n            if (globalFilter.type === \"text\" && globalFilter.rangeOfAllowedValues) {\n                globalFilter.rangeOfAllowedValues = this.getters.getRangeFromSheetXC(\n                    // The default sheet id doesn't matter here, the exported range string\n                    // is fully qualified and contains the sheet name.\n                    // The getter expects a valid sheet id though, let's give it the\n                    // first sheet id.\n                    data.sheets[0].id,\n                    globalFilter.rangeOfAllowedValues\n                );\n            }\n            this.globalFilters.push(globalFilter);\n        }\n    }\n    /**\n     * Export the filters\n     *\n     * @param {Object} data\n     */\n    export(data) {\n        data.globalFilters = this.globalFilters.map((filter) => {\n            /** @type {Object} */\n            const filterData = { ...filter };\n            if (filter.type === \"text\" && filter.rangeOfAllowedValues) {\n                filterData.rangeOfAllowedValues = this.getters.getRangeString(\n                    filter.rangeOfAllowedValues,\n                    \"\" // force the range string to be fully qualified (with the sheet name)\n                );\n            }\n            return filterData;\n        });\n    }\n\n    // ---------------------------------------------------------------------\n    // Global filters\n    // ---------------------------------------------------------------------\n\n    /**\n     * Update all ODOO.FILTER.VALUE formulas to reference a filter\n     * by its new label.\n     *\n     * @param {string} currentLabel\n     * @param {string} newLabel\n     */\n    _updateFilterLabelInFormulas(currentLabel, newLabel) {\n        const sheetIds = this.getters.getSheetIds();\n        currentLabel = escapeRegExp(currentLabel);\n        for (const sheetId of sheetIds) {\n            for (const cell of Object.values(this.getters.getCells(sheetId))) {\n                if (cell.isFormula) {\n                    const newContent = cell.content.replace(\n                        new RegExp(`FILTER\\\\.VALUE\\\\(\\\\s*\"${currentLabel}\"\\\\s*\\\\)`, \"g\"),\n                        `FILTER.VALUE(\"${newLabel}\")`\n                    );\n                    if (newContent !== cell.content) {\n                        const { col, row } = this.getters.getCellPosition(cell.id);\n                        this.dispatch(\"UPDATE_CELL\", {\n                            sheetId,\n                            content: newContent,\n                            col,\n                            row,\n                        });\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Return true if the label is duplicated\n     *\n     * @param {string | undefined} filterId\n     * @param {string} label\n     * @returns {boolean}\n     */\n    _isDuplicatedLabel(filterId, label) {\n        return (\n            this.globalFilters.findIndex(\n                (filter) => (!filterId || filter.id !== filterId) && filter.label === label\n            ) > -1\n        );\n    }\n\n    _onMoveFilter(filterId, delta) {\n        const filters = [...this.globalFilters];\n        const currentIndex = filters.findIndex((s) => s.id === filterId);\n        const filter = filters[currentIndex];\n        const targetIndex = currentIndex + delta;\n\n        filters.splice(currentIndex, 1);\n        filters.splice(targetIndex, 0, filter);\n\n        this.history.update(\"globalFilters\", filters);\n    }\n}\n", "/** @ts-check */\n\n/**\n * @typedef {import(\"@spreadsheet\").GlobalFilter} GlobalFilter\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n * @typedef {import(\"@spreadsheet\").DateGlobalFilter} DateGlobalFilter\n * @typedef {import(\"@spreadsheet\").RelationalGlobalFilter} RelationalGlobalFilter\n */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { Domain } from \"@web/core/domain\";\nimport { user } from \"@web/core/user\";\nimport { constructDateRange, QUARTER_OPTIONS } from \"@web/search/utils/dates\";\n\nimport { EvaluationError, helpers } from \"@odoo/o-spreadsheet\";\nimport { CommandResult } from \"@spreadsheet/o_spreadsheet/cancelled_reason\";\n\nimport { isEmpty } from \"@spreadsheet/helpers/helpers\";\nimport { FILTER_DATE_OPTION } from \"@spreadsheet/assets_backend/constants\";\nimport {\n    checkFilterValueIsValid,\n    getRelativeDateDomain,\n} from \"@spreadsheet/global_filters/helpers\";\nimport { RELATIVE_DATE_RANGE_TYPES } from \"@spreadsheet/helpers/constants\";\nimport { OdooUIPlugin } from \"@spreadsheet/plugins\";\nimport { getItemId } from \"../../helpers/model\";\nimport { serializeDateTime, serializeDate } from \"@web/core/l10n/dates\";\n\nconst { DateTime } = luxon;\n\nconst MONTHS = {\n    january: { value: 1, granularity: \"month\" },\n    february: { value: 2, granularity: \"month\" },\n    march: { value: 3, granularity: \"month\" },\n    april: { value: 4, granularity: \"month\" },\n    may: { value: 5, granularity: \"month\" },\n    june: { value: 6, granularity: \"month\" },\n    july: { value: 7, granularity: \"month\" },\n    august: { value: 8, granularity: \"month\" },\n    september: { value: 9, granularity: \"month\" },\n    october: { value: 10, granularity: \"month\" },\n    november: { value: 11, granularity: \"month\" },\n    december: { value: 12, granularity: \"month\" },\n};\n\nconst { UuidGenerator, createEmptyExcelSheet, createEmptySheet, toXC, toNumber } = helpers;\nconst uuidGenerator = new UuidGenerator();\n\nexport class GlobalFiltersUIPlugin extends OdooUIPlugin {\n    static getters = /** @type {const} */ ([\n        \"exportSheetWithActiveFilters\",\n        \"getFilterDisplayValue\",\n        \"getGlobalFilterDomain\",\n        \"getGlobalFilterValue\",\n        \"getActiveFilterCount\",\n        \"isGlobalFilterActive\",\n        \"getTextFilterOptions\",\n        \"getTextFilterOptionsFromRange\",\n    ]);\n    constructor(config) {\n        super(config);\n        this.orm = config.custom.env?.services.orm;\n        this.odooDataProvider = config.custom.odooDataProvider;\n        /**\n         * Cache record display names for relation filters.\n         * For each filter, contains a promise resolving to\n         * the list of display names.\n         */\n        this.recordsDisplayName = {};\n        /** @type {Object.<string, string|Array<string>|Object>} */\n        this.values = {};\n    }\n\n    /**\n     * Check if the given command can be dispatched\n     *\n     * @param {import(\"@spreadsheet\").AllCommand} cmd Command\n     */\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"SET_GLOBAL_FILTER_VALUE\": {\n                const filter = this.getters.getGlobalFilter(cmd.id);\n                if (!filter) {\n                    return CommandResult.FilterNotFound;\n                }\n                if (!checkFilterValueIsValid(filter, cmd.value)) {\n                    return CommandResult.InvalidValueTypeCombination;\n                }\n                break;\n            }\n        }\n        return CommandResult.Success;\n    }\n\n    /**\n     * Handle a spreadsheet command\n     *\n     * @param {import(\"@spreadsheet\").AllCommand} cmd\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_GLOBAL_FILTER\":\n                this.recordsDisplayName[cmd.filter.id] =\n                    cmd.filter.type === \"relation\"\n                        ? cmd.filter.defaultValueDisplayNames\n                        : undefined;\n                break;\n            case \"EDIT_GLOBAL_FILTER\": {\n                const filter = cmd.filter;\n                const id = filter.id;\n                if (\n                    filter.type === \"date\" &&\n                    this.values[id] &&\n                    this.values[id].rangeType !== filter.rangeType\n                ) {\n                    delete this.values[id];\n                } else if (!checkFilterValueIsValid(filter, this.values[id]?.value)) {\n                    delete this.values[id];\n                }\n                this.recordsDisplayName[id] =\n                    filter.type === \"relation\" ? filter.defaultValueDisplayNames : undefined;\n                break;\n            }\n            case \"SET_GLOBAL_FILTER_VALUE\":\n                this.recordsDisplayName[cmd.id] = cmd.displayNames;\n                if (!cmd.value) {\n                    this._clearGlobalFilterValue(cmd.id);\n                } else {\n                    this._setGlobalFilterValue(cmd.id, cmd.value);\n                }\n                break;\n            case \"SET_MANY_GLOBAL_FILTER_VALUE\":\n                for (const filter of cmd.filters) {\n                    if (filter.value !== undefined) {\n                        this.dispatch(\"SET_GLOBAL_FILTER_VALUE\", {\n                            id: filter.filterId,\n                            value: filter.value,\n                        });\n                    } else {\n                        this.dispatch(\"CLEAR_GLOBAL_FILTER_VALUE\", { id: filter.filterId });\n                    }\n                }\n                break;\n            case \"REMOVE_GLOBAL_FILTER\":\n                delete this.recordsDisplayName[cmd.id];\n                delete this.values[cmd.id];\n                break;\n            case \"CLEAR_GLOBAL_FILTER_VALUE\":\n                this.recordsDisplayName[cmd.id] = [];\n                this._clearGlobalFilterValue(cmd.id);\n                break;\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} filterId\n     * @param {FieldMatching} fieldMatching\n     *\n     * @return {Domain}\n     */\n    getGlobalFilterDomain(filterId, fieldMatching) {\n        /** @type {GlobalFilter} */\n        const filter = this.getters.getGlobalFilter(filterId);\n        if (!filter) {\n            return new Domain();\n        }\n        switch (filter.type) {\n            case \"text\":\n                return this._getTextDomain(filter, fieldMatching);\n            case \"date\":\n                return this._getDateDomain(filter, fieldMatching);\n            case \"relation\":\n                return this._getRelationDomain(filter, fieldMatching);\n        }\n    }\n\n    /**\n     * Get the current value of a global filter\n     *\n     * @param {string} filterId Id of the filter\n     *\n     * @returns {string|Array<string>|Object} value Current value to set\n     */\n    getGlobalFilterValue(filterId) {\n        const filter = this.getters.getGlobalFilter(filterId);\n\n        const value = filterId in this.values ? this.values[filterId].value : undefined;\n        const preventAutomaticValue = this.values[filterId]?.value?.preventAutomaticValue;\n        if (filter.type === \"date\" && filter.rangeType === \"from_to\") {\n            return value || { from: undefined, to: undefined };\n        }\n        const defaultValue = (!preventAutomaticValue && filter.defaultValue) || undefined;\n        if (filter.type === \"date\" && preventAutomaticValue) {\n            return undefined;\n        }\n        if (filter.type === \"date\" && isEmpty(value) && defaultValue) {\n            return this._getValueOfCurrentPeriod(filterId);\n        }\n        if (filter.type === \"relation\" && preventAutomaticValue) {\n            return [];\n        }\n        if (filter.type === \"relation\" && isEmpty(value) && defaultValue === \"current_user\") {\n            return [user.userId];\n        }\n        if (filter.type === \"text\" && preventAutomaticValue) {\n            return \"\";\n        }\n        return value || defaultValue;\n    }\n\n    /**\n     * @param {string} id Id of the filter\n     *\n     * @returns { boolean } true if the given filter is active\n     */\n    isGlobalFilterActive(id) {\n        const { type } = this.getters.getGlobalFilter(id);\n        const value = this.getGlobalFilterValue(id);\n        switch (type) {\n            case \"text\":\n                return value;\n            case \"date\":\n                return (\n                    value &&\n                    (typeof value === \"string\" ||\n                        value.yearOffset !== undefined ||\n                        value.period ||\n                        value.from ||\n                        value.to)\n                );\n            case \"relation\":\n                return value && value.length;\n        }\n    }\n\n    /**\n     * Get the number of active global filters\n     *\n     * @returns {number}\n     */\n    getActiveFilterCount() {\n        return this.getters\n            .getGlobalFilters()\n            .filter((filter) => this.isGlobalFilterActive(filter.id)).length;\n    }\n\n    getFilterDisplayValue(filterName) {\n        const filter = this.getters.getGlobalFilterLabel(filterName);\n        if (!filter) {\n            throw new EvaluationError(sprintf(_t(`Filter \"%s\" not found`), filterName));\n        }\n        const value = this.getGlobalFilterValue(filter.id);\n        switch (filter.type) {\n            case \"text\":\n                return [[{ value: value || \"\" }]];\n            case \"date\": {\n                if (filter.rangeType === \"from_to\") {\n                    const locale = this.getters.getLocale();\n                    const from = {\n                        value: value.from ? toNumber(value.from, locale) : \"\",\n                        format: locale.dateFormat,\n                    };\n                    const to = {\n                        value: value.to ? toNumber(value.to, locale) : \"\",\n                        format: locale.dateFormat,\n                    };\n                    return [[from], [to]];\n                }\n                if (value && typeof value === \"string\") {\n                    const type = RELATIVE_DATE_RANGE_TYPES.find((type) => type.type === value);\n                    if (!type) {\n                        return [[{ value: \"\" }]];\n                    }\n                    return [[{ value: type.description.toString() }]];\n                }\n                if (!value || value.yearOffset === undefined) {\n                    return [[{ value: \"\" }]];\n                }\n                const year = String(DateTime.local().year + value.yearOffset);\n                const period = QUARTER_OPTIONS[value.period];\n                let periodStr = period && \"Q\" + period.setParam.quarter; // we do not want the translated value (like T1 in French)\n                // Named months aren't in QUARTER_OPTIONS\n                if (!period) {\n                    periodStr =\n                        MONTHS[value.period] && String(MONTHS[value.period].value).padStart(2, \"0\");\n                }\n                return [[{ value: periodStr ? periodStr + \"/\" + year : year }]];\n            }\n            case \"relation\":\n                if (!value?.length || !this.orm) {\n                    return [[{ value: \"\" }]];\n                }\n                if (!this.recordsDisplayName[filter.id]) {\n                    const promise = this.orm\n                        .call(filter.modelName, \"read\", [value, [\"display_name\"]])\n                        .then((result) => {\n                            const names = result.map(({ display_name }) => display_name);\n                            this.recordsDisplayName[filter.id] = names;\n                        });\n                    this.odooDataProvider.notifyWhenPromiseResolves(promise);\n                    return [[{ value: \"\" }]];\n                }\n                return [[{ value: this.recordsDisplayName[filter.id].join(\", \") }]];\n        }\n    }\n\n    /**\n     * Returns the possible values a text global filter can take\n     * if the values are restricted by a range of allowed values\n     * @param {string} filterId\n     * @returns {{value: string, formattedValue: string}[]}\n     */\n    getTextFilterOptions(filterId) {\n        const filter = this.getters.getGlobalFilter(filterId);\n        if (filter.type !== \"text\" || !filter.rangeOfAllowedValues) {\n            return [];\n        }\n        const additionOptions = [\n            // add the current value because it might not be in the range\n            // if the range cells changed in the meantime\n            this.getGlobalFilterValue(filterId),\n            filter.defaultValue,\n        ];\n        const options = this.getTextFilterOptionsFromRange(\n            filter.rangeOfAllowedValues,\n            additionOptions\n        );\n        return options;\n    }\n\n    /**\n     * Returns the possible values a text global filter can take from a range\n     * or any addition raw string value. Removes duplicates and empty string values.\n     * @param {object} range\n     * @param {string[]} additionalOptionValues\n     */\n    getTextFilterOptionsFromRange(range, additionalOptionValues = []) {\n        const cells = this.getters.getEvaluatedCellsInZone(range.sheetId, range.zone);\n        const uniqueFormattedValues = new Set();\n        const uniqueValues = new Set();\n        const allowedValues = cells\n            .filter((cell) => ![\"empty\", \"error\"].includes(cell.type) && cell.value !== \"\")\n            .map((cell) => ({\n                value: cell.value.toString(),\n                formattedValue: cell.formattedValue,\n            }))\n            .filter((cell) => {\n                if (uniqueFormattedValues.has(cell.formattedValue)) {\n                    return false;\n                }\n                uniqueFormattedValues.add(cell.formattedValue);\n                uniqueValues.add(cell.value);\n                return true;\n            });\n        const additionalOptions = additionalOptionValues\n            .map((value) => ({ value, formattedValue: value }))\n            .filter((cell) => {\n                if (cell.value === undefined || cell.value === \"\" || uniqueValues.has(cell.value)) {\n                    return false;\n                }\n                uniqueValues.add(cell.value);\n                return true;\n            });\n        return allowedValues.concat(additionalOptions);\n    }\n\n    // -------------------------------------------------------------------------\n    // Handlers\n    // -------------------------------------------------------------------------\n\n    /**\n     * Set the current value of a global filter\n     *\n     * @param {string} id Id of the filter\n     * @param {string|Array<string>|Object} value Current value to set\n     */\n    _setGlobalFilterValue(id, value) {\n        const filter = this.getters.getGlobalFilter(id);\n        this.values[id] = {\n            value: value,\n            rangeType: filter.type === \"date\" ? filter.rangeType : undefined,\n        };\n    }\n\n    /**\n     * Get the filter value corresponding to the current period, depending of the type of range of the filter.\n     * For example if rangeType === \"month\", the value will be the current month of the current year.\n     *\n     * @param {string} filterId a global filter\n     * @return {Object} filter value\n     */\n    _getValueOfCurrentPeriod(filterId) {\n        const filter = this.getters.getGlobalFilter(filterId);\n        switch (filter.defaultValue) {\n            case \"this_year\":\n                return { yearOffset: 0 };\n            case \"this_month\": {\n                const month = new Date().getMonth() + 1;\n                const period = Object.entries(MONTHS).find((item) => item[1].value === month)[0];\n                return { yearOffset: 0, period };\n            }\n            case \"this_quarter\": {\n                const quarter = Math.floor(new Date().getMonth() / 3);\n                const period = FILTER_DATE_OPTION.quarter[quarter];\n                return { yearOffset: 0, period };\n            }\n        }\n        return filter.defaultValue;\n    }\n\n    /**\n     * Set the current value to empty values which functionally deactivate the filter\n     *\n     * @param {string} id Id of the filter\n     */\n    _clearGlobalFilterValue(id) {\n        const filter = this.getters.getGlobalFilter(id);\n        let value;\n        switch (filter.type) {\n            case \"text\":\n                value = { preventAutomaticValue: true };\n                break;\n            case \"date\":\n                value = { preventAutomaticValue: true };\n                break;\n            case \"relation\":\n                value = { preventAutomaticValue: true };\n                break;\n        }\n        this.values[id] = {\n            value,\n            rangeType: filter.type === \"date\" ? filter.rangeType : undefined,\n        };\n    }\n\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n\n    /**\n     * Get the domain relative to a date field\n     *\n     * @private\n     *\n     * @param {DateGlobalFilter} filter\n     * @param {FieldMatching} fieldMatching\n     *\n     * @returns {Domain}\n     */\n    _getDateDomain(filter, fieldMatching) {\n        let granularity;\n        const value = this.getGlobalFilterValue(filter.id);\n        if (!value || !fieldMatching.chain) {\n            return new Domain();\n        }\n        const field = fieldMatching.chain;\n        const type = /** @type {\"date\" | \"datetime\"} */ (fieldMatching.type);\n        const offset = fieldMatching.offset || 0;\n        const now = DateTime.local();\n\n        if (filter.rangeType === \"from_to\") {\n            const serialize = type === \"datetime\" ? serializeDateTime : serializeDate;\n            const from = value.from && serialize(DateTime.fromISO(value.from).startOf(\"day\"));\n            const to = value.to && serialize(DateTime.fromISO(value.to).endOf(\"day\"));\n            if (from && to) {\n                return new Domain([\"&\", [field, \">=\", from], [field, \"<=\", to]]);\n            }\n            if (from) {\n                return new Domain([[field, \">=\", from]]);\n            }\n            if (to) {\n                return new Domain([[field, \"<=\", to]]);\n            }\n            return new Domain();\n        }\n\n        if (filter.rangeType === \"relative\") {\n            return getRelativeDateDomain(now, offset, value, field, type);\n        }\n        const noPeriod = !value.period || value.period === \"empty\";\n        const noYear = value.yearOffset === undefined;\n        if (noPeriod && noYear) {\n            return new Domain();\n        }\n        const setParam = { year: now.year };\n        const yearOffset = value.yearOffset || 0;\n        const plusParam = { years: yearOffset };\n        if (noPeriod) {\n            granularity = \"year\";\n            plusParam.years += offset;\n        } else {\n            // value.period is can be \"first_quarter\", \"second_quarter\", etc. or\n            // full month name (e.g. \"january\", \"february\", \"march\", etc.)\n            granularity = value.period.endsWith(\"_quarter\") ? \"quarter\" : \"month\";\n            switch (granularity) {\n                case \"month\":\n                    setParam.month = MONTHS[value.period].value;\n                    plusParam.month = offset;\n                    break;\n                case \"quarter\":\n                    setParam.quarter = QUARTER_OPTIONS[value.period].setParam.quarter;\n                    plusParam.quarter = offset;\n                    break;\n            }\n        }\n        return constructDateRange({\n            referenceMoment: now,\n            fieldName: field,\n            fieldType: type,\n            granularity,\n            setParam,\n            plusParam,\n        }).domain;\n    }\n\n    /**\n     * Get the domain relative to a text field\n     *\n     * @private\n     *\n     * @param {GlobalFilter} filter\n     * @param {FieldMatching} fieldMatching\n     *\n     * @returns {Domain}\n     */\n    _getTextDomain(filter, fieldMatching) {\n        const value = this.getGlobalFilterValue(filter.id);\n        if (!value || !fieldMatching.chain) {\n            return new Domain();\n        }\n        const field = fieldMatching.chain;\n        return new Domain([[field, \"ilike\", value]]);\n    }\n\n    /**\n     * Get the domain relative to a relation field\n     *\n     * @private\n     *\n     * @param {RelationalGlobalFilter} filter\n     * @param {FieldMatching} fieldMatching\n     *\n     * @returns {Domain}\n     */\n    _getRelationDomain(filter, fieldMatching) {\n        const values = this.getGlobalFilterValue(filter.id);\n        if (!values || values.length === 0 || !fieldMatching.chain) {\n            return new Domain();\n        }\n        const field = fieldMatching.chain;\n        const operator = filter.includeChildren ? \"child_of\" : \"in\";\n        return new Domain([[field, operator, values]]);\n    }\n\n    /**\n     * Adds all active filters (and their values) at the time of export in a dedicated sheet\n     *\n     * @param {Object} data\n     */\n    exportForExcel(data) {\n        if (this.getters.getGlobalFilters().length === 0) {\n            return;\n        }\n        this.exportSheetWithActiveFilters(data);\n        data.sheets[data.sheets.length - 1] = {\n            ...createEmptyExcelSheet(uuidGenerator.uuidv4(), _t(\"Active Filters\")),\n            ...data.sheets.at(-1),\n        };\n    }\n\n    exportSheetWithActiveFilters(data) {\n        if (this.getters.getGlobalFilters().length === 0) {\n            return;\n        }\n        const styleId = getItemId({ bold: true }, data.styles);\n\n        const cells = {\n            A1: { content: \"Filter\" },\n            B1: { content: \"Value\" },\n        };\n        const formats = {};\n        let numberOfCols = 2; // at least 2 cols (filter title and filter value)\n        let filterRowIndex = 1; // first row is the column titles\n        for (const filter of this.getters.getGlobalFilters()) {\n            cells[`A${filterRowIndex + 1}`] = { content: filter.label };\n            const result = this.getFilterDisplayValue(filter.label);\n            for (const colIndex in result) {\n                numberOfCols = Math.max(numberOfCols, Number(colIndex) + 2);\n                for (const rowIndex in result[colIndex]) {\n                    const cell = result[colIndex][rowIndex];\n                    if (cell.value === undefined) {\n                        continue;\n                    }\n                    const xc = toXC(Number(colIndex) + 1, Number(rowIndex) + filterRowIndex);\n                    cells[xc] = { content: cell.value.toString() };\n                    if (cell.format) {\n                        const formatId = getItemId(cell.format, data.formats);\n                        formats[xc] = formatId;\n                    }\n                }\n            }\n            filterRowIndex += result[0].length;\n        }\n        const sheet = {\n            ...createEmptySheet(uuidGenerator.uuidv4(), _t(\"Active Filters\")),\n            cells,\n            formats,\n            styles: {\n                A1: styleId,\n                B1: styleId,\n            },\n            colNumber: numberOfCols,\n            rowNumber: filterRowIndex,\n        };\n        data.sheets.push(sheet);\n    }\n}\n", "/** @odoo-module */\n// @ts-check\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const DEFAULT_LINES_NUMBER = 20;\n\nexport const UNTITLED_SPREADSHEET_NAME = _t(\"Untitled spreadsheet\");\n\nexport const RELATIVE_DATE_RANGE_TYPES = [\n    { type: \"year_to_date\", description: _t(\"Year to Date\") },\n    { type: \"last_week\", description: _t(\"Last 7 Days\") },\n    { type: \"last_month\", description: _t(\"Last 30 Days\") },\n    { type: \"last_three_months\", description: _t(\"Last 90 Days\") },\n    { type: \"last_six_months\", description: _t(\"Last 180 Days\") },\n    { type: \"last_year\", description: _t(\"Last 365 Days\") },\n    { type: \"last_three_years\", description: _t(\"Last 3 Years\") },\n];\n", "/** @odoo-module */\n// @ts-check\n\n/**\n * Get the intersection of two arrays\n *\n * @param {Array} a\n * @param {Array} b\n *\n * @private\n * @returns {Array} intersection between a and b\n */\nexport function intersect(a, b) {\n    return a.filter((x) => b.includes(x));\n}\n\n/**\n * Convert a spreadsheet date representation to an odoo\n * server formatted date\n *\n * @param {Date} value\n * @returns {string}\n */\nexport function toServerDateString(value) {\n    return `${value.getFullYear()}-${value.getMonth() + 1}-${value.getDate()}`;\n}\n\n/**\n * @param {number[]} array\n * @returns {number}\n */\nexport function sum(array) {\n    return array.reduce((acc, n) => acc + n, 0);\n}\n\n/**\n * @param {string} word\n */\nfunction camelToSnakeKey(word) {\n    const result = word.replace(/(.){1}([A-Z])/g, \"$1 $2\");\n    return result.split(\" \").join(\"_\").toLowerCase();\n}\n\n/**\n * Recursively convert camel case object keys to snake case keys\n * @param {object} obj\n * @returns {object}\n */\nexport function camelToSnakeObject(obj) {\n    const result = {};\n    for (const [key, value] of Object.entries(obj)) {\n        const isPojo = typeof value === \"object\" && value !== null && value.constructor === Object;\n        result[camelToSnakeKey(key)] = isPojo ? camelToSnakeObject(value) : value;\n    }\n    return result;\n}\n\n/**\n * Check if the argument is falsy or is an empty object/array\n *\n * TODO : remove this and replace it by the one in o_spreadsheet xlsx import when its merged\n */\nexport function isEmpty(item) {\n    if (!item) {\n        return true;\n    }\n    if (typeof item === \"object\") {\n        if (\n            Object.values(item).length === 0 ||\n            Object.values(item).every((val) => val === undefined)\n        ) {\n            return true;\n        }\n    }\n    return false;\n}\n\n/**\n * @param {import(\"@odoo/o-spreadsheet\").Cell} cell\n */\nexport function containsReferences(cell) {\n    if (!cell.isFormula) {\n        return false;\n    }\n    return cell.compiledFormula.tokens.some((token) => token.type === \"REFERENCE\");\n}\n", "/** @odoo-module */\n// @ts-check\n\nimport { parse, helpers, iterateAstNodes } from \"@odoo/o-spreadsheet\";\nimport { isLoadingError } from \"@spreadsheet/o_spreadsheet/errors\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { OdooSpreadsheetModel } from \"@spreadsheet/model\";\nimport { OdooDataProvider } from \"@spreadsheet/data_sources/odoo_data_provider\";\n\nconst { formatValue, isDefined, toCartesian, toXC } = helpers;\nimport {\n    isMarkdownViewUrl,\n    isMarkdownIrMenuIdUrl,\n    isIrMenuXmlUrl,\n} from \"@spreadsheet/ir_ui_menu/odoo_menu_link_cell\";\n\n/**\n * @typedef {import(\"@spreadsheet\").OdooSpreadsheetModel} OdooSpreadsheetModel\n */\n\nexport async function fetchSpreadsheetModel(env, resModel, resId) {\n    const { data, revisions } = await env.services.orm.call(resModel, \"join_spreadsheet_session\", [\n        resId,\n    ]);\n    return createSpreadsheetModel({ env, data, revisions });\n}\n\nexport function createSpreadsheetModel({ env, data, revisions }) {\n    const odooDataProvider = new OdooDataProvider(env);\n    const model = new OdooSpreadsheetModel(data, { custom: { odooDataProvider } }, revisions);\n    return model;\n}\n\n/**\n * @param {OdooSpreadsheetModel} model\n */\nexport async function waitForOdooSources(model) {\n    const promises = model.getters\n        .getOdooChartIds()\n        .map((chartId) => model.getters.getChartDataSource(chartId).load());\n    promises.push(\n        ...model.getters\n            .getPivotIds()\n            .filter((pivotId) => model.getters.getPivotCoreDefinition(pivotId).type === \"ODOO\")\n            .map((pivotId) => model.getters.getPivot(pivotId))\n            .map((pivot) => pivot.load())\n    );\n    promises.push(\n        ...model.getters\n            .getListIds()\n            .map((listId) => model.getters.getListDataSource(listId))\n            .map((list) => list.load())\n    );\n    await Promise.all(promises);\n}\n\n/**\n * Ensure that the spreadsheet does not contains cells that are in loading state\n * @param {OdooSpreadsheetModel} model\n * @returns {Promise<void>}\n */\nexport async function waitForDataLoaded(model) {\n    await waitForOdooSources(model);\n    const odooDataProvider = model.config.custom.odooDataProvider;\n    if (!odooDataProvider) {\n        return;\n    }\n    await new Promise((resolve, reject) => {\n        function check() {\n            model.dispatch(\"EVALUATE_CELLS\");\n            if (isLoaded(model)) {\n                odooDataProvider.removeEventListener(\"data-source-updated\", check);\n                resolve();\n            }\n        }\n        odooDataProvider.addEventListener(\"data-source-updated\", check);\n        check();\n    });\n}\n\nfunction containsLinkToOdoo(link) {\n    if (link && link.url) {\n        return (\n            isMarkdownViewUrl(link.url) ||\n            isIrMenuXmlUrl(link.url) ||\n            isMarkdownIrMenuIdUrl(link.url)\n        );\n    }\n}\n\n/**\n * @param {OdooSpreadsheetModel} model\n * @returns {Promise<object>}\n */\nexport async function freezeOdooData(model) {\n    await waitForDataLoaded(model);\n    const data = model.exportData();\n    for (const sheet of Object.values(data.sheets)) {\n        sheet.formats ??= {};\n        for (const [xc, cell] of Object.entries(sheet.cells)) {\n            const { col, row } = toCartesian(xc);\n            const sheetId = sheet.id;\n            const position = { sheetId, col, row };\n            const evaluatedCell = model.getters.getEvaluatedCell(position);\n            if (containsOdooFunction(cell.content)) {\n                const pivotId = model.getters.getPivotIdFromPosition(position);\n                if (pivotId && model.getters.getPivotCoreDefinition(pivotId).type !== \"ODOO\") {\n                    continue;\n                }\n                cell.content = evaluatedCell.value.toString();\n                if (evaluatedCell.format) {\n                    sheet.formats[xc] = getItemId(evaluatedCell.format, data.formats);\n                }\n                const spreadZone = model.getters.getSpreadZone(position);\n                if (spreadZone) {\n                    const { left, right, top, bottom } = spreadZone;\n                    for (let row = top; row <= bottom; row++) {\n                        for (let col = left; col <= right; col++) {\n                            const xc = toXC(col, row);\n                            const evaluatedCell = model.getters.getEvaluatedCell({\n                                sheetId,\n                                col,\n                                row,\n                            });\n                            sheet.cells[xc] = {\n                                ...sheet.cells[xc],\n                                content: evaluatedCell.value.toString(),\n                            };\n                            if (evaluatedCell.format) {\n                                sheet.formats[xc] = getItemId(evaluatedCell.format, data.formats);\n                            }\n                        }\n                    }\n                }\n            }\n            if (containsLinkToOdoo(evaluatedCell.link)) {\n                cell.content = evaluatedCell.link.label;\n            }\n        }\n        for (const figure of sheet.figures) {\n            if (figure.tag === \"chart\" && figure.data.type.startsWith(\"odoo_\")) {\n                await loadBundle(\"web.chartjs_lib\");\n                const img = odooChartToImage(model, figure);\n                figure.tag = \"image\";\n                figure.data = {\n                    path: img,\n                    size: { width: figure.width, height: figure.height },\n                };\n            }\n        }\n    }\n    if (data.pivots) {\n        data.pivots = Object.fromEntries(\n            Object.entries(data.pivots).filter(([id, def]) => def.type !== \"ODOO\")\n        );\n    }\n    data.lists = {};\n    exportGlobalFiltersToSheet(model, data);\n    return data;\n}\n\n/**\n * @param {OdooSpreadsheetModel} model\n * @returns {object}\n */\nfunction exportGlobalFiltersToSheet(model, data) {\n    model.getters.exportSheetWithActiveFilters(data);\n    const locale = model.getters.getLocale();\n    for (const filter of data.globalFilters) {\n        const content = model.getters.getFilterDisplayValue(filter.label);\n        filter[\"value\"] = content\n            .flat()\n            .filter(isDefined)\n            .map(({ value, format }) => formatValue(value, { format, locale }))\n            .filter((formattedValue) => formattedValue !== \"\")\n            .join(\", \");\n    }\n}\n\n/**\n * copy-pasted from o-spreadsheet. Should be exported\n * Get the id of the given item (its key in the given dictionnary).\n * If the given item does not exist in the dictionary, it creates one with a new id.\n */\nexport function getItemId(item, itemsDic) {\n    for (const [key, value] of Object.entries(itemsDic)) {\n        if (value === item) {\n            return parseInt(key, 10);\n        }\n    }\n\n    // Generate new Id if the item didn't exist in the dictionary\n    const ids = Object.keys(itemsDic);\n    const maxId = ids.length === 0 ? 0 : Math.max(...ids.map((id) => parseInt(id, 10)));\n\n    itemsDic[maxId + 1] = item;\n    return maxId + 1;\n}\n\n/**\n *\n * @param {string | undefined} content\n * @returns {boolean}\n */\nfunction containsOdooFunction(content) {\n    if (\n        !content ||\n        !content.startsWith(\"=\") ||\n        (!content.toUpperCase().includes(\"ODOO.\") &&\n            !content.toUpperCase().includes(\"_T\") &&\n            !content.toUpperCase().includes(\"PIVOT\"))\n    ) {\n        return false;\n    }\n    try {\n        const ast = parse(content);\n        return iterateAstNodes(ast).some(\n            (ast) =>\n                ast.type === \"FUNCALL\" &&\n                (ast.value.toUpperCase().startsWith(\"ODOO.\") ||\n                    ast.value.toUpperCase().startsWith(\"_T\") ||\n                    ast.value.toUpperCase().startsWith(\"PIVOT\"))\n        );\n    } catch {\n        return false;\n    }\n}\n\n/**\n * @param {OdooSpreadsheetModel} model\n * @returns {boolean}\n */\nfunction isLoaded(model) {\n    for (const sheetId of model.getters.getSheetIds()) {\n        for (const cell of Object.values(model.getters.getEvaluatedCells(sheetId))) {\n            if (cell.type === \"error\" && isLoadingError(cell)) {\n                return false;\n            }\n        }\n    }\n    return true;\n}\n\n/**\n * Return the chart figure as a base64 image.\n * \"data:image/png;base64,iVBORw0KGg...\"\n * @param {OdooSpreadsheetModel} model\n * @param {object} figure\n * @returns {string}\n */\nfunction odooChartToImage(model, figure) {\n    const runtime = model.getters.getChartRuntime(figure.id);\n    // wrap the canvas in a div with a fixed size because chart.js would\n    // fill the whole page otherwise\n    const div = document.createElement(\"div\");\n    div.style.width = `${figure.width}px`;\n    div.style.height = `${figure.height}px`;\n    const canvas = document.createElement(\"canvas\");\n    div.append(canvas);\n    canvas.setAttribute(\"width\", figure.width);\n    canvas.setAttribute(\"height\", figure.height);\n    // we have to add the canvas to the DOM otherwise it won't be rendered\n    document.body.append(div);\n    if (!(\"chartJsConfig\" in runtime)) {\n        return \"\";\n    }\n    runtime.chartJsConfig.plugins = [backgroundColorPlugin];\n    // @ts-ignore\n    const chart = new Chart(canvas, runtime.chartJsConfig);\n    const img = chart.toBase64Image();\n    chart.destroy();\n    div.remove();\n    return img;\n}\n\n/**\n * Custom chart.js plugin to set the background color of the canvas\n * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md\n */\nconst backgroundColorPlugin = {\n    id: \"customCanvasBackgroundColor\",\n    beforeDraw: (chart, args, options) => {\n        const { ctx } = chart;\n        ctx.save();\n        ctx.globalCompositeOperation = \"destination-over\";\n        ctx.fillStyle = \"#ffffff\";\n        ctx.fillRect(0, 0, chart.width, chart.height);\n        ctx.restore();\n    },\n};\n", "/** @odoo-module **/\n// @ts-check\n\n/**\n * Extract the data source id (always the first argument) from the function\n * context of the given token.\n * @param {import(\"@odoo/o-spreadsheet\").EnrichedToken} tokenAtCursor\n * @returns {string | undefined}\n */\nexport function extractDataSourceId(tokenAtCursor) {\n    const idAst = tokenAtCursor.functionContext?.args[0];\n    if (!idAst || ![\"STRING\", \"NUMBER\"].includes(idAst.type)) {\n        return;\n    }\n    return idAst.value;\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { stores } from \"@odoo/o-spreadsheet\";\nimport { useEffect, useExternalListener, useState } from \"@odoo/owl\";\n\nimport { loadBundle } from \"@web/core/assets\";\n\nconst { useStore, useStoreProvider, NotificationStore } = stores;\n/**\n * Hook that will capture the 'Ctrl+p' press that corresponds to the user intent to print a spreadsheet.\n * It will prepare the spreadsheet for printing by:\n * - displaying it in dashboard mode.\n * - altering the spreadsheet dimensions to ensure we render the whole sheet.\n * The hook will also restore the spreadsheet dimensions to their original state after the print.\n *\n * The hook will return the print preparation function to be called manually in other contexts than pressing\n * the common keybind (through a menu for instance).\n *\n * @param {() => Model | undefined} model\n * @returns {() => Promise<void>} preparePrint\n */\nexport function useSpreadsheetPrint(model) {\n    let frozenPrintState = undefined;\n    const printState = useState({ active: false });\n\n    useExternalListener(\n        window,\n        \"keydown\",\n        async (ev) => {\n            const isMeta = ev.metaKey || ev.ctrlKey;\n            if (ev.key === \"p\" && isMeta) {\n                if (!model()) {\n                    return;\n                }\n                ev.preventDefault();\n                ev.stopImmediatePropagation();\n                await preparePrint();\n            }\n        },\n        { capture: true }\n    );\n    useExternalListener(window, \"afterprint\", afterPrint);\n\n    useEffect(\n        () => {\n            if (printState.active) {\n                window.print();\n            }\n        },\n        () => [printState.active]\n    );\n\n    /**\n     * Returns the DOM position & dimensions such that the whole spreadsheet content is visible.\n     * @returns {Rect}\n     */\n    function getPrintRect() {\n        const sheetId = model().getters.getActiveSheetId();\n        const { bottom, right } = model().getters.getSheetZone(sheetId);\n        const { end: width } = model().getters.getColDimensions(sheetId, right);\n        const { end: height } = model().getters.getRowDimensions(sheetId, bottom);\n        return { x: 0, y: 0, width, height };\n    }\n\n    /**\n     * Will alter the spreadsheet dimensions to ensure we render the whole sheet.\n     * invoking this function will ultimately trigger a print of the page after a patch.\n     */\n    async function preparePrint() {\n        if (!model()) {\n            return;\n        }\n        await loadBundle(\"spreadsheet.assets_print\");\n        const { width, height } = model().getters.getSheetViewDimension();\n        const { width: widthAndHeader, height: heightAndHeader } =\n            model().getters.getSheetViewDimension();\n        const viewRect = {\n            x: widthAndHeader - width,\n            y: heightAndHeader - height,\n            width,\n            height,\n        };\n        frozenPrintState = {\n            viewRect,\n            offset: model().getters.getActiveSheetDOMScrollInfo(),\n            mode: model().config.mode,\n        };\n        model().updateMode(\"dashboard\");\n        // reset the viewport to A1 visibility\n        model().dispatch(\"SET_VIEWPORT_OFFSET\", { offsetX: 0, offsetY: 0 });\n        model().dispatch(\"RESIZE_SHEETVIEW\", {\n            ...getPrintRect(),\n        });\n        printState.active = true;\n    }\n\n    function afterPrint() {\n        if (!model()) {\n            return;\n        }\n        if (frozenPrintState) {\n            model().dispatch(\"RESIZE_SHEETVIEW\", frozenPrintState.viewRect);\n            const { scrollX: offsetX, scrollY: offsetY } = frozenPrintState.offset;\n            model().dispatch(\"SET_VIEWPORT_OFFSET\", { offsetX, offsetY });\n            model().updateMode(frozenPrintState.mode);\n            frozenPrintState = undefined;\n        }\n        printState.active = false;\n    }\n\n    return preparePrint;\n}\n\nexport function useSpreadsheetNotificationStore() {\n    /**\n     * Open a dialog to ask a confirmation to the user.\n     *\n     * @param {string} body body content to display\n     * @param {Function} confirm Callback if the user press 'Confirm'\n     */\n    function askConfirmation(body, confirm) {\n        dialog.add(ConfirmationDialog, {\n            title: _t(\"Odoo Spreadsheet\"),\n            body,\n            confirm,\n            cancel: () => {}, // Must be defined to display the Cancel button\n            confirmLabel: _t(\"Confirm\"),\n        });\n    }\n\n    /**\n     * Adds a notification to display to the user\n     * @param {{text: string, type: string, sticky: boolean }} notification\n     */\n    function notifyUser(notification) {\n        notifications.add(notification.text, {\n            type: notification.type,\n            sticky: notification.sticky,\n        });\n    }\n\n    /**\n     * Open a dialog to display an error message to the user.\n     *\n     * @param {string} body Content to display\n     * @param {function} callBack Callback function to be executed when the dialog is closed\n     */\n    function raiseError(body, callBack) {\n        dialog.add(\n            ConfirmationDialog,\n            {\n                title: _t(\"Odoo Spreadsheet\"),\n                body,\n            },\n            {\n                onClose: callBack,\n            }\n        );\n    }\n    const dialog = useService(\"dialog\");\n    const notifications = useService(\"notification\");\n    useStoreProvider();\n    const notificationStore = useStore(NotificationStore);\n    notificationStore.updateNotificationCallbacks({\n        notifyUser: notifyUser,\n        raiseError: raiseError,\n        askConfirmation: askConfirmation,\n    });\n}\n", "/** @odoo-module */\n\n/**\n * This file is meant to load the different subparts of the module\n * to guarantee their plugins are loaded in the right order\n *\n * dependency:\n *             other plugins\n *                   |\n *                  ...\n *                   |\n *                filters\n *                /\\    \\\n *               /  \\    \\\n *           pivot  list  Odoo chart\n */\n\n/** TODO: Introduce a position parameter to the plugin registry in order to load them in a specific order */\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nconst { corePluginRegistry, coreViewsPluginRegistry } = spreadsheet.registries;\n\nimport { GlobalFiltersCorePlugin, GlobalFiltersUIPlugin } from \"@spreadsheet/global_filters/index\";\nimport { PivotOdooCorePlugin, PivotUIGlobalFilterPlugin } from \"@spreadsheet/pivot/index\"; // list depends on filter for its getters\nimport { ListCorePlugin, ListUIPlugin } from \"@spreadsheet/list/index\"; // pivot depends on filter for its getters\nimport {\n    ChartOdooMenuPlugin,\n    OdooChartCorePlugin,\n    OdooChartUIPlugin,\n} from \"@spreadsheet/chart/index\"; // Odoochart depends on filter for its getters\nimport { PivotCoreGlobalFilterPlugin } from \"./pivot/plugins/pivot_core_global_filter_plugin\";\nimport { PivotOdooUIPlugin } from \"./pivot/plugins/pivot_odoo_ui_plugin\";\n\ncorePluginRegistry.add(\"OdooGlobalFiltersCorePlugin\", GlobalFiltersCorePlugin);\ncorePluginRegistry.add(\"PivotOdooCorePlugin\", PivotOdooCorePlugin);\ncorePluginRegistry.add(\"OdooPivotGlobalFiltersCorePlugin\", PivotCoreGlobalFilterPlugin);\ncorePluginRegistry.add(\"OdooListCorePlugin\", ListCorePlugin);\ncorePluginRegistry.add(\"odooChartCorePlugin\", OdooChartCorePlugin);\ncorePluginRegistry.add(\"chartOdooMenuPlugin\", ChartOdooMenuPlugin);\n\ncoreViewsPluginRegistry.add(\"OdooGlobalFiltersUIPlugin\", GlobalFiltersUIPlugin);\ncoreViewsPluginRegistry.add(\"OdooPivotGlobalFilterUIPlugin\", PivotUIGlobalFilterPlugin);\ncoreViewsPluginRegistry.add(\"OdooListUIPlugin\", ListUIPlugin);\ncoreViewsPluginRegistry.add(\"odooChartUIPlugin\", OdooChartUIPlugin);\ncoreViewsPluginRegistry.add(\"odooPivotUIPlugin\", PivotOdooUIPlugin);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nimport { IrMenuPlugin } from \"./ir_ui_menu_plugin\";\n\nimport {\n    isMarkdownIrMenuIdUrl,\n    isIrMenuXmlUrl,\n    isMarkdownViewUrl,\n    parseIrMenuXmlUrl,\n    parseViewLink,\n    parseIrMenuIdLink,\n} from \"./odoo_menu_link_cell\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { navigateTo } from \"../actions/helpers\";\n\nconst { urlRegistry, corePluginRegistry, errorTypes } = spreadsheet.registries;\nconst { EvaluationError } = spreadsheet;\n\ncorePluginRegistry.add(\"ir_ui_menu_plugin\", IrMenuPlugin);\n\nconst LINK_ERROR = \"#LINK\";\nerrorTypes.add(LINK_ERROR);\n\nclass BadOdooLinkError extends EvaluationError {\n    constructor(menuId) {\n        super(\n            sprintf(_t(\"Menu %s not found. You may not have the required access rights.\"), menuId),\n            LINK_ERROR\n        );\n    }\n}\n\nexport const spreadsheetLinkMenuCellService = {\n    dependencies: [\"menu\"],\n    start(env) {\n        function _getIrMenuByXmlId(xmlId) {\n            const menu = env.services.menu.getAll().find((menu) => menu.xmlid === xmlId);\n            if (!menu) {\n                throw new BadOdooLinkError(xmlId);\n            }\n            return menu;\n        }\n\n        urlRegistry\n            .add(\"OdooMenuIdLink\", {\n                sequence: 65,\n                match: isMarkdownIrMenuIdUrl,\n                createLink(url, label) {\n                    const menuId = parseIrMenuIdLink(url);\n                    const menu = env.services.menu.getMenu(menuId);\n                    if (!menu) {\n                        throw new BadOdooLinkError(menuId);\n                    }\n                    return {\n                        url,\n                        label: _t(label),\n                        isExternal: false,\n                        isUrlEditable: false,\n                    };\n                },\n                urlRepresentation(url) {\n                    const menuId = parseIrMenuIdLink(url);\n                    return env.services.menu.getMenu(menuId).name;\n                },\n                open(url) {\n                    const menuId = parseIrMenuIdLink(url);\n                    const menu = env.services.menu.getMenu(menuId);\n                    env.services.action.doAction(menu.actionID);\n                },\n            })\n            .add(\"OdooMenuXmlLink\", {\n                sequence: 66,\n                match: isIrMenuXmlUrl,\n                createLink(url, label) {\n                    const xmlId = parseIrMenuXmlUrl(url);\n                    _getIrMenuByXmlId(xmlId);\n                    return {\n                        url,\n                        label: _t(label),\n                        isExternal: false,\n                        isUrlEditable: false,\n                    };\n                },\n                urlRepresentation(url) {\n                    const xmlId = parseIrMenuXmlUrl(url);\n                    const menuId = _getIrMenuByXmlId(xmlId).id;\n                    return env.services.menu.getMenu(menuId).name;\n                },\n                open(url) {\n                    const xmlId = parseIrMenuXmlUrl(url);\n                    const menuId = _getIrMenuByXmlId(xmlId).id;\n                    const menu = env.services.menu.getMenu(menuId);\n                    env.services.action.doAction(menu.actionID);\n                },\n            })\n            .add(\"OdooViewLink\", {\n                sequence: 67,\n                match: isMarkdownViewUrl,\n                createLink(url, label) {\n                    return {\n                        url,\n                        label: _t(label),\n                        isExternal: false,\n                        isUrlEditable: false,\n                    };\n                },\n                urlRepresentation(url) {\n                    const actionDescription = parseViewLink(url);\n                    return actionDescription.name;\n                },\n                async open(url, env) {\n                    const { viewType, action, name } = parseViewLink(url);\n                    await navigateTo(env, action.xmlId,\n                        {\n                            type: \"ir.actions.act_window\",\n                            name: name,\n                            res_model: action.modelName,\n                            views: action.views,\n                            target: \"current\",\n                            domain: action.domain,\n                            context: action.context,\n                        },\n                        { viewType }\n                    );\n                },\n            });\n\n        return true;\n    },\n};\n\nregistry.category(\"services\").add(\"spreadsheetLinkMenuCell\", spreadsheetLinkMenuCellService);\n", "/** @odoo-module */\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\n\nexport class IrMenuPlugin extends OdooCorePlugin {\n    static getters = /** @type {const} */ ([\"getIrMenu\"]);\n    constructor(config) {\n        super(config);\n        this.env = config.custom.env;\n    }\n\n    /**\n     * Get an ir menu from an id or an xml id\n     * @param {number | string} menuId\n     * @returns {object | undefined}\n     */\n    getIrMenu(menuId) {\n        let menu = this.env.services.menu.getMenu(menuId);\n        if (!menu) {\n            menu = this.env.services.menu.getAll().find((menu) => menu.xmlid === menuId);\n        }\n        return menu;\n    }\n}\n", "/** @odoo-module */\n\nconst VIEW_PREFIX = \"odoo://view/\";\nconst IR_MENU_ID_PREFIX = \"odoo://ir_menu_id/\";\nconst IR_MENU_XML_ID_PREFIX = \"odoo://ir_menu_xml_id/\";\n\n/**\n * @typedef Action\n * @property {Array} domain\n * @property {Object} context\n * @property {string} modelName\n * @property {string} orderBy\n * @property {Array<[boolean, string]>} views\n *\n * @typedef ViewLinkDescription\n * @property {string} name Action name\n * @property {Action} action\n * @property {string} viewType Type of view (list, pivot, ...)\n */\n\n/**\n *\n * @param {string} url\n * @returns {boolean}\n */\nexport function isMarkdownViewUrl(url) {\n    return url.startsWith(VIEW_PREFIX);\n}\n\n/**\n *\n * @param {string} viewLink\n * @returns {ViewLinkDescription}\n */\nexport function parseViewLink(viewLink) {\n    if (viewLink.startsWith(VIEW_PREFIX)) {\n        return JSON.parse(viewLink.substring(VIEW_PREFIX.length));\n    }\n    throw new Error(`${viewLink} is not a valid view link`);\n}\n\n/**\n * @param {ViewLinkDescription} viewDescription Id of the ir.filter\n * @returns {string}\n */\nexport function buildViewLink(viewDescription) {\n    return `${VIEW_PREFIX}${JSON.stringify(viewDescription)}`;\n}\n\n/**\n *\n * @param {string} url\n * @returns {boolean}\n */\nexport function isMarkdownIrMenuIdUrl(url) {\n    return url.startsWith(IR_MENU_ID_PREFIX);\n}\n\n/**\n *\n * @param {string} irMenuLink\n * @returns ir.ui.menu record id\n */\nexport function parseIrMenuIdLink(irMenuLink) {\n    if (irMenuLink.startsWith(IR_MENU_ID_PREFIX)) {\n        return parseInt(irMenuLink.substring(IR_MENU_ID_PREFIX.length), 10);\n    }\n    throw new Error(`${irMenuLink} is not a valid menu id link`);\n}\n\n/**\n * @param {number} menuId\n * @returns\n */\nexport function buildIrMenuIdLink(menuId) {\n    return `${IR_MENU_ID_PREFIX}${menuId}`;\n}\n\n/**\n *\n * @param {string} url\n * @returns {boolean}\n */\nexport function isIrMenuXmlUrl(url) {\n    return url.startsWith(IR_MENU_XML_ID_PREFIX);\n}\n\n/**\n *\n * @param {string} irMenuUrl\n * @returns {string} ir.ui.menu record id\n */\nexport function parseIrMenuXmlUrl(irMenuUrl) {\n    if (irMenuUrl.startsWith(IR_MENU_XML_ID_PREFIX)) {\n        return irMenuUrl.substring(IR_MENU_XML_ID_PREFIX.length);\n    }\n    throw new Error(`${irMenuUrl} is not a valid menu xml link`);\n}\n/**\n * @param {number} menuXmlId\n * @returns\n */\nexport function buildIrMenuXmlLink(menuXmlId) {\n    return `${IR_MENU_XML_ID_PREFIX}${menuXmlId}`;\n}\n", "/** @odoo-module */\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nimport \"./list_functions\";\n\nimport { ListCorePlugin } from \"@spreadsheet/list/plugins/list_core_plugin\";\nimport { ListUIPlugin } from \"@spreadsheet/list/plugins/list_ui_plugin\";\n\nimport { SEE_RECORD_LIST, SEE_RECORD_LIST_VISIBLE } from \"./list_actions\";\nconst { inverseCommandRegistry } = spreadsheet.registries;\n\nfunction identity(cmd) {\n    return [cmd];\n}\n\nconst { coreTypes, invalidateEvaluationCommands } = spreadsheet;\n\nconst { cellMenuRegistry } = spreadsheet.registries;\n\ncoreTypes.add(\"INSERT_ODOO_LIST\");\ncoreTypes.add(\"RENAME_ODOO_LIST\");\ncoreTypes.add(\"REMOVE_ODOO_LIST\");\ncoreTypes.add(\"RE_INSERT_ODOO_LIST\");\ncoreTypes.add(\"UPDATE_ODOO_LIST_DOMAIN\");\ncoreTypes.add(\"UPDATE_ODOO_LIST\");\ncoreTypes.add(\"ADD_LIST_DOMAIN\");\ncoreTypes.add(\"DUPLICATE_ODOO_LIST\");\n\ninvalidateEvaluationCommands.add(\"UPDATE_ODOO_LIST_DOMAIN\");\ninvalidateEvaluationCommands.add(\"UPDATE_ODOO_LIST\");\ninvalidateEvaluationCommands.add(\"INSERT_ODOO_LIST\");\ninvalidateEvaluationCommands.add(\"REMOVE_ODOO_LIST\");\n\ncellMenuRegistry.add(\n    \"list_see_record\",\n    /** @type {import(\"@odoo/o-spreadsheet\").ActionSpec}*/ ({\n        name: _t(\"See record\"),\n        sequence: 200,\n        execute: async (env) => {\n            const position = env.model.getters.getActivePosition();\n            await SEE_RECORD_LIST(position, env);\n        },\n        isVisible: (env) => {\n            const position = env.model.getters.getActivePosition();\n            return SEE_RECORD_LIST_VISIBLE(position, env.model.getters);\n        },\n        icon: \"o-spreadsheet-Icon.SEE_RECORDS\",\n    })\n);\n\ninverseCommandRegistry\n    .add(\"INSERT_ODOO_LIST\", identity)\n    .add(\"UPDATE_ODOO_LIST_DOMAIN\", identity)\n    .add(\"UPDATE_ODOO_LIST\", identity)\n    .add(\"RE_INSERT_ODOO_LIST\", identity)\n    .add(\"RENAME_ODOO_LIST\", identity)\n    .add(\"REMOVE_ODOO_LIST\", identity);\n\nexport { ListCorePlugin, ListUIPlugin };\n", "/** @odoo-module */\n// @ts-check\n\nimport { astToFormula, helpers } from \"@odoo/o-spreadsheet\";\nimport { getFirstListFunction, getNumberOfListFormulas } from \"./list_helpers\";\nimport { navigateTo } from \"../actions/helpers\";\n\nconst { isMatrix } = helpers;\n\n/**\n * @param {import(\"@odoo/o-spreadsheet\").CellPosition} position\n * @param {import(\"@spreadsheet\").SpreadsheetChildEnv} env\n * @returns {Promise<void>}\n */\nexport const SEE_RECORD_LIST = async (position, env) => {\n    const cell = env.model.getters.getCorrespondingFormulaCell(position);\n    const sheetId = position.sheetId;\n    if (!cell || !cell.isFormula) {\n        return;\n    }\n    const { args } = getFirstListFunction(cell.compiledFormula.tokens);\n    const evaluatedArgs = args\n        .map(astToFormula)\n        .map((arg) => env.model.getters.evaluateFormula(sheetId, arg));\n    const listId = env.model.getters.getListIdFromPosition(position);\n    const { model, actionXmlId, context } = env.model.getters.getListDefinition(listId);\n    const dataSource = await env.model.getters.getAsyncListDataSource(listId);\n    let index = evaluatedArgs[1];\n    if (isMatrix(index)) {\n        const mainPosition = env.model.getters.getCellPosition(cell.id);\n        const rowOffset = position.row - mainPosition.row;\n        const colOffset = position.col - mainPosition.col;\n        index = index[colOffset][rowOffset];\n    }\n\n    if (typeof index !== \"number\") {\n        return;\n    }\n    const recordId = dataSource.getIdFromPosition(index - 1);\n    if (!recordId) {\n        return;\n    }\n    await navigateTo(\n        env,\n        actionXmlId,\n        {\n            type: \"ir.actions.act_window\",\n            res_model: model,\n            res_id: recordId,\n            views: [[false, \"form\"]],\n            context,\n        },\n        { viewType: \"form\" }\n    );\n};\n\n/**\n * @param {import(\"@odoo/o-spreadsheet\").CellPosition} position\n * @param {import(\"@spreadsheet\").OdooGetters} getters\n * @returns {boolean}\n */\nexport const SEE_RECORD_LIST_VISIBLE = (position, getters) => {\n    const evaluatedCell = getters.getEvaluatedCell(position);\n    const cell = getters.getCorrespondingFormulaCell(position);\n    return !!(\n        evaluatedCell.type !== \"empty\" &&\n        evaluatedCell.type !== \"error\" &&\n        evaluatedCell.value !== \"\" &&\n        cell &&\n        cell.isFormula &&\n        getNumberOfListFormulas(cell.compiledFormula.tokens) === 1 &&\n        getFirstListFunction(cell.compiledFormula.tokens).functionName === \"ODOO.LIST\"\n    );\n};\n", "/** @odoo-module */\n\nimport { OdooViewsDataSource } from \"@spreadsheet/data_sources/odoo_views_data_source\";\nimport { EvaluationError } from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    formatDateTime,\n    deserializeDateTime,\n    formatDate,\n    deserializeDate,\n} from \"@web/core/l10n/dates\";\nimport { orderByToString } from \"@web/search/utils/order_by\";\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { LOADING_ERROR } from \"@spreadsheet/data_sources/data_source\";\n\nconst { toNumber } = spreadsheet.helpers;\nconst { DEFAULT_LOCALE } = spreadsheet.constants;\n\n/**\n * @typedef {import(\"@spreadsheet\").OdooFields} OdooFields\n *\n * @typedef {Object} ListMetaData\n * @property {Array<string>} columns\n * @property {string} resModel\n * @property {OdooFields} fields\n *\n * @typedef {Object} ListSearchParams\n * @property {Array<string>} orderBy\n * @property {Object} domain\n * @property {Object} context\n */\n\nexport class ListDataSource extends OdooViewsDataSource {\n    /**\n     * @override\n     * @param {Object} services Services (see DataSource)\n     * @param {Object} params\n     * @param {ListMetaData} params.metaData\n     * @param {ListSearchParams} params.searchParams\n     * @param {number} params.limit\n     */\n    constructor(services, params) {\n        super(services, params);\n        this.maxPosition = 0;\n        this.maxPositionFetched = 0;\n        this.data = [];\n        this.fieldsToFetch = new Set();\n    }\n\n    /**\n     * Increase the max position of the list\n     * @param {number} position\n     */\n    increaseMaxPosition(position) {\n        this.maxPosition = Math.max(this.maxPosition, position);\n    }\n\n    isModelValid() {\n        return this._isModelValid;\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    addFieldToFetch(fieldName) {\n        if (this.data.length && fieldName in this.data[0]) {\n            return;\n        }\n        this.fieldsToFetch.add(fieldName);\n    }\n\n    async load(params) {\n        if (this._fetchingPromise) {\n            // if fetching is already scheduled for the next tick,\n            // wait the fetching promise to trigger the data source loading\n            // and then await the loading.\n            await this._fetchingPromise;\n            await this._loadPromise;\n            return;\n        }\n        return super.load(params);\n    }\n\n    async _load() {\n        await super._load();\n        if (this.maxPosition === 0) {\n            this.data = [];\n            return;\n        }\n        const { domain, orderBy, context } = this._searchParams;\n        const { records } = await this._orm.webSearchRead(this._metaData.resModel, domain, {\n            specification: this._getReadSpec(),\n            order: orderByToString(orderBy),\n            limit: this.maxPosition,\n            context,\n        });\n        this.data = records;\n        this.maxPositionFetched = this.maxPosition;\n    }\n\n    /**\n     * Get the fields to fetch from the server.\n     * Automatically add the currency field if the field is a monetary field.\n     */\n    _getReadSpec() {\n        const spec = {};\n        const fields = [...this.fieldsToFetch].map((f) => this.getField(f)).filter(Boolean);\n        for (const field of fields) {\n            switch (field.type) {\n                case \"monetary\":\n                    spec[field.name] = {};\n                    spec[field.currency_field] = {\n                        fields: {\n                            ...spec[field.currency_field]?.fields,\n                            name: {}, // currency code\n                            symbol: {},\n                            decimal_places: {},\n                            position: {},\n                        },\n                    };\n                    break;\n                case \"many2one\":\n                case \"many2many\":\n                case \"one2many\":\n                    spec[field.name] = {\n                        fields: {\n                            display_name: {},\n                            ...spec[field.name]?.fields,\n                        },\n                    };\n                    break;\n                default:\n                    spec[field.name] = {};\n                    break;\n            }\n        }\n        return spec;\n    }\n\n    /**\n     * @param {number} position\n     * @returns {number}\n     */\n    getIdFromPosition(position) {\n        this.assertIsValid();\n        const record = this.data[position];\n        return record ? record.id : undefined;\n    }\n\n    /**\n     * @param {string} fieldName\n     * @returns {string | EvaluationError}\n     */\n    getListHeaderValue(fieldName) {\n        if (this.isLoading()) {\n            return LOADING_ERROR;\n        }\n        if (!this._isValid || !this._isModelValid) {\n            return this._loadError;\n        }\n        if (!this.isMetaDataLoaded()) {\n            this._triggerFetching();\n            return LOADING_ERROR;\n        }\n        this.assertIsValid();\n        const field = this.getField(fieldName);\n        return field ? field.string : fieldName;\n    }\n\n    /**\n     * @param {number} position\n     * @param {string} fieldName\n     * @returns {string|number|undefined|EvaluationError}\n     */\n    getListCellValue(position, fieldName) {\n        if (this.isLoading()) {\n            return LOADING_ERROR;\n        }\n        if (!this._isValid || !this._isModelValid) {\n            return this._loadError;\n        }\n        if (position >= this.maxPositionFetched) {\n            this.increaseMaxPosition(position + 1);\n            // A reload is needed because the asked position is not already loaded.\n            this._triggerFetching();\n            return LOADING_ERROR;\n        }\n        const record = this.data[position];\n        if (!record) {\n            return \"\";\n        }\n        const field = this.getField(fieldName);\n        if (!field) {\n            return new EvaluationError(\n                _t(\"The field %s does not exist or you do not have access to that field\", fieldName)\n            );\n        }\n        if (!(fieldName in record)) {\n            this.addFieldToFetch(fieldName);\n            this._triggerFetching();\n            return LOADING_ERROR;\n        }\n        this.assertIsValid();\n        switch (field.type) {\n            case \"many2one\":\n                return record[fieldName].display_name ?? \"\";\n            case \"one2many\":\n            case \"many2many\": {\n                const labels = record[fieldName]\n                    .map(({ display_name }) => display_name)\n                    .filter((displayName) => displayName !== undefined);\n                return labels.join(\", \");\n            }\n            case \"selection\": {\n                const key = record[fieldName];\n                const value = field.selection.find((array) => array[0] === key);\n                return value ? value[1] : \"\";\n            }\n            case \"boolean\":\n                return record[fieldName] ? true : false;\n            case \"date\":\n                return record[fieldName]\n                    ? toNumber(this._formatDate(record[fieldName]), DEFAULT_LOCALE)\n                    : \"\";\n            case \"datetime\":\n                return record[fieldName]\n                    ? toNumber(this._formatDateTime(record[fieldName]), DEFAULT_LOCALE)\n                    : \"\";\n            case \"properties\": {\n                const properties = record[fieldName] || [];\n                return properties.map((property) => property.string).join(\", \");\n            }\n            case \"json\":\n                return new EvaluationError(_t('Fields of type \"%s\" are not supported', \"json\"));\n            default:\n                return record[fieldName] || \"\";\n        }\n    }\n\n    /**\n     * @param {number} position\n     * @param {string} currencyFieldName\n     * @returns {import(\"@spreadsheet/currency/currency_data_source\").Currency | undefined}\n     */\n    getListCurrency(position, currencyFieldName) {\n        this.assertIsValid();\n        const currency = this.data[position]?.[currencyFieldName];\n        if (!currency) {\n            return undefined;\n        }\n        return {\n            code: currency.name,\n            symbol: currency.symbol,\n            decimalPlaces: currency.decimal_places,\n            position: currency.position,\n        };\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _formatDateTime(dateValue) {\n        const date = deserializeDateTime(dateValue);\n        return formatDateTime(date.reconfigure({ numberingSystem: \"latn\" }), {\n            format: \"yyyy-MM-dd HH:mm:ss\",\n        });\n    }\n\n    _formatDate(dateValue) {\n        const date = deserializeDate(dateValue);\n        return formatDate(date.reconfigure({ numberingSystem: \"latn\" }), {\n            format: \"yyyy-MM-dd\",\n        });\n    }\n\n    /**\n     * Ask the parent data source to force a reload of this data source in the\n     * next clock cycle. It's necessary when this.limit was updated and new\n     * records have to be fetched.\n     */\n    _triggerFetching() {\n        if (this._fetchingPromise) {\n            return;\n        }\n        this._fetchingPromise = Promise.resolve().then(() => {\n            return new Promise((resolve) => {\n                this._fetchingPromise = undefined;\n                this.load({ reload: true });\n                resolve();\n            });\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { helpers, registries, EvaluationError } from \"@odoo/o-spreadsheet\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nconst { arg, toString, toNumber } = helpers;\nconst { functionRegistry } = registries;\n\n//--------------------------------------------------------------------------\n// Spreadsheet functions\n//--------------------------------------------------------------------------\n\nfunction assertListsExists(listId, getters) {\n    if (!getters.isExistingList(listId)) {\n        throw new EvaluationError(sprintf(_t('There is no list with id \"%s\"'), listId));\n    }\n}\n\nconst ODOO_LIST = {\n    description: _t(\"Get the value from a list.\"),\n    args: [\n        arg(\"list_id (string)\", _t(\"ID of the list.\")),\n        arg(\"index (string)\", _t(\"Position of the record in the list.\")),\n        arg(\"field_name (string)\", _t(\"Name of the field.\")),\n    ],\n    category: \"Odoo\",\n    compute: function (listId, index, fieldName) {\n        const id = toString(listId);\n        const position = toNumber(index, this.locale) - 1;\n        const _fieldName = toString(fieldName);\n        assertListsExists(id, this.getters);\n        return this.getters.getListCellValueAndFormat(id, position, _fieldName);\n    },\n    returns: [\"NUMBER\", \"STRING\"],\n};\n\nconst ODOO_LIST_HEADER = {\n    description: _t(\"Get the header of a list.\"),\n    args: [\n        arg(\"list_id (string)\", _t(\"ID of the list.\")),\n        arg(\"field_name (string)\", _t(\"Name of the field.\")),\n    ],\n    category: \"Odoo\",\n    compute: function (listId, fieldName) {\n        const id = toString(listId);\n        const field = toString(fieldName);\n        assertListsExists(id, this.getters);\n        return this.getters.getListHeaderValue(id, field);\n    },\n    returns: [\"NUMBER\", \"STRING\"],\n};\n\nfunctionRegistry.add(\"ODOO.LIST\", ODOO_LIST);\nfunctionRegistry.add(\"ODOO.LIST.HEADER\", ODOO_LIST_HEADER);\n", "/** @odoo-module */\n// @ts-check\n\nimport { helpers } from \"@odoo/o-spreadsheet\";\n\nconst { getFunctionsFromTokens } = helpers;\n\n/** @typedef {import(\"@odoo/o-spreadsheet\").Token} Token */\n\n/**\n * Parse a spreadsheet formula and detect the number of LIST functions that are\n * present in the given formula.\n *\n * @param {Token[]} tokens\n *\n * @returns {number}\n */\nexport function getNumberOfListFormulas(tokens) {\n    return getFunctionsFromTokens(tokens, [\"ODOO.LIST\", \"ODOO.LIST.HEADER\"]).length;\n}\n\n/**\n * Get the first List function description of the given formula.\n *\n * @param {Token[]} tokens\n *\n * @returns {import(\"../helpers/odoo_functions_helpers\").OdooFunctionDescription|undefined}\n */\nexport function getFirstListFunction(tokens) {\n    return getFunctionsFromTokens(tokens, [\"ODOO.LIST\", \"ODOO.LIST.HEADER\"])[0];\n}\n", "/** @odoo-module */\n\nimport { CommandResult } from \"../../o_spreadsheet/cancelled_reason\";\nimport { helpers } from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { checkFilterFieldMatching } from \"@spreadsheet/global_filters/helpers\";\nimport { Domain } from \"@web/core/domain\";\nimport { deepCopy } from \"@web/core/utils/objects\";\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\n\nconst { getMaxObjectId } = helpers;\n\n/**\n * @typedef {Object} ListDefinition\n * @property {Array<string>} columns\n * @property {Object} context\n * @property {Array<Array<string>>} domain\n * @property {string} id The id of the list\n * @property {string} model The technical name of the model we are listing\n * @property {string} name Name of the list\n * @property {Array<string>} orderBy\n * @property {string} actionXmlId\n *\n * @typedef {Object} List\n * @property {string} id\n * @property {string} dataSourceId\n * @property {ListDefinition} definition\n * @property {Object} fieldMatching\n *\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n */\n\nexport class ListCorePlugin extends OdooCorePlugin {\n    static getters = /** @type {const} */ ([\n        \"getListDisplayName\",\n        \"getListDefinition\",\n        \"getListModelDefinition\",\n        \"getListIds\",\n        \"getListName\",\n        \"getNextListId\",\n        \"isExistingList\",\n        \"getListFieldMatch\",\n        \"getListFieldMatching\",\n    ]);\n    constructor(config) {\n        super(config);\n\n        this.nextId = 1;\n        /** @type {Object.<string, List>} */\n        this.lists = {};\n\n        globalFiltersFieldMatchers[\"list\"] = {\n            getIds: () => this.getters.getListIds(),\n            getDisplayName: (listId) => this.getters.getListName(listId),\n            getTag: (listId) => sprintf(_t(\"List #%s\"), listId),\n            getFieldMatching: (listId, filterId) => this.getListFieldMatching(listId, filterId),\n            getModel: (listId) => this.getListDefinition(listId).model,\n        };\n    }\n\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"INSERT_ODOO_LIST\":\n                if (cmd.id !== this.nextId.toString()) {\n                    return CommandResult.InvalidNextId;\n                }\n                if (this.lists[cmd.id]) {\n                    return CommandResult.ListIdDuplicated;\n                }\n                break;\n            case \"DUPLICATE_ODOO_LIST\":\n                if (!this.lists[cmd.listId]) {\n                    return CommandResult.ListIdNotFound;\n                }\n                if (cmd.newListId !== this.nextId.toString()) {\n                    return CommandResult.InvalidNextId;\n                }\n                break;\n            case \"RENAME_ODOO_LIST\":\n                if (!(cmd.listId in this.lists)) {\n                    return CommandResult.ListIdNotFound;\n                }\n                if (cmd.name === \"\") {\n                    return CommandResult.EmptyName;\n                }\n                break;\n            case \"UPDATE_ODOO_LIST\":\n            case \"UPDATE_ODOO_LIST_DOMAIN\":\n                if (!(cmd.listId in this.lists)) {\n                    return CommandResult.ListIdNotFound;\n                }\n                break;\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n                if (cmd.list) {\n                    return checkFilterFieldMatching(cmd.list);\n                }\n        }\n        return CommandResult.Success;\n    }\n\n    /**\n     * Handle a spreadsheet command\n     *\n     * @param {Object} cmd Command\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"INSERT_ODOO_LIST\": {\n                const { sheetId, col, row, id, definition, linesNumber, columns } = cmd;\n                const anchor = [col, row];\n                this._addList(id, definition);\n                this._insertList(sheetId, anchor, id, linesNumber, columns);\n                this.history.update(\"nextId\", parseInt(id, 10) + 1);\n                break;\n            }\n            case \"DUPLICATE_ODOO_LIST\": {\n                const { listId, newListId } = cmd;\n                this._addList(newListId, deepCopy(this.lists[listId].definition));\n                this.history.update(\"nextId\", parseInt(newListId, 10) + 1);\n                break;\n            }\n            case \"RE_INSERT_ODOO_LIST\": {\n                const { sheetId, col, row, id, linesNumber, columns } = cmd;\n                const anchor = [col, row];\n                this._insertList(sheetId, anchor, id, linesNumber, columns);\n                break;\n            }\n            case \"RENAME_ODOO_LIST\": {\n                this.history.update(\"lists\", cmd.listId, \"definition\", \"name\", cmd.name);\n                break;\n            }\n            case \"REMOVE_ODOO_LIST\": {\n                const lists = { ...this.lists };\n                delete lists[cmd.listId];\n                this.history.update(\"lists\", lists);\n                break;\n            }\n            case \"UPDATE_ODOO_LIST_DOMAIN\": {\n                this.history.update(\n                    \"lists\",\n                    cmd.listId,\n                    \"definition\",\n                    \"searchParams\",\n                    \"domain\",\n                    cmd.domain\n                );\n                break;\n            }\n            case \"UPDATE_ODOO_LIST\": {\n                this.history.update(\"lists\", cmd.listId, \"definition\", cmd.list);\n                break;\n            }\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n                if (cmd.list) {\n                    this._setListFieldMatching(cmd.filter.id, cmd.list);\n                }\n                break;\n            case \"REMOVE_GLOBAL_FILTER\":\n                this._onFilterDeletion(cmd.id);\n                break;\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} id\n     * @returns {string}\n     */\n    getListDisplayName(id) {\n        return `(#${id}) ${this.getListName(id)}`;\n    }\n\n    /**\n     * @param {string} id\n     * @returns {string}\n     */\n    getListName(id) {\n        return this.lists[id].definition.name;\n    }\n\n    /**\n     * @param {string} id\n     * @returns {string}\n     */\n    getListFieldMatch(id) {\n        return this.lists[id].fieldMatching;\n    }\n\n    /**\n     * Retrieve all the list ids\n     *\n     * @returns {Array<string>} list ids\n     */\n    getListIds() {\n        return Object.keys(this.lists);\n    }\n\n    /**\n     * Retrieve the next available id for a new list\n     *\n     * @returns {string} id\n     */\n    getNextListId() {\n        return this.nextId.toString();\n    }\n\n    /**\n     * @param {string} id\n     * @returns {ListDefinition}\n     */\n    getListDefinition(id) {\n        const def = this.lists[id].definition;\n        return {\n            columns: [...def.metaData.columns],\n            domain: def.searchParams.domain,\n            model: def.metaData.resModel,\n            context: { ...def.searchParams.context },\n            orderBy: [...def.searchParams.orderBy],\n            id,\n            name: def.name,\n            actionXmlId: def.actionXmlId,\n        };\n    }\n\n    getListModelDefinition(id) {\n        return this.lists[id].definition;\n    }\n\n    /**\n     * Check if an id is an id of an existing list\n     *\n     * @param {string} id Id of the list\n     *\n     * @returns {boolean}\n     */\n    isExistingList(id) {\n        return id in this.lists;\n    }\n\n    // ---------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------\n\n    /**\n     * Get the current FieldMatching on a list\n     *\n     * @param {string} listId\n     * @param {string} filterId\n     */\n    getListFieldMatching(listId, filterId) {\n        return this.lists[listId].fieldMatching[filterId];\n    }\n\n    /**\n     * Sets the current FieldMatching on a list\n     *\n     * @param {string} filterId\n     * @param {Record<string,FieldMatching>} listFieldMatches\n     */\n    _setListFieldMatching(filterId, listFieldMatches) {\n        const lists = { ...this.lists };\n        for (const [listId, fieldMatch] of Object.entries(listFieldMatches)) {\n            lists[listId].fieldMatching[filterId] = fieldMatch;\n        }\n        this.history.update(\"lists\", lists);\n    }\n\n    _onFilterDeletion(filterId) {\n        const lists = { ...this.lists };\n        for (const listId in lists) {\n            this.history.update(\"lists\", listId, \"fieldMatching\", filterId, undefined);\n        }\n    }\n\n    _addList(id, definition, fieldMatching = undefined) {\n        const lists = { ...this.lists };\n        if (!fieldMatching) {\n            const model = definition.metaData.resModel;\n            fieldMatching = this.getters.getFieldMatchingForModel(model);\n        }\n        lists[id] = {\n            id,\n            definition,\n            fieldMatching,\n        };\n\n        this.history.update(\"lists\", lists);\n    }\n\n    /**\n     * Build an Odoo List\n     * @param {string} sheetId Id of the sheet\n     * @param {[number,number]} anchor Top-left cell in which the list should be inserted\n     * @param {string} id Id of the list\n     * @param {number} linesNumber Number of records to insert\n     * @param {Array<Object>} columns Columns ({name, type})\n     */\n    _insertList(sheetId, anchor, id, linesNumber, columns) {\n        this._resizeSheet(sheetId, anchor, columns.length, linesNumber + 1);\n        this._insertHeaders(sheetId, anchor, id, columns);\n        this._insertValues(sheetId, anchor, id, columns, linesNumber);\n    }\n\n    _insertHeaders(sheetId, anchor, id, columns) {\n        let [col, row] = anchor;\n        for (const column of columns) {\n            this.dispatch(\"UPDATE_CELL\", {\n                sheetId,\n                col,\n                row,\n                content: `=ODOO.LIST.HEADER(${id},\"${column.name}\")`,\n            });\n            col++;\n        }\n    }\n\n    _insertValues(sheetId, anchor, id, columns, linesNumber) {\n        let col = anchor[0];\n        let row = anchor[1] + 1;\n        for (let i = 1; i <= linesNumber; i++) {\n            col = anchor[0];\n            for (const column of columns) {\n                this.dispatch(\"UPDATE_CELL\", {\n                    sheetId,\n                    col,\n                    row,\n                    content: `=ODOO.LIST(${id},${i},\"${column.name}\")`,\n                });\n                col++;\n            }\n            row++;\n        }\n    }\n\n    /**\n     * Resize the sheet to match the size of the listing. Columns and/or rows\n     * could be added to be sure to insert the entire sheet.\n     *\n     * @param {string} sheetId Id of the sheet\n     * @param {[number,number]} anchor Anchor of the list [col,row]\n     * @param {number} columns Number of columns of the list\n     * @param {number} rows Number of rows of the list\n     */\n    _resizeSheet(sheetId, anchor, columns, rows) {\n        const numberCols = this.getters.getNumberCols(sheetId);\n        const deltaCol = numberCols - anchor[0];\n        if (deltaCol < columns) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"COL\",\n                base: numberCols - 1,\n                sheetId: sheetId,\n                quantity: columns - deltaCol,\n                position: \"after\",\n            });\n        }\n        const numberRows = this.getters.getNumberRows(sheetId);\n        const deltaRow = numberRows - anchor[1];\n        if (deltaRow < rows) {\n            this.dispatch(\"ADD_COLUMNS_ROWS\", {\n                dimension: \"ROW\",\n                base: numberRows - 1,\n                sheetId: sheetId,\n                quantity: rows - deltaRow,\n                position: \"after\",\n            });\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------\n\n    /**\n     * Import the lists\n     *\n     * @param {Object} data\n     */\n    import(data) {\n        if (data.lists) {\n            for (const [id, list] of Object.entries(data.lists)) {\n                const definition = {\n                    metaData: {\n                        resModel: list.model,\n                        columns: list.columns,\n                    },\n                    searchParams: {\n                        domain: list.domain,\n                        context: list.context,\n                        orderBy: list.orderBy,\n                    },\n                    actionXmlId: list.actionXmlId,\n                    name: list.name,\n                };\n                this._addList(id, definition, list.fieldMatching);\n            }\n        }\n        this.nextId = data.listNextId || getMaxObjectId(this.lists) + 1;\n    }\n    /**\n     * Export the lists\n     *\n     * @param {Object} data\n     */\n    export(data) {\n        data.lists = {};\n        for (const id in this.lists) {\n            data.lists[id] = JSON.parse(JSON.stringify(this.getListDefinition(id)));\n            data.lists[id].domain = new Domain(data.lists[id].domain).toJson();\n            data.lists[id].fieldMatching = this.lists[id].fieldMatching;\n        }\n        data.listNextId = this.nextId;\n    }\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { getFirstListFunction } from \"../list_helpers\";\nimport { Domain } from \"@web/core/domain\";\nimport { ListDataSource } from \"../list_data_source\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { OdooUIPlugin } from \"@spreadsheet/plugins\";\n\nconst { astToFormula, constants } = spreadsheet;\nconst { isEvaluationError } = spreadsheet.helpers;\nconst { PIVOT_TABLE_CONFIG } = constants;\n\n/**\n * @typedef {import(\"./list_core_plugin\").SpreadsheetList} SpreadsheetList\n */\n\nexport class ListUIPlugin extends OdooUIPlugin {\n    static getters = /** @type {const} */ ([\n        \"getListComputedDomain\",\n        \"getListHeaderValue\",\n        \"getListIdFromPosition\",\n        \"getListCellValueAndFormat\",\n        \"getListDataSource\",\n        \"getAsyncListDataSource\",\n        \"isListUnused\",\n    ]);\n    constructor(config) {\n        super(config);\n        /** @type {string} */\n        this.env = config.custom.env;\n\n        /** @type {Record<string, ListDataSource>} */\n        this.lists = {};\n\n        this.custom = config.custom;\n\n        globalFiltersFieldMatchers[\"list\"] = {\n            ...globalFiltersFieldMatchers[\"list\"],\n            getFields: (listId) => this.getListDataSource(listId).getFields(),\n            waitForReady: () => this.getListsWaitForReady(),\n        };\n    }\n\n    beforeHandle(cmd) {\n        switch (cmd.type) {\n            case \"START\":\n                for (const listId of this.getters.getListIds()) {\n                    this._setupListDataSource(listId, 0);\n                }\n\n                // make sure the domains are correctly set before\n                // any evaluation\n                this._addDomains();\n                break;\n        }\n    }\n\n    /**\n     * Handle a spreadsheet command\n     * @param {Object} cmd Command\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"INSERT_ODOO_LIST\": {\n                const { id, linesNumber } = cmd;\n                this._setupListDataSource(id, linesNumber);\n                break;\n            }\n            case \"INSERT_ODOO_LIST_WITH_TABLE\": {\n                this.dispatch(\"INSERT_ODOO_LIST\", cmd);\n                this._addTable(cmd);\n                break;\n            }\n            case \"RE_INSERT_ODOO_LIST_WITH_TABLE\": {\n                this.dispatch(\"RE_INSERT_ODOO_LIST\", cmd);\n                this._addTable(cmd);\n                break;\n            }\n            case \"DUPLICATE_ODOO_LIST\": {\n                this._setupListDataSource(cmd.newListId, 0);\n                break;\n            }\n            case \"REFRESH_ALL_DATA_SOURCES\":\n                this._refreshOdooLists();\n                break;\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n            case \"REMOVE_GLOBAL_FILTER\":\n            case \"SET_GLOBAL_FILTER_VALUE\":\n            case \"CLEAR_GLOBAL_FILTER_VALUE\":\n                this._addDomains();\n                break;\n            case \"UPDATE_ODOO_LIST\":\n            case \"UPDATE_ODOO_LIST_DOMAIN\": {\n                const listDefinition = this.getters.getListModelDefinition(cmd.listId);\n                const dataSourceId = this._getListDataSourceId(cmd.listId);\n                this.lists[dataSourceId] = new ListDataSource(this.custom, listDefinition);\n                this._addDomain(cmd.listId);\n                break;\n            }\n            case \"DELETE_SHEET\":\n                this.unusedLists = undefined;\n                break;\n            case \"UPDATE_CELL\":\n                this.unusedLists = undefined;\n                break;\n            case \"UNDO\":\n            case \"REDO\": {\n                this.unusedLists = undefined;\n                if (\n                    cmd.commands.find((command) =>\n                        [\n                            \"ADD_GLOBAL_FILTER\",\n                            \"EDIT_GLOBAL_FILTER\",\n                            \"REMOVE_GLOBAL_FILTER\",\n                        ].includes(command.type)\n                    )\n                ) {\n                    this._addDomains();\n                }\n\n                const updateCommands = cmd.commands.filter(\n                    (cmd) =>\n                        cmd.type === \"UPDATE_ODOO_LIST_DOMAIN\" ||\n                        cmd.type === \"UPDATE_ODOO_LIST\" ||\n                        cmd.type === \"INSERT_ODOO_LIST\"\n                );\n                for (const cmd of updateCommands) {\n                    if (!this.getters.isExistingList(cmd.listId)) {\n                        continue;\n                    }\n\n                    const listDefinition = this.getters.getListModelDefinition(cmd.listId);\n                    const dataSourceId = this._getListDataSourceId(cmd.listId);\n                    this.lists[dataSourceId] = new ListDataSource(this.custom, listDefinition);\n                    this._addDomain(cmd.listId);\n                }\n                break;\n            }\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Handlers\n    // -------------------------------------------------------------------------\n\n    _setupListDataSource(listId, limit, definition) {\n        const dataSourceId = this._getListDataSourceId(listId);\n        definition = definition || this.getters.getListModelDefinition(listId);\n        if (!(dataSourceId in this.lists)) {\n            this.lists[dataSourceId] = new ListDataSource(this.custom, { ...definition, limit });\n        }\n    }\n\n    /**\n     * Add an additional domain to a list\n     *\n     * @private\n     *\n     * @param {string} listId list id\n     *\n     */\n    _addDomain(listId) {\n        const domainList = [];\n        for (const [filterId, fieldMatch] of Object.entries(\n            this.getters.getListFieldMatch(listId)\n        )) {\n            domainList.push(this.getters.getGlobalFilterDomain(filterId, fieldMatch));\n        }\n        const domain = Domain.combine(domainList, \"AND\").toString();\n        this.getters.getListDataSource(listId).addDomain(domain);\n    }\n\n    /**\n     * Add an additional domain to all lists\n     *\n     * @private\n     *\n     */\n    _addDomains() {\n        for (const listId of this.getters.getListIds()) {\n            this._addDomain(listId);\n        }\n    }\n\n    /**\n     * Refresh the cache of a list\n     * @param {string} listId Id of the list\n     */\n    _refreshOdooList(listId) {\n        this.getters.getListDataSource(listId).load({ reload: true });\n    }\n\n    /**\n     * Refresh the cache of all the lists\n     */\n    _refreshOdooLists() {\n        for (const listId of this.getters.getListIds()) {\n            this._refreshOdooList(listId);\n        }\n    }\n\n    _getListDataSourceId(listId) {\n        return `list-${listId}`;\n    }\n\n    _getUnusedLists() {\n        if (this.unusedLists !== undefined) {\n            return this.unusedLists;\n        }\n        const unusedLists = new Set(this.getters.getListIds());\n        for (const sheetId of this.getters.getSheetIds()) {\n            for (const cellId in this.getters.getCells(sheetId)) {\n                const position = this.getters.getCellPosition(cellId);\n                const listId = this.getListIdFromPosition(position);\n                if (listId) {\n                    unusedLists.delete(listId);\n                    if (!unusedLists.size) {\n                        this.unusedLists = [];\n                        return this.unusedLists;\n                    }\n                }\n            }\n        }\n        this.unusedLists = [...unusedLists];\n        return this.unusedLists;\n    }\n\n    _getListFormat(listId, position, field) {\n        const locale = this.getters.getLocale();\n        switch (field?.type) {\n            case \"integer\":\n                return \"0\";\n            case \"float\":\n                return \"#,##0.00\";\n            case \"monetary\": {\n                const currency = this.getListCurrency(listId, position, field.currency_field);\n                if (!currency) {\n                    return \"#,##0.00\";\n                }\n                return this.getters.computeFormatFromCurrency(currency);\n            }\n            case \"date\":\n                return locale.dateFormat;\n            case \"datetime\":\n                return locale.dateFormat + \" \" + locale.timeFormat;\n            case \"char\":\n            case \"text\":\n                return \"@\";\n            default:\n                return undefined;\n        }\n    }\n\n    _addTable({ sheetId, col, row, linesNumber, columns }) {\n        const zone = {\n            left: col,\n            right: col + columns.length - 1,\n            top: row,\n            bottom: row + linesNumber,\n        };\n        this.dispatch(\"CREATE_TABLE\", {\n            tableType: \"static\",\n            sheetId,\n            ranges: [this.getters.getRangeDataFromZone(sheetId, zone)],\n            config: { ...PIVOT_TABLE_CONFIG, firstColumn: false },\n        });\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    /**\n     * Get the computed domain of a list\n     *\n     * @param {string} listId Id of the list\n     * @returns {Array}\n     */\n    getListComputedDomain(listId) {\n        return this.getters.getListDataSource(listId).getComputedDomain();\n    }\n\n    /**\n     * Get the id of the list at the given position. Returns undefined if there\n     * is no list at this position\n     *\n     * @param {{ sheetId: string; col: number; row: number}} position\n     *\n     * @returns {string|undefined}\n     */\n    getListIdFromPosition(position) {\n        const cell = this.getters.getCorrespondingFormulaCell(position);\n        const sheetId = position.sheetId;\n        if (cell && cell.isFormula) {\n            const listFunction = getFirstListFunction(cell.compiledFormula.tokens);\n            if (listFunction) {\n                const content = astToFormula(listFunction.args[0]);\n                return this.getters.evaluateFormula(sheetId, content)?.toString();\n            }\n        }\n        return undefined;\n    }\n\n    /**\n     * Get the value of a list header\n     *\n     * @param {string} listId Id of a list\n     * @param {string} fieldName\n     */\n    getListHeaderValue(listId, fieldName) {\n        return this.getters.getListDataSource(listId).getListHeaderValue(fieldName);\n    }\n\n    /**\n     * Get the value for a field of a record in the list\n     * @param {string} listId Id of the list\n     * @param {number} position Position of the record in the list\n     * @param {string} fieldName Field Name\n     *\n     * @returns {string|undefined}\n     */\n    getListCellValueAndFormat(listId, position, fieldName) {\n        const dataSource = this.getters.getListDataSource(listId);\n        dataSource.addFieldToFetch(fieldName);\n        const value = dataSource.getListCellValue(position, fieldName);\n        if (typeof value === \"object\" && isEvaluationError(value.value)) {\n            return value;\n        }\n        const field = dataSource.getField(fieldName);\n        const format = this._getListFormat(listId, position, field);\n        return { value, format };\n    }\n\n    getListCurrency(listId, position, fieldName) {\n        return this.getters.getListDataSource(listId).getListCurrency(position, fieldName);\n    }\n\n    /**\n     * @param {string} id\n     * @returns {import(\"@spreadsheet/list/list_data_source\").default|undefined}\n     */\n    getListDataSource(id) {\n        const dataSourceId = this._getListDataSourceId(id);\n        return this.lists[dataSourceId];\n    }\n\n    /**\n     * @param {string} id\n     * @returns {Promise<import(\"@spreadsheet/list/list_data_source\").ListDataSource>}\n     */\n    async getAsyncListDataSource(id) {\n        const dataSource = this.getListDataSource(id);\n        await dataSource.load();\n        return dataSource;\n    }\n\n    /**\n     *\n     * @return {Promise[]}\n     */\n    getListsWaitForReady() {\n        return this.getters\n            .getListIds()\n            .map((listId) => this.getListDataSource(listId).loadMetadata());\n    }\n\n    isListUnused(listId) {\n        return this._getUnusedLists().includes(listId);\n    }\n}\n", "/** @odoo-module */\n\nimport { Model } from \"@odoo/o-spreadsheet\";\n\n/**\n * An o-spreadsheet model with all custom Odoo plugins\n * @type {import(\"@spreadsheet\").OdooSpreadsheetModelConstructor}\n **/\nexport const OdooSpreadsheetModel = Model;\n", "/** @odoo-module */\n\n/**\n * @enum {string}\n */\nexport const CommandResult = {\n    Success: \"Success\", // should be imported from o-spreadsheet instead of redefined here\n    FilterNotFound: \"FilterNotFound\",\n    InvalidFilterMove: \"InvalidFilterMove\",\n    DuplicatedFilterLabel: \"DuplicatedFilterLabel\",\n    PivotCacheNotLoaded: \"PivotCacheNotLoaded\",\n    InvalidValueTypeCombination: \"InvalidValueTypeCombination\",\n    ListIdDuplicated: \"ListIdDuplicated\",\n    InvalidNextId: \"InvalidNextId\",\n    ListIdNotFound: \"ListIdNotFound\",\n    EmptyName: \"EmptyName\",\n    PivotIdNotFound: \"PivotIdNotFound\",\n    InvalidFieldMatch: \"InvalidFieldMatch\",\n};\n", "/** @odoo-module */\n\nimport { registries, EvaluationError } from \"@odoo/o-spreadsheet\";\n\nconst LOADING_ERROR = \"Loading...\";\n\nregistries.errorTypes.add(LOADING_ERROR);\n\n/**\n * @param {{ value: unknown }} valueOrError\n * @returns {boolean}\n */\nexport function isLoadingError(valueOrError) {\n    return valueOrError.value === LOADING_ERROR;\n}\n\nexport class LoadingDataError extends EvaluationError {\n    constructor() {\n        super(\"\", LOADING_ERROR);\n    }\n}\n", "/** @odoo-module */\n\nimport { Registry } from \"@odoo/o-spreadsheet\";\n\nexport const initCallbackRegistry = new Registry();\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\nconst { tokenize, parse, convertAstNodes, astToFormula } = spreadsheet;\nconst { corePluginRegistry, migrationStepRegistry } = spreadsheet.registries;\n\nexport const ODOO_VERSION = 12;\n\nconst MAP_V1 = {\n    PIVOT: \"ODOO.PIVOT\",\n    \"PIVOT.HEADER\": \"ODOO.PIVOT.HEADER\",\n    \"PIVOT.POSITION\": \"ODOO.PIVOT.POSITION\",\n    \"FILTER.VALUE\": \"ODOO.FILTER.VALUE\",\n    LIST: \"ODOO.LIST\",\n    \"LIST.HEADER\": \"ODOO.LIST.HEADER\",\n};\n\nconst MAP_FN_NAMES_V10 = {\n    \"ODOO.PIVOT\": \"PIVOT.VALUE\",\n    \"ODOO.PIVOT.HEADER\": \"PIVOT.HEADER\",\n    \"ODOO.PIVOT.TABLE\": \"PIVOT\",\n};\n\nconst dmyRegex = /^([0|1|2|3][1-9])\\/(0[1-9]|1[0-2])\\/(\\d{4})$/i;\n\nmigrationStepRegistry.add(\"odoo_migration\", {\n    versionFrom: \"16.1\",\n    migrate(data) {\n        return migrateOdooData(data);\n    },\n});\n\nfunction migrateOdooData(data) {\n    const version = data.odooVersion || 0;\n    if (version < 1) {\n        data = migrate0to1(data);\n    }\n    if (version < 2) {\n        data = migrate1to2(data);\n    }\n    if (version < 3) {\n        data = migrate2to3(data);\n    }\n    if (version < 4) {\n        data = migrate3to4(data);\n    }\n    if (version < 5) {\n        data = migrate4to5(data);\n    }\n    if (version < 6) {\n        data = migrate5to6(data);\n    }\n    if (version < 7) {\n        data = migrate6to7(data);\n    }\n    if (version < 8) {\n        data = migrate7to8(data);\n    }\n    if (version < 9) {\n        data = migrate8to9(data);\n    }\n    if (version < 10) {\n        data = migrate9to10(data);\n    }\n    if (version < 11) {\n        data = migrate10to11(data);\n    }\n    if (version < 12) {\n        data = migrate11to12(data);\n    }\n    return data;\n}\n\nfunction parseDimension(dimension) {\n    const [name, granularity] = dimension.split(\":\");\n    if (granularity) {\n        return { name, granularity };\n    }\n    return { name };\n}\n\nfunction renameFunctions(data, map) {\n    for (const sheet of data.sheets || []) {\n        for (const xc in sheet.cells || []) {\n            const cell = sheet.cells[xc];\n            if (cell.content && cell.content.startsWith(\"=\")) {\n                const tokens = tokenize(cell.content);\n                for (const token of tokens) {\n                    if (token.type === \"SYMBOL\" && token.value.toUpperCase() in map) {\n                        token.value = map[token.value.toUpperCase()];\n                    }\n                }\n                cell.content = tokensToString(tokens);\n            }\n        }\n    }\n    return data;\n}\n\nfunction tokensToString(tokens) {\n    return tokens.reduce((acc, token) => acc + token.value, \"\");\n}\n\nfunction migrate0to1(data) {\n    return renameFunctions(data, MAP_V1);\n}\n\nfunction migrate1to2(data) {\n    for (const sheet of data.sheets || []) {\n        for (const xc in sheet.cells || []) {\n            const cell = sheet.cells[xc];\n            if (cell.content && cell.content.startsWith(\"=\")) {\n                try {\n                    cell.content = migratePivotDaysParameters(cell.content);\n                } catch {\n                    continue;\n                }\n            }\n        }\n    }\n    return data;\n}\n\n/**\n * Migration of global filters\n */\nfunction migrate2to3(data) {\n    if (data.globalFilters) {\n        for (const gf of data.globalFilters) {\n            if (gf.fields) {\n                gf.pivotFields = gf.fields;\n                delete gf.fields;\n            }\n            if (\n                gf.type === \"date\" &&\n                typeof gf.defaultValue === \"object\" &&\n                \"year\" in gf.defaultValue\n            ) {\n                switch (gf.defaultValue.year) {\n                    case \"last_year\":\n                        gf.defaultValue.yearOffset = -1;\n                        break;\n                    case \"antepenultimate_year\":\n                        gf.defaultValue.yearOffset = -2;\n                        break;\n                    case \"this_year\":\n                    case undefined:\n                        gf.defaultValue.yearOffset = 0;\n                        break;\n                }\n                delete gf.defaultValue.year;\n            }\n            if (!gf.listFields) {\n                gf.listFields = {};\n            }\n            if (!gf.graphFields) {\n                gf.graphFields = {};\n            }\n        }\n    }\n    return data;\n}\n\n/**\n * Migration of list/pivot names\n */\nfunction migrate3to4(data) {\n    if (data.lists) {\n        for (const list of Object.values(data.lists)) {\n            list.name = list.name || list.model;\n        }\n    }\n    if (data.pivots) {\n        for (const pivot of Object.values(data.pivots)) {\n            pivot.name = pivot.name || pivot.model;\n        }\n    }\n    return data;\n}\n\nfunction migrate4to5(data) {\n    for (const filter of data.globalFilters || []) {\n        for (const [id, fm] of Object.entries(filter.pivotFields || {})) {\n            if (!(data.pivots && id in data.pivots)) {\n                delete filter.pivotFields[id];\n                continue;\n            }\n            if (!data.pivots[id].fieldMatching) {\n                data.pivots[id].fieldMatching = {};\n            }\n            data.pivots[id].fieldMatching[filter.id] = { chain: fm.field, type: fm.type };\n            if (\"offset\" in fm) {\n                data.pivots[id].fieldMatching[filter.id].offset = fm.offset;\n            }\n        }\n        delete filter.pivotFields;\n\n        for (const [id, fm] of Object.entries(filter.listFields || {})) {\n            if (!(data.lists && id in data.lists)) {\n                delete filter.listFields[id];\n                continue;\n            }\n            if (!data.lists[id].fieldMatching) {\n                data.lists[id].fieldMatching = {};\n            }\n            data.lists[id].fieldMatching[filter.id] = { chain: fm.field, type: fm.type };\n            if (\"offset\" in fm) {\n                data.lists[id].fieldMatching[filter.id].offset = fm.offset;\n            }\n        }\n        delete filter.listFields;\n\n        const findFigureFromId = (id) => {\n            for (const sheet of data.sheets) {\n                const fig = sheet.figures.find((f) => f.id === id);\n                if (fig) {\n                    return fig;\n                }\n            }\n            return undefined;\n        };\n        for (const [id, fm] of Object.entries(filter.graphFields || {})) {\n            const figure = findFigureFromId(id);\n            if (!figure) {\n                delete filter.graphFields[id];\n                continue;\n            }\n            if (!figure.data.fieldMatching) {\n                figure.data.fieldMatching = {};\n            }\n            figure.data.fieldMatching[filter.id] = { chain: fm.field, type: fm.type };\n            if (\"offset\" in fm) {\n                figure.data.fieldMatching[filter.id].offset = fm.offset;\n            }\n        }\n        delete filter.graphFields;\n    }\n    return data;\n}\n\n/**\n * Convert pivot formulas days parameters from day/month/year\n * format to the standard spreadsheet month/day/year format.\n * e.g. =PIVOT.HEADER(1,\"create_date:day\",\"30/07/2022\") becomes =PIVOT.HEADER(1,\"create_date:day\",\"07/30/2022\")\n * @param {string} formulaString\n * @returns {string}\n */\nfunction migratePivotDaysParameters(formulaString) {\n    const ast = parse(formulaString);\n    const convertedAst = convertAstNodes(ast, \"FUNCALL\", (ast) => {\n        if ([\"ODOO.PIVOT\", \"ODOO.PIVOT.HEADER\"].includes(ast.value.toUpperCase())) {\n            for (const subAst of ast.args) {\n                if (subAst.type === \"STRING\") {\n                    const date = subAst.value.match(dmyRegex);\n                    if (date) {\n                        subAst.value = `${[date[2], date[1], date[3]].join(\"/\")}`;\n                    }\n                }\n            }\n        }\n        return ast;\n    });\n    return \"=\" + astToFormula(convertedAst);\n}\n\nfunction migrate5to6(data) {\n    if (!data.globalFilters?.length) {\n        return data;\n    }\n    for (const filter of data.globalFilters) {\n        if (filter.type === \"date\" && [\"year\", \"quarter\", \"month\"].includes(filter.rangeType)) {\n            if (filter.defaultsToCurrentPeriod) {\n                filter.defaultValue = `this_${filter.rangeType}`;\n            }\n            filter.rangeType = \"fixedPeriod\";\n        }\n        delete filter.defaultsToCurrentPeriod;\n    }\n    return data;\n}\n\n/**\n * Migrate the pivot data to add the type, by default \"ODOO\". And replace the\n * pivot with a new object that contains type and definition (the old pivot).\n */\nfunction migrate6to7(data) {\n    if (data.pivots) {\n        for (const [id, definition] of Object.entries(data.pivots)) {\n            definition.measures = definition.measures.map((measure) => measure.field);\n            const fieldMatching = definition.fieldMatching;\n            delete definition.fieldMatching;\n            data.pivots[id] = {\n                type: \"ODOO\",\n                definition,\n                fieldMatching,\n            };\n        }\n    }\n    return data;\n}\n\nfunction migrate7to8(data) {\n    if (data.pivots) {\n        for (const [id, pivot] of Object.entries(data.pivots)) {\n            data.pivots[id] = {\n                type: pivot.type,\n                fieldMatching: pivot.fieldMatching,\n                ...pivot.definition,\n            };\n        }\n    }\n    return data;\n}\n\nfunction migrate8to9(data) {\n    if (data.pivots) {\n        for (const id of Object.keys(data.pivots)) {\n            data.pivots[id].formulaId = id;\n        }\n    }\n    return data;\n}\n\nfunction migrate9to10(data) {\n    return renameFunctions(data, MAP_FN_NAMES_V10);\n}\n\nfunction migrate10to11(data) {\n    if (data.pivots) {\n        for (const pivot of Object.values(data.pivots)) {\n            pivot.measures = pivot.measures.map((measure) => ({\n                name: measure,\n            }));\n            pivot.columns = pivot.colGroupBys.map(parseDimension);\n            delete pivot.colGroupBys;\n            pivot.rows = pivot.rowGroupBys.map(parseDimension);\n            delete pivot.rowGroupBys;\n        }\n    }\n    return data;\n}\n\nfunction migrate11to12(data) {\n    // remove the calls to ODOO.PIVOT.POSITION and replace\n    // the previous argument to a relative position\n    for (const sheet of data.sheets || []) {\n        for (const xc in sheet.cells || []) {\n            const cell = sheet.cells[xc];\n            if (\n                cell.content &&\n                cell.content.startsWith(\"=\") &&\n                cell.content.includes(\"ODOO.PIVOT.POSITION\")\n            ) {\n                const tokens = tokenize(cell.content);\n                /* given that odoo.pivot.position is automatically set, we know that:\n                1) it is always on the form of ODOO.PIVOT.POSITION(1, ...)\n                2) it is always preceded by a dimension of a pivot or header, inside another pivot formula\n                3) there is only one odoo.pivot.position per cell\n                4) odoo.pivot.position can only exist after the 3rd token and needs at least 7 tokens to be valid*/\n                for (let i = 2; i < tokens.length - 7; i++) {\n                    const token = tokens[i];\n                    if (\n                        token.type === \"SYMBOL\" &&\n                        token.value.toUpperCase() === \"ODOO.PIVOT.POSITION\"\n                    ) {\n                        const order = tokens[i + 6];\n                        tokens[i - 2].value = '\"#' + tokens[i - 2].value.slice(1); // \"dimension\" becomes \"#dimension\"\n                        tokens.splice(i, 7); // remove \"ODOO.PIVOT.POSITION\", \"(\", \"1\", \",\", \"dimension\", \", \", order\n                        // tokens[i-1] is the comma before odoo.pivot.position\n                        tokens[i] = order;\n                        cell.content = tokensToString(tokens);\n                    }\n                }\n            }\n        }\n    }\n    return data;\n}\n\nexport class OdooVersion extends OdooCorePlugin {\n    static getters = /** @type {const} */ ([]);\n\n    export(data) {\n        data.odooVersion = ODOO_VERSION;\n    }\n}\n\ncorePluginRegistry.add(\"odooMigration\", OdooVersion);\n", "// @odoo-module ignore\n\nodoo.define(\n    \"@odoo/o-spreadsheet\",\n    [\"@web/core/l10n/translation\", \"@spreadsheet/o_spreadsheet/o_spreadsheet\"],\n    function (require) {\n        \"use strict\";\n        const { _t, translationLoaded, translatedTerms } = require(\"@web/core/l10n/translation\");\n        const spreadsheet = require(\"@spreadsheet/o_spreadsheet/o_spreadsheet\");\n        window.o_spreadsheet = spreadsheet;\n        spreadsheet.setTranslationMethod(_t, () => translatedTerms[translationLoaded]);\n        return spreadsheet;\n    }\n);\n", "/** @odoo-module **/\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst { arg, toString } = spreadsheet.helpers;\nconst { functionRegistry } = spreadsheet.registries;\n\nfunctionRegistry.add(\"_t\", {\n    description: _t(\"Get the translated value of the given string\"),\n    args: [arg(\"value (string)\", _t(\"Value to translate.\"))],\n    compute: function (value) {\n        return _t(toString(value));\n    },\n    returns: [\"STRING\"],\n    hidden: true,\n});\n", "/** @odoo-module */\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nimport { SEE_RECORDS_PIVOT, SEE_RECORDS_PIVOT_VISIBLE } from \"./pivot_actions\";\nimport { PivotOdooCorePlugin } from \"./plugins/pivot_odoo_core_plugin\";\nimport { PivotUIGlobalFilterPlugin } from \"./plugins/pivot_ui_global_filter_plugin\";\n\nconst { coreTypes, invalidateEvaluationCommands } = spreadsheet;\n\nconst { cellMenuRegistry } = spreadsheet.registries;\n\nconst { inverseCommandRegistry } = spreadsheet.registries;\n\nfunction identity(cmd) {\n    return [cmd];\n}\n\ncoreTypes.add(\"UPDATE_ODOO_PIVOT_DOMAIN\");\n\ninvalidateEvaluationCommands.add(\"UPDATE_ODOO_PIVOT_DOMAIN\");\n\ncellMenuRegistry.add(\"pivot_see_records\", {\n    name: _t(\"See records\"),\n    sequence: 175,\n    execute: async (env) => {\n        const position = env.model.getters.getActivePosition();\n        await SEE_RECORDS_PIVOT(position, env);\n    },\n    isVisible: (env) => {\n        const position = env.model.getters.getActivePosition();\n        return SEE_RECORDS_PIVOT_VISIBLE(position, env.model.getters);\n    },\n    icon: \"o-spreadsheet-Icon.SEE_RECORDS\",\n});\n\ninverseCommandRegistry.add(\"UPDATE_ODOO_PIVOT_DOMAIN\", identity);\n\nexport { PivotOdooCorePlugin, PivotUIGlobalFilterPlugin };\n", "//@ts-check\n\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { NO_RECORD_AT_THIS_POSITION, OdooPivotModel } from \"./pivot_model\";\nimport { EvaluationError, PivotRuntimeDefinition, registries, helpers } from \"@odoo/o-spreadsheet\";\nimport { LOADING_ERROR } from \"@spreadsheet/data_sources/data_source\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { OdooPivotLoader } from \"./odoo_pivot_loader\";\n\nconst { pivotRegistry, supportedPivotPositionalFormulaRegistry } = registries;\nconst { pivotTimeAdapter, toString, areDomainArgsFieldsValid, toNormalizedPivotValue, deepEquals } =\n    helpers;\n\n/**\n * @typedef {import(\"@odoo/o-spreadsheet\").FunctionResultObject} FunctionResultObject\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotMeasure} PivotMeasure\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotDomain} PivotDomain\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotDimension} PivotDimension\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotCoreMeasure} PivotCoreMeasure\n * @typedef {import(\"@spreadsheet\").WebPivotModelParams} WebPivotModelParams\n * @typedef {import(\"@spreadsheet\").OdooPivot<OdooPivotRuntimeDefinition>} IPivot\n * @typedef {import(\"@spreadsheet\").OdooFields} OdooFields\n * @typedef {import(\"@spreadsheet\").OdooPivotCoreDefinition} OdooPivotCoreDefinition\n * @typedef {import(\"@spreadsheet\").SortedColumn} SortedColumn\n * @typedef {import(\"@spreadsheet\").OdooGetters} OdooGetters\n * @typedef {import(\"@spreadsheet/data_sources/odoo_data_provider\").OdooDataProvider} OdooDataProvider\n */\n\n/**\n * @implements {IPivot}\n */\nexport class OdooPivot {\n    /**\n     *\n     * @override\n     * @param {Object} services Services (see DataSource)\n     * @param {Object} params\n     * @param {OdooPivotCoreDefinition} params.definition\n     * @param {OdooGetters} params.getters\n     */\n    constructor(services, { definition, getters }) {\n        /** @type {\"ODOO\"} */\n        this.type = \"ODOO\";\n\n        /** @type {OdooPivotCoreDefinition} @protected */\n        this.coreDefinition = definition;\n\n        this.needsReevaluation = false;\n\n        /** @type {OdooPivotRuntimeDefinition | undefined} @protected */\n        this.runtimeDefinition = undefined;\n\n        /** @type {OdooPivotModel | undefined} @protected */\n        this.model = undefined;\n\n        /** @type {OdooGetters} @protected */\n        this.getters = getters;\n\n        /** @protected */\n        this.loader = new OdooPivotLoader(services.odooDataProvider, this._load.bind(this));\n\n        /** @type {OdooFields | undefined} @protected */\n        this._fields = undefined;\n\n        /** @protected @type {OdooDataProvider}*/\n        this.odooDataProvider = services.odooDataProvider;\n\n        /** @protected @type {Object} */\n        this.context = omit(\n            definition.context,\n            ...Object.keys(user.context),\n            \"pivot_measures\",\n            \"pivot_row_groupby\",\n            \"pivot_column_groupby\"\n        );\n\n        /** @protected */\n        this.domainWithGlobalFilters = this.coreDefinition.domain;\n    }\n\n    /**\n     * @param {OdooPivotCoreDefinition} nextDefinition\n     */\n    onDefinitionChange(nextDefinition) {\n        this.context = omit(nextDefinition.context, ...Object.keys(user.context));\n        this.domainWithGlobalFilters = nextDefinition.domain;\n        const actualDefinition = this.coreDefinition;\n        this.coreDefinition = nextDefinition;\n        if (\n            deepEquals(actualDefinition.columns, nextDefinition.columns) &&\n            deepEquals(actualDefinition.rows, nextDefinition.rows) &&\n            deepEquals(actualDefinition.sortedColumn, nextDefinition.sortedColumn) &&\n            deepEquals(actualDefinition.domain, nextDefinition.domain) &&\n            deepEquals(actualDefinition.context, nextDefinition.context) &&\n            deepEquals(actualDefinition.actionXmlId, nextDefinition.actionXmlId) &&\n            deepEquals(actualDefinition.model, nextDefinition.model)\n        ) {\n            if (deepEquals(actualDefinition.measures, nextDefinition.measures)) {\n                // Nothing change for the table structure, no need to reload the data\n                return;\n            }\n            if (\n                !this.isMeasuresChangesRequireRPC(\n                    actualDefinition.measures,\n                    nextDefinition.measures\n                )\n            ) {\n                this.coreDefinition = nextDefinition;\n                const runtimeDefinition = new OdooPivotRuntimeDefinition(\n                    this.coreDefinition,\n                    this.getFields()\n                );\n                this.model.updateMeasures(runtimeDefinition.measures);\n                return;\n            }\n        }\n        this.load({ reload: true });\n    }\n\n    /**\n     * Check if the measures changes require a reload of the data\n     *\n     * A measure change requires a reload of the data if:\n     * - a new non-computed measure is added\n     * - a non-computed measure is removed\n     * - a non-computed measure has its fieldName or aggregator changed\n     *\n     * @param {PivotCoreMeasure[]} actualMeasures\n     * @param {PivotCoreMeasure[]} nextMeasures\n     * @returns {boolean}\n     */\n    isMeasuresChangesRequireRPC(actualMeasures, nextMeasures) {\n        const nonComputedActualMeasures = actualMeasures.filter((m) => !m.computedBy);\n        const nonComputedNextMeasures = nextMeasures.filter((m) => !m.computedBy);\n        if (nonComputedActualMeasures.length !== nonComputedNextMeasures.length) {\n            return true;\n        }\n        for (const measure of nonComputedActualMeasures) {\n            const updatedMeasure = nonComputedNextMeasures.find((m) => m.id === measure.id);\n            if (\n                !updatedMeasure ||\n                updatedMeasure.fieldName !== measure.fieldName ||\n                updatedMeasure.aggregator !== measure.aggregator\n            ) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    async loadMetadata() {\n        this._fields = await this.loader.getFields(this.coreDefinition.model);\n    }\n\n    async getModelLabel() {\n        return this.loader.getModelLabel(this.coreDefinition.model);\n    }\n\n    getFields() {\n        return this._fields || {};\n    }\n\n    /**\n     * @param {object} [options] options for fetching data\n     * @param {boolean} [options.reload=false] Force the reload of the data\n     */\n    init(options) {\n        this.load(options);\n    }\n\n    /**\n     * @param {object} [options] options for fetching data\n     * @param {boolean} [options.reload=false] Force the reload of the data\n     * @returns {Promise<void>}\n     */\n    async load(options) {\n        return this.loader.load(options);\n    }\n\n    async createModelAndDefinition() {\n        await this.loadMetadata();\n        const definition = new OdooPivotRuntimeDefinition(this.coreDefinition, this.getFields());\n        const model = new OdooPivotModel(\n            { _t },\n            {\n                fields: this.getFields(),\n                definition,\n                searchParams: {\n                    context: this.context,\n                    domain: this.coreDefinition.domain,\n                },\n            },\n            {\n                orm: this.odooDataProvider.orm,\n                serverData: this.odooDataProvider.serverData,\n            }\n        );\n        return { model, definition };\n    }\n\n    async _load() {\n        const { model, definition } = await this.createModelAndDefinition();\n        this.model = model;\n        this.runtimeDefinition = definition;\n        await this.model.load({ context: this.context, domain: this.getDomainWithGlobalFilters() });\n    }\n\n    get definition() {\n        if (!this.runtimeDefinition) {\n            throw LOADING_ERROR;\n        }\n        return this.runtimeDefinition;\n    }\n\n    /**\n     * @param {import(\"@odoo/o-spreadsheet\").Maybe<FunctionResultObject>[]} args\n     *\n     * @returns {PivotDomain}\n     */\n    parseArgsToPivotDomain(args) {\n        /** @type {PivotDomain} */\n        const domain = [];\n        const stringArgs = args.map(toString);\n        for (let i = 0; i < stringArgs.length; i += 2) {\n            const nameWithGranularity = stringArgs[i];\n            if (nameWithGranularity === \"measure\") {\n                domain.push({ field: nameWithGranularity, value: stringArgs[i + 1], type: \"char\" });\n                continue;\n            }\n            const { dimensionWithGranularity, isPositional, field } =\n                this.parseGroupField(nameWithGranularity);\n            if (isPositional) {\n                const previousDomain = [\n                    ...domain,\n                    // Need to keep the \"#\"\n                    { field: nameWithGranularity, value: stringArgs[i + 1], type: \"number\" },\n                ];\n                domain.push({\n                    field: dimensionWithGranularity,\n                    value: this.getLastPivotGroupValue(previousDomain),\n                    type: field.type,\n                });\n            } else {\n                const normalizedValue = toNormalizedPivotValue(\n                    this.definition.getDimension(dimensionWithGranularity),\n                    args[i + 1]\n                );\n                domain.push({\n                    field: dimensionWithGranularity,\n                    value: normalizedValue,\n                    type: field.type,\n                });\n            }\n        }\n        return domain;\n    }\n\n    /**\n     * @param {import(\"@odoo/o-spreadsheet\").Maybe<FunctionResultObject>[]} args\n     * @returns {boolean}\n     */\n    areDomainArgsFieldsValid(args) {\n        let dimensions = args\n            .filter((_, index) => index % 2 === 0)\n            .map(toString)\n            .map((arg) =>\n                arg === \"measure\" ? \"measure\" : this.parseGroupField(arg).dimensionWithGranularity\n            );\n        if (dimensions.length && dimensions.at(-1) === \"measure\") {\n            dimensions = dimensions.slice(0, -1);\n        }\n        return areDomainArgsFieldsValid(dimensions, this.definition);\n    }\n\n    /**\n     * Retrieves the display name of the measure with the given name from the pivot model.\n     *\n     * @param {string} name - The name of the measure.\n     * @return {Object} - An object containing the display name of the measure.\n     */\n    getPivotMeasureValue(name) {\n        this.assertIsValid();\n        return {\n            value: this.getMeasure(name).displayName,\n        };\n    }\n\n    /**\n     * High level method computing the result of PIVOT.HEADER functions.\n     * - regular function 'PIVOT.HEADER(1,\"stage_id\",2,\"user_id\",6)'\n     * - measure header 'PIVOT.HEADER(1,\"stage_id\",2,\"user_id\",6,\"measure\",\"expected_revenue\")\n     * - positional header 'PIVOT.HEADER(1,\"#stage_id\",1,\"#user_id\",1)'\n     *\n     * @param {PivotDomain} domain arguments of the function (except the first one which is the pivot id)\n     * @returns {FunctionResultObject}\n     */\n    getPivotHeaderValueAndFormat(domain) {\n        this.assertIsValid();\n        const lastNode = domain.at(-1);\n        if (!lastNode) {\n            return { value: _t(\"Total\") };\n        }\n        if (lastNode.field === \"measure\") {\n            const measureId = lastNode.value;\n            return { value: this.getMeasure(measureId).displayName };\n        }\n        const value = this.model.getGroupByCellValue(lastNode.field, lastNode.value);\n        const format = this._getPivotFieldFormat(lastNode.field, lastNode.value);\n        return { value, format };\n    }\n\n    /**\n     * Get the measure object from its id\n     *\n     * @param {string} id\n     * @returns {PivotMeasure}\n     */\n    getMeasure(id) {\n        const measures = this.definition.measures;\n        const measure = measures.find((m) => m.id === id);\n        if (!measure) {\n            throw new EvaluationError(_t(\"Field %s does not exist\", id));\n        }\n        return measure;\n    }\n\n    /**\n     * @param {PivotDomain} domain\n     * @returns {string | number | boolean}\n     */\n    getLastPivotGroupValue(domain) {\n        this.assertIsValid();\n        return this.model.getLastPivotGroupValue(domain);\n    }\n\n    getTableStructure() {\n        this.assertIsValid();\n        return this.model.getTableStructure();\n    }\n\n    /**\n     * Get the format associated to a pivot field (based on its type)\n     * e.g. integer => 0, float => #,##0.00, monetary => #,##0.00\n     *\n     * @param {string} fieldName\n     * @returns {string | undefined}\n     */\n    _getPivotFieldFormat(fieldName, value) {\n        const { field, granularity } = this.parseGroupField(fieldName);\n        switch (field.type) {\n            case \"integer\":\n                return \"0\";\n            case \"float\":\n                return \"#,##0.00\";\n            case \"monetary\":\n                return this.getters.getCompanyCurrencyFormat() || \"#,##0.00\";\n            case \"date\":\n            case \"datetime\": {\n                const timeAdapter = pivotTimeAdapter(granularity);\n                return timeAdapter.toValueAndFormat(value, this.getters.getLocale()).format;\n            }\n            default:\n                return undefined;\n        }\n    }\n\n    /**\n     * @param {string} measureId\n     * @param {PivotDomain} domain\n     * @returns {FunctionResultObject}\n     */\n    getPivotCellValueAndFormat(measureId, domain) {\n        this.assertIsValid();\n        if (domain.filter((node) => node.value === NO_RECORD_AT_THIS_POSITION).length) {\n            return { value: \"\" };\n        }\n        const measure = this.getMeasure(measureId);\n        const value = this.model.getPivotCellValue(measure, domain);\n        let format;\n        switch (measure.aggregator) {\n            case \"count\":\n            case \"count_distinct\":\n                format = \"0\";\n                break;\n            default:\n                format =\n                    measure.fieldName === \"__count\"\n                        ? \"0\"\n                        : this._getPivotFieldFormat(measure.fieldName, value);\n        }\n        return { value, format };\n    }\n\n    //--------------------------------------------------------------------------\n    // Odoo specific\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {string} groupFieldString\n     */\n    parseGroupField(groupFieldString) {\n        this.assertIsValid();\n        return this.model.parseGroupField(groupFieldString);\n    }\n\n    /**\n     * @param {PivotDomain} domain\n     */\n    getPivotCellDomain(domain) {\n        this.assertIsValid();\n        return this.model.getPivotCellDomain(domain);\n    }\n\n    /**\n     * @param {PivotDimension} dimension\n     * @returns {{ value: string | number | boolean, label: string }[]}\n     */\n    getPossibleFieldValues(dimension) {\n        this.assertIsValid();\n        return this.model.getPossibleFieldValues(dimension);\n    }\n\n    async copyModelWithOriginalDomain() {\n        const { model } = await this.createModelAndDefinition();\n\n        const domain = new Domain(this.coreDefinition.domain).toList({\n            ...this.context,\n            ...user.context,\n        });\n\n        const searchParams = { context: this.context, domain };\n        await model.load(searchParams);\n        return model;\n    }\n\n    //--------------------------------------------------------------------------\n    // Loader\n    //--------------------------------------------------------------------------\n\n    get lastUpdate() {\n        return this.loader.lastUpdate;\n    }\n\n    isModelValid() {\n        return this.loader.isModelValid();\n    }\n\n    isValid() {\n        return this.loader.isValid();\n    }\n\n    assertIsValid({ throwOnError } = { throwOnError: true }) {\n        return this.loader.assertIsValid({ throwOnError });\n    }\n\n    //--------------------------------------------------------------------------\n    // Global filters\n    //--------------------------------------------------------------------------\n\n    /**\n     *\n     * @param {string} globalFilterDomain\n     */\n    addGlobalFilterDomain(globalFilterDomain) {\n        const domain = Domain.and([this.coreDefinition.domain, globalFilterDomain]).toString();\n        if (domain.toString() === new Domain(this.domainWithGlobalFilters).toString()) {\n            return;\n        }\n        this.domainWithGlobalFilters = domain;\n        if (!this.loader.hasEverBeenLoaded()) {\n            // if the data source has never been loaded, there's no point\n            // at reloading it now.\n            return;\n        }\n        this.load({ reload: true });\n    }\n\n    /**\n     * Get the computed domain of this source\n     * @returns {Array}\n     */\n    getDomainWithGlobalFilters() {\n        return new Domain(this.domainWithGlobalFilters).toList({\n            ...this.context,\n            ...user.context,\n        });\n    }\n}\n\nexport class OdooPivotRuntimeDefinition extends PivotRuntimeDefinition {\n    /**\n     * @param {OdooPivotCoreDefinition} definition\n     * @param {OdooFields} fields\n     */\n    constructor(definition, fields) {\n        super(definition, fields);\n        /** @type {Domain} */\n        this._domain = new Domain(definition.domain);\n        /** @type {Object} */\n        this._context = definition.context;\n        /** @type {string} */\n        this._model = definition.model;\n        /** @type {SortedColumn} */\n        this._sortedColumn = definition.sortedColumn;\n        for (const dimension of this.columns.concat(this.rows)) {\n            if (\n                (dimension.type === \"date\" || dimension.type === \"datetime\") &&\n                !dimension.granularity\n            ) {\n                dimension.granularity = \"month\";\n                dimension.nameWithGranularity = `${dimension.fieldName}:month`;\n            }\n        }\n    }\n\n    get sortedColumn() {\n        return this._sortedColumn;\n    }\n\n    get domain() {\n        return this._domain;\n    }\n\n    get context() {\n        return this._context;\n    }\n\n    get model() {\n        return this._model;\n    }\n\n    /**\n     * Only for Web pivot model compatibility\n     * @param {OdooFields} [fields]\n     *\n     * @returns {WebPivotModelParams}\n     */\n\n    getDefinitionForPivotModel(fields) {\n        return {\n            searchParams: {\n                domain: this.domain,\n                context: this.context,\n                groupBy: [],\n                orderBy: [],\n            },\n            metaData: {\n                sortedColumn: this.sortedColumn,\n                activeMeasures: this.measures.filter((m) => !m.computedBy).map((m) => m.fieldName),\n                resModel: this.model,\n                colGroupBys: this.columns.map((c) => c.nameWithGranularity),\n                rowGroupBys: this.rows.map((r) => r.nameWithGranularity),\n                fieldAttrs: {},\n                fields,\n            },\n        };\n    }\n}\n\nconst MEASURES_TYPES = [\"integer\", \"float\", \"monetary\"];\n\nconst granularities = [\n    \"year\",\n    \"quarter_number\",\n    \"quarter\",\n    \"month_number\",\n    \"month\",\n    \"iso_week_number\",\n    \"week\",\n    \"day_of_month\",\n    \"day\",\n    \"day_of_week\",\n];\n\npivotRegistry.add(\"ODOO\", {\n    ui: OdooPivot,\n    definition: OdooPivotRuntimeDefinition,\n    externalData: true,\n    onIterationEndEvaluation: () => {},\n    dateGranularities: [...granularities],\n    datetimeGranularities: [...granularities, \"hour_number\", \"minute_number\", \"second_number\"],\n    isMeasureCandidate: (field) =>\n        ((MEASURES_TYPES.includes(field.type) && field.aggregator) || field.type === \"many2one\") &&\n        field.name !== \"id\" &&\n        field.store,\n    isGroupable: (field) => field.groupable,\n});\n\nsupportedPivotPositionalFormulaRegistry.add(\"ODOO\", true);\n", "//@ts-check\n\nimport { EvaluationError, CellErrorType } from \"@odoo/o-spreadsheet\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport {\n    getFields,\n    LOADING_ERROR,\n    ModelNotFoundError,\n} from \"@spreadsheet/data_sources/data_source\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {import(\"@spreadsheet\").OdooFields} OdooFields\n * @typedef {import(\"@spreadsheet/data_sources/odoo_data_provider\").OdooDataProvider} OdooDataProvider\n */\n\nexport class OdooPivotLoader {\n    /**\n     * @param {OdooDataProvider} odooDataProvider\n     * @param {Function} load Function to fetch data\n     */\n    constructor(odooDataProvider, load) {\n        /** @private @type {OdooDataProvider} */\n        this.odooDataProvider = odooDataProvider;\n        /** @protected */\n        this.loadFn = load;\n\n        /**\n         * Last time that this dataSource has been updated\n         */\n        this.lastUpdate = undefined;\n\n        /** @protected */\n        this.concurrency = new KeepLast();\n        /** @protected */\n        this.loadPromise = undefined;\n        /** @protected */\n        this.isFullyLoaded = false;\n        /** @protected */\n        this._isValid = true;\n        /** @protected */\n        this.loadError = undefined;\n        /** @protected */\n        this._isModelValid = true;\n    }\n\n    /**\n     * Load data in the model\n     * @param {object} [options] options for fetching data\n     * @param {boolean} [options.reload=false] Force the reload of the data\n     *\n     * @returns {Promise} Resolved when data are fetched.\n     */\n    async load(options) {\n        if (options && options.reload) {\n            this.odooDataProvider.cancelPromise(this.loadPromise);\n            this.loadPromise = undefined;\n        }\n        if (!this.loadPromise) {\n            this.isFullyLoaded = false;\n            this._isValid = true;\n            this.loadError = undefined;\n            this.loadPromise = this.concurrency\n                .add(this.loadFn())\n                .catch((e) => {\n                    this._isValid = false;\n                    if (e instanceof ModelNotFoundError) {\n                        this._isModelValid = false;\n                        this.loadError = Object.assign(\n                            new EvaluationError(\n                                _t(`The model \"%(model)s\" does not exist.`, { model: e.message })\n                            ),\n                            {\n                                cause: e,\n                            }\n                        );\n                        return;\n                    }\n                    this.loadError = Object.assign(\n                        new EvaluationError(e instanceof RPCError ? e.data.message : e.message),\n                        { cause: e }\n                    );\n                })\n                .finally(() => {\n                    this.lastUpdate = Date.now();\n                    this.isFullyLoaded = true;\n                });\n            await this.odooDataProvider.notifyWhenPromiseResolves(this.loadPromise);\n        }\n        return this.loadPromise;\n    }\n\n    /**\n     * @param {string} model Technical name of the model\n     * @returns {Promise<OdooFields>} Fields of the model\n     */\n    async getFields(model) {\n        return getFields(this.odooDataProvider.serverData, model);\n    }\n    /**\n     * @param {string} model Technical name of the model\n     * @returns {Promise<string>} Display name of the model\n     */\n    async getModelLabel(model) {\n        const result = await this.odooDataProvider.serverData.fetch(\n            \"ir.model\",\n            \"display_name_for\",\n            [[model]]\n        );\n        return (result[0] && result[0].display_name) || \"\";\n    }\n\n    isModelValid() {\n        return this.isFullyLoaded && this._isModelValid;\n    }\n\n    isValid() {\n        return this.isFullyLoaded && this._isValid;\n    }\n\n    hasEverBeenLoaded() {\n        return this.loadPromise !== undefined;\n    }\n\n    assertIsValid({ throwOnError } = { throwOnError: true }) {\n        if (!this.isFullyLoaded) {\n            this.load();\n            if (throwOnError) {\n                throw LOADING_ERROR;\n            }\n            return LOADING_ERROR;\n        }\n        if (!this.isValid()) {\n            if (throwOnError) {\n                throw this.loadError;\n            }\n            return { value: CellErrorType.GenericError, message: this.loadError.message };\n        }\n    }\n}\n", "// @ts-check\n\nimport { navigateTo } from \"../actions/helpers\";\nimport { helpers } from \"@odoo/o-spreadsheet\";\nconst { getNumberOfPivotFunctions } = helpers;\n\n/**\n * @param {import(\"@odoo/o-spreadsheet\").CellPosition} position\n * @param {import(\"@spreadsheet\").SpreadsheetChildEnv} env\n * @returns {Promise<void>}\n */\nexport const SEE_RECORDS_PIVOT = async (position, env) => {\n    const pivotId = env.model.getters.getPivotIdFromPosition(position);\n    const pivot = env.model.getters.getPivot(pivotId);\n    await pivot.load();\n    const { model } = pivot.definition;\n    const { actionXmlId, context } = env.model.getters.getPivotCoreDefinition(pivotId);\n    const pivotCell = env.model.getters.getPivotCellFromPosition(position);\n    const domain = pivot.getPivotCellDomain(pivotCell.domain);\n    const name = await pivot.getModelLabel();\n    await navigateTo(\n        env,\n        actionXmlId,\n        {\n            type: \"ir.actions.act_window\",\n            name,\n            res_model: model,\n            views: [\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            target: \"current\",\n            domain,\n            context,\n        },\n        { viewType: \"list\" }\n    );\n};\n\n/**\n * @param {import(\"@odoo/o-spreadsheet\").CellPosition} position\n * @param {import(\"@spreadsheet\").OdooGetters} getters\n * @returns {boolean}\n */\nexport const SEE_RECORDS_PIVOT_VISIBLE = (position, getters) => {\n    const cell = getters.getCorrespondingFormulaCell(position);\n    const evaluatedCell = getters.getEvaluatedCell(position);\n    const pivotId = getters.getPivotIdFromPosition(position);\n    const pivotCell = getters.getPivotCellFromPosition(position);\n    return !!(\n        pivotId &&\n        evaluatedCell.type !== \"empty\" &&\n        evaluatedCell.type !== \"error\" &&\n        evaluatedCell.value !== \"\" &&\n        pivotCell.type !== \"EMPTY\" &&\n        cell &&\n        cell.isFormula &&\n        getNumberOfPivotFunctions(cell.compiledFormula.tokens) === 1 &&\n        getters.getPivotCoreDefinition(pivotId).type === \"ODOO\" &&\n        getters.getPivot(pivotId).getPivotCellDomain(pivotCell.domain)\n    );\n};\n\n/**\n * Check if the cell is a pivot formula and if there is a filter matching the\n * pivot domain args.\n * e.g. =PIVOT.VALUE(\"1\", \"measure\", \"country_id\", 1) matches a filter on\n * country_id.\n *\n * @returns {boolean}\n */\nexport function SET_FILTER_MATCHING_CONDITION(position, getters) {\n    if (!SEE_RECORDS_PIVOT_VISIBLE(position, getters)) {\n        return false;\n    }\n\n    const pivotId = getters.getPivotIdFromPosition(position);\n    const pivotCell = getters.getPivotCellFromPosition(position);\n    if (pivotCell.type === \"EMPTY\") {\n        return false;\n    }\n    const matchingFilters = getters.getFiltersMatchingPivotArgs(pivotId, pivotCell.domain);\n    return matchingFilters.length > 0 && pivotCell.type === \"HEADER\";\n}\n\nexport function SET_FILTER_MATCHING(position, env) {\n    const pivotId = env.model.getters.getPivotIdFromPosition(position);\n    const domain = env.model.getters.getPivotCellFromPosition(position).domain;\n    const filters = env.model.getters.getFiltersMatchingPivotArgs(pivotId, domain);\n    env.model.dispatch(\"SET_MANY_GLOBAL_FILTER_VALUE\", { filters });\n}\n", "/** @odoo-module **/\n// @ts-check\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nconst { arg, toString } = spreadsheet.helpers;\nconst { functionRegistry } = spreadsheet.registries;\n\n/**\n * @typedef {import(\"@spreadsheet\").CustomFunctionDescription} CustomFunctionDescription\n * @typedef {import(\"@odoo/o-spreadsheet\").FPayload} FPayload\n */\n\n//--------------------------------------------------------------------------\n// Spreadsheet functions\n//--------------------------------------------------------------------------\n\nconst ODOO_FILTER_VALUE = /** @satisfies {CustomFunctionDescription} */ ({\n    description: _t(\"Return the current value of a spreadsheet filter.\"),\n    args: [arg(\"filter_name (string)\", _t(\"The label of the filter whose value to return.\"))],\n    category: \"Odoo\",\n    /**\n     * @param {FPayload} filterName\n     */\n    compute: function (filterName) {\n        const unEscapedFilterName = toString(filterName).replaceAll('\\\\\"', '\"');\n        return this.getters.getFilterDisplayValue(unEscapedFilterName);\n    },\n    returns: [\"STRING\"],\n});\n\nfunctionRegistry\n    .add(\"ODOO.FILTER.VALUE\", ODOO_FILTER_VALUE);\n", "// @ts-check\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { EvaluationError, helpers } from \"@odoo/o-spreadsheet\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nconst { isDateOrDatetimeField } = helpers;\n\n/**\n * @typedef {import(\"@odoo/o-spreadsheet\").Token} Token\n * @typedef {import(\"@odoo/o-spreadsheet\").Granularity} Granularity\n * */\n\nexport const pivotFormulaRegex = /^=.*PIVOT/;\n\nconst AGGREGATOR_NAMES = {\n    count: _t(\"Count\"),\n    count_distinct: _t(\"Count Distinct\"),\n    bool_and: _t(\"Boolean And\"),\n    bool_or: _t(\"Boolean Or\"),\n    max: _t(\"Maximum\"),\n    min: _t(\"Minimum\"),\n    avg: _t(\"Average\"),\n    sum: _t(\"Sum\"),\n};\n\nconst NUMBER_AGGREGATORS = [\"max\", \"min\", \"avg\", \"sum\", \"count_distinct\", \"count\"];\nconst DATE_AGGREGATORS = [\"max\", \"min\", \"count_distinct\", \"count\"];\n\nconst AGGREGATORS_BY_FIELD_TYPE = {\n    integer: NUMBER_AGGREGATORS,\n    float: NUMBER_AGGREGATORS,\n    monetary: NUMBER_AGGREGATORS,\n    date: DATE_AGGREGATORS,\n    datetime: DATE_AGGREGATORS,\n    boolean: [\"count_distinct\", \"count\", \"bool_and\", \"bool_or\"],\n    char: [\"count_distinct\", \"count\"],\n    many2one: [\"count_distinct\", \"count\"],\n    reference: [\"count_distinct\", \"count\"],\n};\n\nexport const ODOO_AGGREGATORS = {};\n\nfor (const type in AGGREGATORS_BY_FIELD_TYPE) {\n    ODOO_AGGREGATORS[type] = {};\n    for (const aggregator of AGGREGATORS_BY_FIELD_TYPE[type]) {\n        ODOO_AGGREGATORS[type][aggregator] = AGGREGATOR_NAMES[aggregator];\n    }\n}\n\n//--------------------------------------------------------------------------\n// Public\n//--------------------------------------------------------------------------\n\n/**\n * @typedef {import(\"@spreadsheet\").OdooField} OdooField\n */\n\n/**\n * Parses the positional char (#), the field and operator string of pivot group.\n * e.g. \"create_date:month\"\n * @param {Record<string, OdooField | undefined>} allFields\n * @param {string} groupFieldString\n * @returns {{field: OdooField, granularity: Granularity, isPositional: boolean, dimensionWithGranularity: string}}\n */\nexport function parseGroupField(allFields, groupFieldString) {\n    let fieldName = groupFieldString;\n    let granularity = undefined;\n    const index = groupFieldString.indexOf(\":\");\n    if (index !== -1) {\n        fieldName = groupFieldString.slice(0, index);\n        granularity = groupFieldString.slice(index + 1);\n    }\n    const isPositional = fieldName.startsWith(\"#\");\n    fieldName = isPositional ? fieldName.substring(1) : fieldName;\n    const field = allFields[fieldName];\n    if (field === undefined) {\n        throw new EvaluationError(sprintf(_t(\"Field %s does not exist\"), fieldName));\n    }\n    if (isDateOrDatetimeField(field)) {\n        granularity = granularity || \"month\";\n    }\n    const dimensionWithGranularity = granularity ? `${fieldName}:${granularity}` : fieldName;\n    return {\n        isPositional,\n        field,\n        granularity,\n        dimensionWithGranularity,\n    };\n}\n\nexport function domainHasNoRecordAtThisPosition(domain) {\n    return domain.some((node) => node.value === \"NO_RECORD_AT_THIS_POSITION\");\n}\n", "//@ts-check\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { PivotModel } from \"@web/views/pivot/pivot_model\";\n\nimport { helpers, constants, EvaluationError, SpreadsheetPivotTable } from \"@odoo/o-spreadsheet\";\nimport { parseGroupField } from \"./pivot_helpers\";\n\nconst { toNormalizedPivotValue, toNumber, isDateOrDatetimeField, pivotTimeAdapter } = helpers;\nconst { DEFAULT_LOCALE } = constants;\n\n/**\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotTableColumn} PivotTableColumn\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotTableRow} PivotTableRow\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotDomain} PivotDomain\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotMeasure} PivotMeasure\n */\n\nexport const NO_RECORD_AT_THIS_POSITION = \"__NO_RECORD_AT_THIS_POSITION__\";\n\n/**\n * This class is an extension of PivotModel with some additional information\n * that we need in spreadsheet (display_name, isUsedInSheet, ...)\n */\nexport class OdooPivotModel extends PivotModel {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"@spreadsheet\").OdooPivotModelParams} params\n     * @param {import(\"@spreadsheet\").PivotModelServices} services\n     */\n    constructor(env, params, services) {\n        super(env, params, services);\n        /**\n         * @private\n         */\n        this._displayNames = {};\n        /**\n         * @private\n         */\n        this._displayLabels = {};\n        /**\n         * @private\n         * @type {import(\"@spreadsheet/data_sources/server_data\").ServerData}\n         */\n        this.serverData = services.serverData;\n    }\n\n    /**\n     * @param {import(\"@spreadsheet\").OdooPivotModelParams} params\n     * @param {import(\"@spreadsheet\").PivotModelServices} services\n     */\n    setup(params, services) {\n        /** This is necessary to ensure the compatibility with the PivotModel from web */\n        const p = params.definition.getDefinitionForPivotModel(params.fields);\n        p.searchParams = {\n            ...p.searchParams,\n            ...params.searchParams,\n        };\n        super.setup(p);\n        this.definition = params.definition;\n    }\n\n    /**\n     * Update the parts of the pivot measures that do not impact data fetching\n     * (do not update fieldName or aggregate).\n     * @param {PivotMeasure[]} measures\n     */\n    updateMeasures(measures) {\n        for (const measure of this.definition.measures) {\n            const updatedMeasure = measures.find((m) => m.id === measure.id);\n            if (!updatedMeasure || updatedMeasure.computedBy) {\n                continue;\n            }\n            if (\n                updatedMeasure.fieldName !== measure.fieldName ||\n                updatedMeasure.aggregator !== measure.aggregator\n            ) {\n                throw new Error(\"Measures fieldName or aggregator cannot be updated\");\n            }\n        }\n        this.definition.measures = measures;\n        this.resetTableStructure();\n    }\n\n    getDefinition() {\n        return this.definition;\n    }\n\n    async load(searchParams) {\n        if (\n            this.metaData.activeMeasures.find(\n                (fieldName) => fieldName !== \"__count\" && !this.metaData.fields[fieldName]\n            )\n        ) {\n            throw new Error(\n                _t(\n                    \"Some measures are not available: %s\",\n                    this.metaData.activeMeasures\n                        .filter((fieldName) => !this.metaData.fields[fieldName])\n                        .join(\", \")\n                )\n            );\n        }\n        searchParams.groupBy = [];\n        searchParams.orderBy = [];\n        await super.load(searchParams);\n    }\n\n    //--------------------------------------------------------------------------\n    // Evaluation\n    //--------------------------------------------------------------------------\n\n    /**\n     * Get the value of the given domain for the given measure\n     * @param {PivotMeasure} measure\n     * @param {PivotDomain} domain\n     */\n    getPivotCellValue(measure, domain) {\n        if (domain.some((node) => node.value === NO_RECORD_AT_THIS_POSITION)) {\n            return \"\";\n        }\n        const { cols, rows } = this._getColsRowsValuesFromDomain(domain);\n        const group = JSON.stringify([rows, cols]);\n        const values = this.data.measurements[group];\n        const measurementId = this._computeMeasurementId(measure);\n\n        if (values && (values[0][measurementId] || values[0][measurementId] === 0)) {\n            return values[0][measurementId];\n        }\n        return \"\";\n    }\n\n    /**\n     * Get the value of a field\n     *\n     * @example\n     * getGroupByCellValue(\"stage_id\", 42) // \"Won\"\n     *\n     * @param {string} groupFieldString Name of the field\n     * @param {string | number | boolean} groupValueString Value of the group by\n     * @returns {string | number | boolean}\n     */\n    getGroupByCellValue(groupFieldString, groupValueString) {\n        if (groupValueString === NO_RECORD_AT_THIS_POSITION) {\n            return \"\";\n        }\n        const { field, granularity, dimensionWithGranularity } =\n            this.parseGroupField(groupFieldString);\n        const dimension = this.definition.getDimension(dimensionWithGranularity);\n        const value = toNormalizedPivotValue(dimension, groupValueString);\n        const undef = _t(\"None\");\n        if (isDateOrDatetimeField(field)) {\n            const adapter = pivotTimeAdapter(granularity);\n            return adapter.toValueAndFormat(value).value;\n        }\n        if (field.relation) {\n            if (value === false) {\n                return undef;\n            }\n            return this._getRelationalDisplayName(field.relation, value);\n        }\n        const label = this._displayLabels[field.name]?.[value];\n        if (!label) {\n            return undef;\n        }\n        return label;\n    }\n\n    /**\n     * Get the value of the last group by of the function arguments\n     * e.g. in `PIVOT.HEADER(1, \"stage_id\", \"42\", \"status\", \"won\")`\n     *      the last group value is \"won\".\n     *\n     * It can also handle positional arguments.\n     * e.g. in `PIVOT.HEADER(1, \"#stage_id\", 1, \"#user_id\", 1)`\n     *      the last group value is the id of the first user of the first stage.\n     *\n     * @param {PivotDomain} domain PIVOT.HEADER arguments\n     * @returns {string | boolean | number}\n     */\n    getLastPivotGroupValue(domain) {\n        const lastNode = domain.at(-1);\n        if (!lastNode) {\n            throw new Error(\"Domain size should be at least 1\");\n        }\n        if (lastNode.field.startsWith(\"#\")) {\n            if (domain.filter((node) => node.value === NO_RECORD_AT_THIS_POSITION).length) {\n                return NO_RECORD_AT_THIS_POSITION;\n            }\n            const { dimensionWithGranularity } = this.parseGroupField(lastNode.field);\n            const { cols, rows } = this._getColsRowsValuesFromDomain(domain);\n            return this._isCol(dimensionWithGranularity) ? cols.at(-1) : rows.at(-1);\n        }\n        return lastNode.value;\n    }\n\n    //--------------------------------------------------------------------------\n    // Misc\n    //--------------------------------------------------------------------------\n\n    /**\n     * Get the Odoo domain corresponding to the given domain\n     * @param {PivotDomain} domain\n     */\n    getPivotCellDomain(domain) {\n        if (domain.some((node) => node.value === NO_RECORD_AT_THIS_POSITION)) {\n            return undefined;\n        }\n        const { cols, rows } = this._getColsRowsValuesFromDomain(domain);\n        const key = JSON.stringify([rows, cols]);\n        const domains = this.data.groupDomains[key];\n        return domains ? domains[0] : Domain.FALSE.toList();\n    }\n\n    resetTableStructure() {\n        this._tableStructure = undefined;\n    }\n\n    getTableStructure() {\n        if (this._tableStructure === undefined) {\n            // lazy build the structure\n            this._tableStructure = this._buildTableStructure();\n        }\n        return this._tableStructure;\n    }\n\n    /**\n     * @param {import(\"@odoo/o-spreadsheet\").PivotDimension} dimension\n     * @returns {{ value: string | number | boolean, label: string }[]}\n     */\n    getPossibleFieldValues(dimension) {\n        const valuesWithLabels = [];\n        const valuesUniqueness = new Set();\n        const isCol = this._isCol(dimension.nameWithGranularity);\n        const groupBys = isCol ? this.definition.columns : this.definition.rows;\n        const tree = isCol ? this.data.colGroupTree : this.data.rowGroupTree;\n        const groupByIndex = groupBys.findIndex(\n            (d) => d.nameWithGranularity === dimension.nameWithGranularity\n        );\n        const visitTree = (tree) => {\n            const { values, labels } = tree.root;\n            const value = values[groupByIndex];\n            if (value !== undefined && !valuesUniqueness.has(value)) {\n                valuesUniqueness.add(value);\n                valuesWithLabels.push({\n                    value: value,\n                    label: labels[groupByIndex].toString(),\n                });\n            }\n            [...tree.directSubTrees.values()].forEach((subTree) => {\n                visitTree(subTree);\n            });\n        };\n        visitTree(tree);\n        return valuesWithLabels;\n    }\n\n    /**\n     * @returns {SpreadsheetPivotTable}\n     */\n    _buildTableStructure() {\n        const cols = this._getSpreadsheetCols();\n        const rows = this._getSpreadsheetRows(this.data.rowGroupTree);\n        rows.push(rows.shift()); //Put the Total row at the end.\n        const measures = this.getDefinition()\n            .measures.filter((measure) => !measure.isHidden)\n            .map((measure) => measure.id);\n        /** @type {Record<string, string | undefined>} */\n        const fieldsType = {};\n        for (const columns of this.getDefinition().columns) {\n            fieldsType[columns.fieldName] = columns.type;\n        }\n        for (const row of this.getDefinition().rows) {\n            fieldsType[row.fieldName] = row.type;\n        }\n        return new SpreadsheetPivotTable(cols, rows, measures, fieldsType);\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _loadData(config) {\n        /** @type {(groupFieldString: string) => ReturnType<parseGroupField>} */\n        this.parseGroupField = parseGroupField.bind(null, this.metaData.fields);\n        /*\n         * prune is manually set to false in order to expand all the groups\n         * automatically\n         */\n        const prune = false;\n        await super._loadData(config, prune);\n\n        const registerLabels = (tree, groupBys) => {\n            const group = tree.root;\n            if (!tree.directSubTrees.size) {\n                for (let i = 0; i < group.values.length; i++) {\n                    const { field } = this.parseGroupField(groupBys[i]);\n                    if (!field.relation) {\n                        this._registerDisplayLabel(field.name, group.values[i], group.labels[i]);\n                    } else {\n                        const id = group.values[i];\n                        const displayName = group.labels[i];\n                        this._registerDisplayName(field.relation, id, displayName);\n                    }\n                }\n            }\n            [...tree.directSubTrees.values()].forEach((subTree) => {\n                registerLabels(subTree, groupBys);\n            });\n        };\n\n        registerLabels(this.data.colGroupTree, this.metaData.fullColGroupBys);\n        registerLabels(this.data.rowGroupTree, this.metaData.fullRowGroupBys);\n    }\n\n    _registerDisplayLabel(fieldName, value, label) {\n        if (!this._displayLabels[fieldName]) {\n            this._displayLabels[fieldName] = {};\n        }\n        this._displayLabels[fieldName][value] = label;\n    }\n\n    _registerDisplayName(resModel, resId, displayName) {\n        if (!this._displayNames[resModel]) {\n            this._displayNames[resModel] = {};\n        }\n        this._displayNames[resModel][resId] = displayName;\n    }\n\n    _getRelationalDisplayName(resModel, resId) {\n        const displayName =\n            this._displayNames[resModel]?.[resId] ||\n            this.serverData.batch.get(\"spreadsheet.mixin\", \"get_display_names_for_spreadsheet\", {\n                model: resModel,\n                id: resId,\n            });\n        if (!displayName) {\n            throw new EvaluationError(\n                _t(\"Unable to fetch the label of %(id)s of model %(model)s\", {\n                    id: resId,\n                    model: resModel,\n                })\n            );\n        }\n        return displayName;\n    }\n\n    _normalize(groupBy) {\n        const [fieldName] = groupBy.split(\":\");\n        const field = this.metaData.fields[fieldName];\n        if (!field) {\n            throw new EvaluationError(_t(\"Field %s does not exist\", fieldName));\n        }\n        return super._normalize(groupBy);\n    }\n\n    /**\n     * @override\n     */\n    _getGroupValues(group, groupBys) {\n        return groupBys.map((gb) => {\n            const groupBy = this._normalize(gb);\n            const { field, granularity } = this.parseGroupField(gb);\n            if (isDateOrDatetimeField(field)) {\n                return pivotTimeAdapter(granularity).normalizeServerValue(groupBy, field, group);\n            }\n            return this._sanitizeValue(group[groupBy]);\n        });\n    }\n\n    /**\n     * Check if the given field is used as col group by\n     */\n    _isCol(nameWithGranularity) {\n        return this.metaData.fullColGroupBys.includes(nameWithGranularity);\n    }\n\n    /**\n     * Check if the given field is used as row group by\n     */\n    _isRow(nameWithGranularity) {\n        return this.metaData.fullRowGroupBys.includes(nameWithGranularity);\n    }\n\n    /**\n     * Get the value of a field-value for a positional group by\n     *\n     * @param {string} dimensionWithGranularity e.g. create_date:month\n     * @param {unknown} groupValueString Value of the group by\n     * @param {(number | boolean | string)[]} rows Values for the previous row group bys\n     * @param {(number | boolean | string)[]} cols Values for the previous col group bys\n     *\n     * @private\n     * @returns {number | boolean | string}\n     */\n    _parsePivotFormulaWithPosition(dimensionWithGranularity, groupValueString, cols, rows) {\n        const position = toNumber(groupValueString, DEFAULT_LOCALE) - 1;\n        let tree;\n        if (this._isCol(dimensionWithGranularity)) {\n            tree = this.data.colGroupTree;\n            for (const col of cols) {\n                tree = tree && tree.directSubTrees.get(col);\n            }\n        } else {\n            tree = this.data.rowGroupTree;\n            for (const row of rows) {\n                tree = tree && tree.directSubTrees.get(row);\n            }\n        }\n        if (tree) {\n            const treeKeys = tree.sortedKeys || [...tree.directSubTrees.keys()];\n            const sortedKey = treeKeys[position];\n            return sortedKey !== undefined ? sortedKey : NO_RECORD_AT_THIS_POSITION;\n        }\n        return NO_RECORD_AT_THIS_POSITION;\n    }\n\n    /**\n     * Transform the given domain in the structure used in this class\n     *\n     * @param {PivotDomain} domain Domain\n     *\n     * @private\n     */\n    _getColsRowsValuesFromDomain(domain) {\n        const rows = [];\n        const cols = [];\n        for (const node of domain) {\n            const { isPositional, dimensionWithGranularity } = this.parseGroupField(node.field);\n            let value;\n            if (isPositional) {\n                value = this._parsePivotFormulaWithPosition(\n                    dimensionWithGranularity,\n                    node.value,\n                    cols,\n                    rows\n                );\n            } else {\n                const dimension = this.definition.getDimension(dimensionWithGranularity);\n                value = toNormalizedPivotValue(dimension, node.value);\n            }\n            if (this._isCol(dimensionWithGranularity)) {\n                cols.push(value);\n            } else if (this._isRow(dimensionWithGranularity)) {\n                rows.push(value);\n            } else {\n                throw new EvaluationError(\n                    sprintf(_t(\"Dimension %s is not a group by\"), dimensionWithGranularity)\n                );\n            }\n        }\n        return { rows, cols };\n    }\n\n    /**\n     * Get the row structure\n     * @returns {PivotTableRow[]}\n     */\n    _getSpreadsheetRows(tree) {\n        /**@type {PivotTableRow[]}*/\n        const rows = [];\n        const group = tree.root;\n        const indent = group.labels.length;\n        const rowGroupBys = this.metaData.fullRowGroupBys;\n\n        rows.push({\n            fields: rowGroupBys.slice(0, indent),\n            values: group.values.map((val) => val.toString()),\n            indent,\n        });\n\n        const subTreeKeys = tree.sortedKeys || [...tree.directSubTrees.keys()];\n        subTreeKeys.forEach((subTreeKey) => {\n            const subTree = tree.directSubTrees.get(subTreeKey);\n            rows.push(...this._getSpreadsheetRows(subTree));\n        });\n        return rows;\n    }\n\n    /**\n     * Get the col structure\n     * @returns {PivotTableColumn[][]}\n     */\n    _getSpreadsheetCols() {\n        const colGroupBys = this.metaData.fullColGroupBys;\n        const height = colGroupBys.length;\n        const measures = this.getDefinition().measures.filter((measure) => !measure.isHidden);\n        const measureCount = measures.length;\n        const leafCounts = this._getLeafCounts(this.data.colGroupTree);\n\n        const headers = new Array(height).fill(0).map(() => []);\n\n        function generateTreeHeaders(tree, fields) {\n            const group = tree.root;\n            const rowIndex = group.values.length;\n            if (rowIndex !== 0) {\n                const row = headers[rowIndex - 1];\n                const leafCount = leafCounts[JSON.stringify(tree.root.values)];\n                const cell = {\n                    fields: colGroupBys.slice(0, rowIndex),\n                    values: group.values.map((val) => val.toString()),\n                    width: leafCount * measureCount,\n                };\n                row.push(cell);\n            }\n\n            [...tree.directSubTrees.values()].forEach((subTree) => {\n                generateTreeHeaders(subTree, fields);\n            });\n        }\n\n        generateTreeHeaders(this.data.colGroupTree, this.metaData.fields);\n        const hasColGroupBys = this.metaData.colGroupBys.length;\n\n        // 2) generate measures row\n        const measureRow = [];\n\n        if (hasColGroupBys) {\n            headers[headers.length - 1].forEach((cell) => {\n                measures.forEach((measure) => {\n                    const measureCell = {\n                        fields: [...cell.fields, \"measure\"],\n                        values: [...cell.values, measure.id],\n                        width: 1,\n                    };\n                    measureRow.push(measureCell);\n                });\n            });\n        }\n        measures.forEach((measure) => {\n            const measureCell = {\n                fields: [\"measure\"],\n                values: [measure.id],\n                width: 1,\n            };\n            measureRow.push(measureCell);\n        });\n        headers.push(measureRow);\n        // 3) Add the total cell\n        if (headers.length === 1) {\n            headers.unshift([]); // Will add the total there\n        }\n        headers[headers.length - 2].push({\n            fields: [],\n            values: [],\n            width: measures.length,\n        });\n\n        return headers;\n    }\n\n    /**\n     * @override\n     * @protected\n     * @return {string[]}\n     */\n    _getMeasureSpecs() {\n        return this.getDefinition()\n            .measures.filter((measure) => !measure.computedBy)\n            .map((measure) => {\n                const measurementId = `${measure.fieldName}_${measure.aggregator}_id`;\n                if (measure.type === \"many2one\" && !measure.aggregator) {\n                    return `${measure.fieldName}:count_distinct`;\n                }\n                if (measure.fieldName === \"__count\") {\n                    // Remove aggregator that is not supported by python\n                    return \"__count\";\n                }\n                return measure.aggregator\n                    ? `${measurementId}:${measure.aggregator}(${measure.fieldName})`\n                    : measure.fieldName;\n            });\n    }\n\n    /**\n     * @override to add the order by clause to the read_group kwargs\n     */\n    _getSubGroups(groupBys, params) {\n        const { columns, rows } = this.getDefinition();\n        const order = columns\n            .concat(rows)\n            .filter(\n                (dimension) => dimension.order && groupBys.includes(dimension.nameWithGranularity)\n            )\n            .map((dimension) => `${dimension.nameWithGranularity} ${dimension.order}`)\n            .join(\",\");\n        params.kwargs.orderby = order;\n        return super._getSubGroups(groupBys, params);\n    }\n\n    /**\n     * This method is used to compute the identifier of a measurement in the\n     * data of the web model. It's needed since we support to define an\n     * aggregator for a field.\n     */\n    _computeMeasurementId(measure) {\n        if (measure.fieldName === \"__count\") {\n            return \"__count\";\n        }\n        if (measure.aggregator) {\n            return `${measure.fieldName}_${measure.aggregator}_id`;\n        }\n        return measure.fieldName;\n    }\n\n    /**\n     * Override to support multiple aggregators for a same field\n     *\n     * @override\n     */\n    _getMeasurements(group) {\n        return this.getDefinition()\n            .measures.filter((measure) => !measure.computedBy)\n            .reduce((measurements, measure) => {\n                const measurementId = this._computeMeasurementId(measure);\n                var measurement = group[measurementId];\n                if (measurement instanceof Array) {\n                    // case field is many2one and used as measure and groupBy simultaneously\n                    measurement = 1;\n                }\n                if (measure.type === \"boolean\" && measurement instanceof Boolean) {\n                    measurement = measurement ? 1 : 0;\n                }\n                measurements[measurementId] = measurement;\n                return measurements;\n            }, {});\n    }\n\n    /**\n     * Override to support multiple aggregators for a same field\n     *\n     * @override\n     */\n    _getCellValue(groupId, measureName, originIndexes, config) {\n        const measure = this.getDefinition().measures.find((m) => m.fieldName === measureName);\n        const measurementId = this._computeMeasurementId(measure);\n        var key = JSON.stringify(groupId);\n        if (!config.data.measurements[key]) {\n            return;\n        }\n        var values = originIndexes.map((originIndex) => {\n            return config.data.measurements[key][originIndex][measurementId];\n        });\n        return values[0];\n    }\n}\n", "/** @odoo-module */\n// @ts-check\n\nimport { registries, helpers, constants } from \"@odoo/o-spreadsheet\";\nimport { deserializeDate } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\n\nconst { pivotTimeAdapterRegistry } = registries;\nconst { formatValue, toNumber, toJsDate, toString } = helpers;\nconst { DEFAULT_LOCALE } = constants;\n\nconst { DateTime } = luxon;\n\n/**\n * The Time Adapter: Managing Time Periods for Pivot Functions\n * This is the extension of the one of o-spreadsheet to handle the normalization of\n * data received from the server. It also manage the increment of a date, used in\n * the autofill.\n *\n * Normalization Process:\n * When dealing with the server, the time adapter ensures that the received periods are\n * normalized before being stored in the datasource.\n * For example, if the server returns a day period as \"2023-12-25 22:00:00,\" the time adapter\n * transforms it into the normalized form \"12/25/2023\" for storage in the datasource.\n *\n * Example:\n * To illustrate the normalization process, let's consider the day period:\n *\n * 1. The server returns a day period as \"2023-12-25 22:00:00\"\n * 2. The time adapter normalizes this period to \"12/25/2023\" for storage in the datasource.\n *\n * By applying the appropriate normalization, the time adapter ensures that the periods from\n * different sources are consistently represented and can be effectively utilized for lookup\n * operations in the datasource.\n *\n * Implementation notes/tips:\n * - Do not mix luxon and spreadsheet dates in the same function. Timezones are not handled the same way.\n *   Spreadsheet dates are naive dates (no timezone) while luxon dates are timezone aware dates.\n *   **Don't do this**: DateTime.fromJSDate(toJsDate(value)) (it will be interpreted as UTC)\n *\n * - spreadsheet formats and luxon formats are not the same but can be equivalent.\n *   For example: \"MM/dd/yyyy\" (luxon format) is equivalent to \"mm/dd/yyyy\" (spreadsheet format)\n *\n * Limitations:\n * If a period value is provided as a **string** to a function, it will interpreted as being in the default locale.\n * e.g. in `PIVOT.VALUE(1, \"amount\", \"create_date\", \"1/5/2023\")`, the day is interpreted as being the 5th of January 2023,\n * even if the spreadsheet locale is set to French and such a date is usually interpreted as the 1st of May 2023.\n * The reason is PIVOT functions are currently generated without being aware of the spreadsheet locale.\n */\n\nconst odooNumberDateAdapter = {\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        return Number(readGroupResult[groupBy]);\n    },\n    increment(normalizedValue, step) {\n        return normalizedValue + step;\n    },\n};\n\nconst odooDayAdapter = {\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        const serverDayValue = getGroupStartingDay(field, groupBy, readGroupResult);\n        return toNumber(serverDayValue, DEFAULT_LOCALE);\n    },\n    increment(normalizedValue, step) {\n        return normalizedValue + step;\n    },\n};\n\n/**\n * Normalized value: \"2/2023\" for week 2 of 2023\n */\nconst odooWeekAdapter = {\n    normalizeFunctionValue(value) {\n        const [week, year] = toString(value).split(\"/\");\n        return `${Number(week)}/${Number(year)}`;\n    },\n    toValueAndFormat(normalizedValue, locale) {\n        const [week, year] = normalizedValue.split(\"/\");\n        return {\n            value: _t(\"W%(week)s %(year)s\", { week, year }),\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `\"${normalizedValue}\"`;\n    },\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        const weekValue = readGroupResult[groupBy];\n        const { week, year } = parseServerWeekHeader(weekValue);\n        return `${week}/${year}`;\n    },\n    increment(normalizedValue, step) {\n        const [week, year] = normalizedValue.split(\"/\");\n        const weekNumber = Number(week);\n        const yearNumber = Number(year);\n        const date = DateTime.fromObject({ weekNumber, weekYear: yearNumber });\n        const nextWeek = date.plus({ weeks: step });\n        return `${nextWeek.weekNumber}/${nextWeek.weekYear}`;\n    },\n};\n\n/**\n * normalized month value is a string formatted as \"MM/yyyy\" (luxon format)\n * e.g. \"01/2020\" for January 2020\n */\nconst odooMonthAdapter = {\n    normalizeFunctionValue(value) {\n        const date = toNumber(value, DEFAULT_LOCALE);\n        return formatValue(date, { locale: DEFAULT_LOCALE, format: \"mm/yyyy\" });\n    },\n    toValueAndFormat(normalizedValue) {\n        return {\n            value: toNumber(normalizedValue, DEFAULT_LOCALE),\n            format: \"mmmm yyyy\",\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `\"${normalizedValue}\"`;\n    },\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        const firstOfTheMonth = getGroupStartingDay(field, groupBy, readGroupResult);\n        const date = deserializeDate(firstOfTheMonth).reconfigure({ numberingSystem: \"latn\" });\n        return date.toFormat(\"MM/yyyy\");\n    },\n    increment(normalizedValue, step) {\n        return DateTime.fromFormat(normalizedValue, \"MM/yyyy\", { numberingSystem: \"latn\" })\n            .plus({ months: step })\n            .toFormat(\"MM/yyyy\");\n    },\n};\n\nconst NORMALIZED_QUARTER_REGEXP = /^[1-4]\\/\\d{4}$/;\n\n/**\n * normalized quarter value is \"quarter/year\"\n * e.g. \"1/2020\" for Q1 2020\n */\nconst odooQuarterAdapter = {\n    normalizeFunctionValue(value) {\n        // spreadsheet normally interprets \"4/2020\" as the 1st April\n        // but it should be understood as a quarter here.\n        if (typeof value === \"string\" && NORMALIZED_QUARTER_REGEXP.test(value)) {\n            return value;\n        }\n        // Any other value is interpreted as any date-like spreadsheet value\n        const dateTime = toJsDate(value, DEFAULT_LOCALE);\n        return `${dateTime.getQuarter()}/${dateTime.getFullYear()}`;\n    },\n    toValueAndFormat(normalizedValue) {\n        const [quarter, year] = normalizedValue.split(\"/\");\n        return {\n            value: _t(\"Q%(quarter)s %(year)s\", { quarter, year }),\n        };\n    },\n    toFunctionValue(normalizedValue) {\n        return `\"${normalizedValue}\"`;\n    },\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        const firstOfTheQuarter = getGroupStartingDay(field, groupBy, readGroupResult);\n        const date = deserializeDate(firstOfTheQuarter);\n        return `${date.quarter}/${date.year}`;\n    },\n    increment(normalizedValue, step) {\n        const [quarter, year] = normalizedValue.split(\"/\");\n        const date = DateTime.fromObject({ year: Number(year), month: Number(quarter) * 3 });\n        const nextQuarter = date.plus({ quarters: step });\n        return `${nextQuarter.quarter}/${nextQuarter.year}`;\n    },\n};\n\nconst odooDayOfWeekAdapter = {\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        /**\n         * 0: First day of the week in the locale.\n         */\n        return Number(readGroupResult[groupBy]) + 1;\n    },\n    increment(normalizedValue, step) {\n        return (normalizedValue + step) % 7;\n    },\n};\n\nconst odooHourNumberAdapter = {\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        return Number(readGroupResult[groupBy]);\n    },\n    increment(normalizedValue, step) {\n        return (normalizedValue + step) % 24;\n    },\n};\nconst odooMinuteNumberAdapter = {\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        return Number(readGroupResult[groupBy]);\n    },\n    increment(normalizedValue, step) {\n        return (normalizedValue + step) % 60;\n    },\n};\nconst odooSecondNumberAdapter = {\n    normalizeServerValue(groupBy, field, readGroupResult) {\n        return Number(readGroupResult[groupBy]);\n    },\n    increment(normalizedValue, step) {\n        return (normalizedValue + step) % 60;\n    },\n};\n\n/**\n * Decorate adapter functions to handle the empty value \"false\"\n */\nfunction falseHandlerDecorator(adapter) {\n    return {\n        normalizeServerValue(groupBy, field, readGroupResult) {\n            if (readGroupResult[groupBy] === false) {\n                return false;\n            }\n            return adapter.normalizeServerValue(groupBy, field, readGroupResult);\n        },\n        increment(normalizedValue, step) {\n            if (\n                normalizedValue === false ||\n                (typeof normalizedValue === \"string\" && normalizedValue.toLowerCase() === \"false\")\n            ) {\n                return false;\n            }\n            return adapter.increment(normalizedValue, step);\n        },\n        normalizeFunctionValue(value) {\n            if (value.toLowerCase() === \"false\") {\n                return false;\n            }\n            return adapter.normalizeFunctionValue(value);\n        },\n        toValueAndFormat(normalizedValue, locale) {\n            if (\n                normalizedValue === false ||\n                (typeof normalizedValue === \"string\" && normalizedValue.toLowerCase() === \"false\")\n            ) {\n                return { value: _t(\"None\") };\n            }\n            return adapter.toValueAndFormat(normalizedValue, locale);\n        },\n        toFunctionValue(value) {\n            if (value === false) {\n                return \"FALSE\";\n            }\n            return adapter.toFunctionValue(value);\n        },\n    };\n}\n\nfunction extendSpreadsheetAdapter(granularity, adapter) {\n    const originalAdapter = pivotTimeAdapterRegistry.get(granularity);\n    pivotTimeAdapterRegistry.add(\n        granularity,\n        falseHandlerDecorator({\n            ...originalAdapter,\n            ...adapter,\n        })\n    );\n}\n\npivotTimeAdapterRegistry.add(\"week\", falseHandlerDecorator(odooWeekAdapter));\npivotTimeAdapterRegistry.add(\"month\", falseHandlerDecorator(odooMonthAdapter));\npivotTimeAdapterRegistry.add(\"quarter\", falseHandlerDecorator(odooQuarterAdapter));\n\nextendSpreadsheetAdapter(\"day\", odooDayAdapter);\nextendSpreadsheetAdapter(\"year\", odooNumberDateAdapter);\nextendSpreadsheetAdapter(\"day_of_month\", odooNumberDateAdapter);\nextendSpreadsheetAdapter(\"day\", odooDayAdapter);\nextendSpreadsheetAdapter(\"iso_week_number\", odooNumberDateAdapter);\nextendSpreadsheetAdapter(\"month_number\", odooNumberDateAdapter);\nextendSpreadsheetAdapter(\"quarter_number\", odooNumberDateAdapter);\nextendSpreadsheetAdapter(\"day_of_week\", odooDayOfWeekAdapter);\nextendSpreadsheetAdapter(\"hour_number\", odooHourNumberAdapter);\nextendSpreadsheetAdapter(\"minute_number\", odooMinuteNumberAdapter);\nextendSpreadsheetAdapter(\"second_number\", odooSecondNumberAdapter);\n\n/**\n * When grouping by a time field, return\n * the group starting day (local to the timezone)\n * @param {object} field\n * @param {string} groupBy\n * @param {object} readGroup\n * @returns {string | undefined}\n */\nfunction getGroupStartingDay(field, groupBy, readGroup) {\n    if (!readGroup[\"__range\"] || !readGroup[\"__range\"][groupBy]) {\n        return undefined;\n    }\n    const sqlValue = readGroup[\"__range\"][groupBy].from;\n    if (field.type === \"date\") {\n        return sqlValue;\n    }\n    const userTz = user.tz || luxon.Settings.defaultZone.name;\n    return DateTime.fromSQL(sqlValue, { zone: \"utc\" }).setZone(userTz).toISODate();\n}\n\n/**\n * Parses a pivot week header value.\n * @param {string} value\n * @example\n * parseServerWeekHeader(\"W1 2020\") // { week: 1, year: 2020 }\n */\nfunction parseServerWeekHeader(value) {\n    // Value is always formatted as \"W1 2020\", no matter the language.\n    // Parsing this formatted value is the only way to ensure we get the same\n    // locale aware week number as the one used in the server.\n    const [week, year] = value.split(\" \");\n    return { week: Number(week.slice(1)), year: Number(year) };\n}\n", "import { registries, helpers, constants } from \"@odoo/o-spreadsheet\";\n\nconst { DEFAULT_LOCALE } = constants;\nconst { pivotToFunctionValueRegistry } = registries;\nconst { toString, toNumber } = helpers;\n\n/**\n * Add pivot formatting functions to support odoo specific fields\n * in spreadsheet\n */\n\nconst toFunctionValueDateTime = pivotToFunctionValueRegistry.get(\"date\");\n\nfunction isFalseValue(value) {\n    return value === false || (typeof value === \"string\" && value.toLowerCase() === \"false\");\n}\n\nfunction _toDate(value, granularity) {\n    if (isFalseValue(value)) {\n        return \"FALSE\";\n    }\n    if (!granularity) {\n        granularity = \"month\";\n    }\n    return toFunctionValueDateTime(value, granularity);\n}\n\nfunction _toString(value) {\n    if (isFalseValue(value)) {\n        return \"FALSE\";\n    }\n    return `\"${toString(value).replace(/\"/g, '\\\\\"')}\"`;\n}\nfunction _toNumber(value) {\n    if (isFalseValue(value)) {\n        return \"FALSE\";\n    }\n    return `${toNumber(value, DEFAULT_LOCALE)}`;\n}\n\npivotToFunctionValueRegistry\n    .add(\"text\", _toString)\n    .add(\"selection\", _toString)\n    .add(\"char\", _toString)\n    .add(\"integer\", _toNumber)\n    .add(\"monetary\", _toNumber)\n    .add(\"many2one\", _toNumber)\n    .add(\"many2many\", _toNumber)\n    .add(\"float\", _toNumber)\n    .add(\"date\", _toDate)\n    .add(\"datetime\", _toDate);\n", "import { registries, helpers, constants } from \"@odoo/o-spreadsheet\";\n\nconst { DEFAULT_LOCALE } = constants;\nconst { pivotNormalizationValueRegistry } = registries;\nconst { toString, toNumber } = helpers;\n\n/**\n * Add pivot normalizaton functions to support odoo specific fields\n * in spreadsheet\n */\n\npivotNormalizationValueRegistry\n    .add(\"text\", (value) => toString(value))\n    .add(\"selection\", (value) => toString(value))\n    .add(\"monetary\", (value) => toNumber(value, DEFAULT_LOCALE))\n    .add(\"many2one\", (value) => toNumber(value, DEFAULT_LOCALE))\n    .add(\"many2many\", (value) => toNumber(value, DEFAULT_LOCALE))\n    .add(\"float\", (value) => toNumber(value, DEFAULT_LOCALE));\n", "/** @odoo-module */\n// @ts-check\n/**\n *\n * @typedef {import(\"@spreadsheet\").OdooPivotDefinition} OdooPivotDefinition\n * @typedef {import(\"@spreadsheet\").AllCoreCommand} AllCoreCommand\n * @typedef {import(\"@spreadsheet\").GFLocalPivot} GFLocalPivot\n *\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n */\n\nimport { CommandResult } from \"../../o_spreadsheet/cancelled_reason\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { checkFilterFieldMatching } from \"@spreadsheet/global_filters/helpers\";\nimport { deepCopy } from \"@web/core/utils/objects\";\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\n\nexport class PivotCoreGlobalFilterPlugin extends OdooCorePlugin {\n    static getters = /** @type {const} */ ([\"getPivotFieldMatch\", \"getPivotFieldMatching\"]);\n    constructor(config) {\n        super(config);\n\n        /** @type {Object.<string, GFLocalPivot>} */\n        this.pivots = {};\n        globalFiltersFieldMatchers[\"pivot\"] = {\n            getIds: () =>\n                this.getters\n                    .getPivotIds()\n                    .filter(\n                        (id) =>\n                            this.getters.getPivotCoreDefinition(id).type === \"ODOO\" &&\n                            id in this.pivots\n                    ),\n            getDisplayName: (pivotId) => this.getters.getPivotName(pivotId),\n            getTag: (pivotId) => sprintf(_t(\"Pivot #%s\"), this.getters.getPivotFormulaId(pivotId)),\n            getFieldMatching: (pivotId, filterId) => this.getPivotFieldMatching(pivotId, filterId),\n            getModel: (pivotId) => {\n                const pivot = this.getters.getPivotCoreDefinition(pivotId);\n                return pivot.type === \"ODOO\" && pivot.model;\n            },\n        };\n    }\n\n    /**\n     * @param {AllCoreCommand} cmd\n     *\n     * @returns {string | string[]}\n     */\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n                if (cmd.pivot) {\n                    return checkFilterFieldMatching(cmd.pivot);\n                }\n        }\n        return CommandResult.Success;\n    }\n\n    /**\n     * @param {AllCoreCommand} cmd\n     *\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_PIVOT\": {\n                if (cmd.pivot.type === \"ODOO\") {\n                    this._addPivot(cmd.pivotId, undefined);\n                }\n                break;\n            }\n            case \"REMOVE_PIVOT\": {\n                this.history.update(\"pivots\", cmd.pivotId, undefined);\n                break;\n            }\n            case \"DUPLICATE_PIVOT\": {\n                const { pivotId, newPivotId } = cmd;\n                const pivotDefinition = this.getters.getPivotCoreDefinition(pivotId);\n                if(pivotDefinition.type !== \"ODOO\") {\n                    break;\n                }\n                const pivot = deepCopy(this.pivots[pivotId]);\n                this._addPivot(newPivotId, pivot.fieldMatching);\n                break;\n            }\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n                if (cmd.pivot) {\n                    this._setPivotFieldMatching(cmd.filter.id, cmd.pivot);\n                }\n                break;\n            case \"REMOVE_GLOBAL_FILTER\":\n                this._onFilterDeletion(cmd.id);\n                break;\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} id\n     * @returns {Record<string, FieldMatching>}\n     */\n    getPivotFieldMatch(id) {\n        const pivot = this.getters.getPivotCoreDefinition(id);\n        if (pivot.type !== \"ODOO\") {\n            return {};\n        }\n        return this.pivots[id].fieldMatching;\n    }\n\n    /**\n     * Get the current pivotFieldMatching on a pivot\n     *\n     * @param {string} pivotId\n     * @param {string} filterId\n     */\n    getPivotFieldMatching(pivotId, filterId) {\n        return this.getPivotFieldMatch(pivotId)[filterId];\n    }\n\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n\n    /**\n     * Sets the current pivotFieldMatching on a pivot\n     *\n     * @param {string} filterId\n     * @param {Record<string,FieldMatching>} pivotFieldMatches\n     */\n    _setPivotFieldMatching(filterId, pivotFieldMatches) {\n        const pivots = { ...this.pivots };\n        for (const [pivotId, fieldMatch] of Object.entries(pivotFieldMatches)) {\n            const pivot = this.getters.getPivotCoreDefinition(pivotId);\n            if (pivot.type !== \"ODOO\") {\n                continue;\n            }\n            this.pivots[pivotId].fieldMatching[filterId] = fieldMatch;\n        }\n        this.history.update(\"pivots\", pivots);\n    }\n\n    _onFilterDeletion(filterId) {\n        const pivots = { ...this.pivots };\n        for (const pivotId in pivots) {\n            this.history.update(\"pivots\", pivotId, \"fieldMatching\", filterId, undefined);\n        }\n    }\n\n    /**\n     * @param {string} id\n     * @param {Record<string, FieldMatching>} [fieldMatching]\n     */\n    _addPivot(id, fieldMatching = undefined) {\n        const pivot = this.getters.getPivotCoreDefinition(id);\n        if (pivot.type === \"ODOO\") {\n            const pivots = { ...this.pivots };\n            const model = pivot.model;\n            pivots[id] = {\n                id,\n                fieldMatching: fieldMatching || this.getters.getFieldMatchingForModel(model),\n            };\n            this.history.update(\"pivots\", pivots);\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------\n\n    /**\n     * Import the pivots\n     *\n     * @param {Object} data\n     */\n    import(data) {\n        if (data.pivots) {\n            for (const [id, pivot] of Object.entries(data.pivots)) {\n                this._addPivot(id, pivot.fieldMatching);\n            }\n        }\n    }\n    /**\n     * Export the pivots\n     *\n     * @param {Object} data\n     */\n    export(data) {\n        for (const id in this.pivots) {\n            const pivot = this.getters.getPivotCoreDefinition(id);\n            data.pivots[id].fieldMatching =\n                pivot.type === \"ODOO\" ? this.pivots[id].fieldMatching : {};\n        }\n    }\n}\n", "/** @odoo-module */\n// @ts-check\n\nimport { Domain } from \"@web/core/domain\";\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\n\nexport class PivotOdooCorePlugin extends OdooCorePlugin {\n    handle(cmd) {\n        switch (cmd.type) {\n            // this command is deprecated. use UPDATE_PIVOT instead\n            case \"UPDATE_ODOO_PIVOT_DOMAIN\":\n                this.dispatch(\"UPDATE_PIVOT\", {\n                    pivotId: cmd.pivotId,\n                    pivot: {\n                        ...this.getters.getPivotCoreDefinition(cmd.pivotId),\n                        domain: cmd.domain,\n                    },\n                });\n                break;\n        }\n    }\n\n    /**\n     * Transform the domain of a pivot definition to a more readable format\n     *\n     * @param {Object} data\n     */\n    export(data) {\n        if (data.pivots) {\n            for (const id in data.pivots) {\n                if (data.pivots[id].type === \"ODOO\") {\n                    data.pivots[id].domain = new Domain(data.pivots[id].domain).toJson();\n                }\n            }\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { OdooUIPlugin } from \"@spreadsheet/plugins\";\nimport { helpers } from \"@odoo/o-spreadsheet\";\n\nconst { UNDO_REDO_PIVOT_COMMANDS } = helpers;\nUNDO_REDO_PIVOT_COMMANDS.push(\"UPDATE_ODOO_PIVOT_DOMAIN\");\n\nexport class PivotOdooUIPlugin extends OdooUIPlugin {\n    static getters = /** @type {const} */ ([]);\n\n    /**\n     * Handle a spreadsheet command\n     * @param {Object} cmd Command\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"REFRESH_ALL_DATA_SOURCES\":\n                this.refreshAllPivots();\n                break;\n        }\n    }\n\n    /**\n     * Refresh the cache of all the pivots\n     */\n    refreshAllPivots() {\n        for (const pivotId of this.getters.getPivotIds()) {\n            this.dispatch(\"REFRESH_PIVOT\", { id: pivotId });\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { FILTER_DATE_OPTION, monthsOptions } from \"@spreadsheet/assets_backend/constants\";\nimport { Domain } from \"@web/core/domain\";\nimport { NO_RECORD_AT_THIS_POSITION } from \"../pivot_model\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { OdooUIPlugin } from \"@spreadsheet/plugins\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n * @typedef {import(\"@odoo/o-spreadsheet\").Token} Token\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotDomain} PivotDomain\n */\n\n/**\n * Convert pivot period to the related filter value\n *\n * @param {import(\"@spreadsheet/global_filters/plugins/global_filters_core_plugin\").RangeType} timeRange\n * @param {string} value\n * @returns {object}\n */\nfunction pivotPeriodToFilterValue(timeRange, value) {\n    // reuse the same logic as in `parseAccountingDate`?\n    if (typeof value === \"number\") {\n        value = value.toString(10);\n    }\n    if (\n        value === \"false\" || // the value \"false\" is the default value when there is no data for a group header\n        typeof value !== \"string\"\n    ) {\n        // anything else then a string at this point is incorrect, so no filtering\n        return undefined;\n    }\n\n    const yearValue = Number.parseInt(value.split(\"/\").at(-1), 10);\n    if (isNaN(yearValue)) {\n        return undefined;\n    }\n    const yearOffset = yearValue - DateTime.now().year;\n    switch (timeRange) {\n        case \"year\":\n            return {\n                yearOffset,\n            };\n        case \"month\": {\n            const month = value.includes(\"/\") ? Number.parseInt(value.split(\"/\")[0]) : -1;\n            if (!(month in monthsOptions)) {\n                return { yearOffset, period: undefined };\n            }\n            return {\n                yearOffset,\n                period: monthsOptions[month - 1].id,\n            };\n        }\n        case \"quarter\": {\n            const quarter = value.includes(\"/\") ? Number.parseInt(value.split(\"/\")[0]) : -1;\n            if (!(quarter in FILTER_DATE_OPTION.quarter)) {\n                return { yearOffset, period: undefined };\n            }\n            return {\n                yearOffset,\n                period: FILTER_DATE_OPTION.quarter[quarter - 1],\n            };\n        }\n    }\n}\n\nexport class PivotUIGlobalFilterPlugin extends OdooUIPlugin {\n    static getters = /** @type {const} */ ([\n        \"getPivotComputedDomain\",\n        \"getFiltersMatchingPivotArgs\",\n    ]);\n    constructor(config) {\n        super(config);\n        /** @type {string} */\n        this.selection.observe(this, {\n            handleEvent: this.handleEvent.bind(this),\n        });\n\n        globalFiltersFieldMatchers[\"pivot\"] = {\n            ...globalFiltersFieldMatchers[\"pivot\"],\n            waitForReady: () => this._getPivotsWaitForReady(),\n            getFields: (pivotId) => this.getters.getPivot(pivotId).getFields(),\n        };\n    }\n\n    handleEvent(event) {\n        if (!this.getters.isDashboard()) {\n            return;\n        }\n        switch (event.type) {\n            case \"ZonesSelected\": {\n                const sheetId = this.getters.getActiveSheetId();\n                const { col, row } = event.anchor.cell;\n                const cell = this.getters.getCell({ sheetId, col, row });\n                if (cell !== undefined && cell.content.startsWith(\"=PIVOT.HEADER(\")) {\n                    const filters = this._getFiltersMatchingPivot(\n                        sheetId,\n                        cell.compiledFormula.tokens\n                    );\n                    this.dispatch(\"SET_MANY_GLOBAL_FILTER_VALUE\", { filters });\n                }\n                break;\n            }\n        }\n    }\n\n    beforeHandle(cmd) {\n        switch (cmd.type) {\n            case \"START\":\n                // make sure the domains are correctly set before\n                // any evaluation\n                this._addDomains();\n                break;\n        }\n    }\n\n    /**\n     * Handle a spreadsheet command\n     * @param {Object} cmd Command\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_GLOBAL_FILTER\":\n            case \"EDIT_GLOBAL_FILTER\":\n            case \"REMOVE_GLOBAL_FILTER\":\n            case \"SET_GLOBAL_FILTER_VALUE\":\n            case \"CLEAR_GLOBAL_FILTER_VALUE\":\n                this._addDomains();\n                break;\n            case \"UPDATE_PIVOT\":\n            case \"UPDATE_ODOO_PIVOT_DOMAIN\":\n                this._addDomain(cmd.pivotId);\n                break;\n            case \"UNDO\":\n            case \"REDO\": {\n                if (\n                    cmd.commands.find((command) =>\n                        [\n                            \"ADD_GLOBAL_FILTER\",\n                            \"EDIT_GLOBAL_FILTER\",\n                            \"REMOVE_GLOBAL_FILTER\",\n                            \"UPDATE_ODOO_PIVOT_DOMAIN\",\n                            \"UPDATE_PIVOT\",\n                        ].includes(command.type)\n                    )\n                ) {\n                    this._addDomains();\n                }\n                break;\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------\n\n    /**\n     * Get the computed domain of a pivot\n     * CLEAN ME not used outside of tests\n     * @param {string} pivotId Id of the pivot\n     * @returns {Array}\n     */\n    getPivotComputedDomain(pivotId) {\n        return this.getters.getPivot(pivotId).getDomainWithGlobalFilters();\n    }\n\n    /**\n     * Get the filter impacted by a pivot formula's argument\n     * @param {Token[]} tokens Formula of the pivot cell\n     *\n     * @returns {Array<Object>}\n     */\n    _getFiltersMatchingPivot(sheetId, tokens) {\n        const functionDescription = this.getters.getFirstPivotFunction(sheetId, tokens);\n        if (!functionDescription) {\n            return [];\n        }\n        const { args } = functionDescription;\n        if (args.length <= 2) {\n            return [];\n        }\n        const formulaId = args[0];\n        const pivotId = this.getters.getPivotId(formulaId);\n        const index = functionDescription.functionName === \"PIVOT.HEADER\" ? 1 : 2;\n        const pivot = this.getters.getPivot(pivotId);\n        const domainArgs = args.slice(index).map((value) => ({ value }));\n        const domain = pivot.parseArgsToPivotDomain(domainArgs);\n        return this.getFiltersMatchingPivotArgs(pivotId, domain);\n    }\n\n    /**\n     * Get the filter impacted by a pivot\n     * @param {string} pivotId Id of the pivot\n     * @param {PivotDomain} PivotDomain\n     */\n    getFiltersMatchingPivotArgs(pivotId, PivotDomain) {\n        const lastNode = PivotDomain.at(-1);\n        if (!lastNode || lastNode.field === \"measure\") {\n            return [];\n        }\n        const filters = this.getters.getGlobalFilters();\n        const matchingFilters = [];\n\n        for (const filter of filters) {\n            const dataSource = this.getters.getPivot(pivotId);\n            const { type } = this.getters.getPivotCoreDefinition(pivotId);\n            if (type !== \"ODOO\") {\n                continue;\n            }\n            const { field, granularity: time } = dataSource.parseGroupField(lastNode.field);\n            const pivotFieldMatching = this.getters.getPivotFieldMatching(pivotId, filter.id);\n            if (pivotFieldMatching && pivotFieldMatching.chain === field.name) {\n                let value = dataSource.getLastPivotGroupValue(PivotDomain.slice(-1));\n                if (value === NO_RECORD_AT_THIS_POSITION) {\n                    continue;\n                }\n                let transformedValue;\n                const currentValue = this.getters.getGlobalFilterValue(filter.id);\n                switch (filter.type) {\n                    case \"date\":\n                        if (filter.rangeType === \"fixedPeriod\" && time) {\n                            if (value === \"false\") {\n                                transformedValue = undefined;\n                            } else {\n                                transformedValue = pivotPeriodToFilterValue(time, value);\n                                if (\n                                    JSON.stringify(transformedValue) ===\n                                    JSON.stringify(currentValue)\n                                ) {\n                                    transformedValue = undefined;\n                                }\n                            }\n                        } else {\n                            continue;\n                        }\n                        break;\n                    case \"relation\":\n                        if (typeof value == \"string\") {\n                            value = Number(value);\n                            if (Number.isNaN(value)) {\n                                break;\n                            }\n                        }\n                        if (JSON.stringify(currentValue) !== `[${value}]`) {\n                            transformedValue = [value];\n                        }\n                        break;\n                    case \"text\":\n                        if (currentValue !== value) {\n                            transformedValue = value;\n                        }\n                        break;\n                }\n                matchingFilters.push({ filterId: filter.id, value: transformedValue });\n            }\n        }\n        return matchingFilters;\n    }\n\n    // ---------------------------------------------------------------------\n    // Private\n    // ---------------------------------------------------------------------\n\n    /**\n     * Add an additional domain to a pivot\n     *\n     * @private\n     *\n     * @param {string} pivotId pivot id\n     */\n    _addDomain(pivotId) {\n        if (this.getters.getPivotCoreDefinition(pivotId).type !== \"ODOO\") {\n            return;\n        }\n        const domainList = [];\n        for (const [filterId, fieldMatch] of Object.entries(\n            this.getters.getPivotFieldMatch(pivotId)\n        )) {\n            domainList.push(this.getters.getGlobalFilterDomain(filterId, fieldMatch));\n        }\n        const domain = Domain.combine(domainList, \"AND\").toString();\n        this.getters.getPivot(pivotId).addGlobalFilterDomain(domain);\n    }\n\n    /**\n     * Add an additional domain to all pivots\n     *\n     * @private\n     *\n     */\n    _addDomains() {\n        for (const pivotId of this.getters\n            .getPivotIds()\n            .filter((pivotId) => this.getters.getPivot(pivotId).type === \"ODOO\")) {\n            this._addDomain(pivotId);\n        }\n    }\n\n    /**\n     *\n     * @return {Promise[]}\n     */\n    _getPivotsWaitForReady() {\n        return this.getters\n            .getPivotIds()\n            .map((pivotId) => this.getters.getPivot(pivotId))\n            .filter((pivot) => pivot.type === \"ODOO\")\n            .map((pivot) => pivot.loadMetadata());\n    }\n}\n", "/** @odoo-module */\n\nimport { CorePlugin, UIPlugin } from \"@odoo/o-spreadsheet\";\n\n/**\n * An o-spreadsheet core plugin with access to all custom Odoo plugins\n * @type {import(\"@spreadsheet\").OdooCorePluginConstructor}\n **/\nexport const OdooCorePlugin = CorePlugin;\n\n/**\n * An o-spreadsheet UI plugin with access to all custom Odoo plugins\n * @type {import(\"@spreadsheet\").OdooUIPluginConstructor}\n **/\nexport const OdooUIPlugin = UIPlugin;\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { DashboardLoader, Status } from \"./dashboard_loader\";\nimport { SpreadsheetComponent } from \"@spreadsheet/actions/spreadsheet_component\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { DashboardMobileSearchPanel } from \"./mobile_search_panel/mobile_search_panel\";\nimport { MobileFigureContainer } from \"./mobile_figure_container/mobile_figure_container\";\nimport { FilterValue } from \"@spreadsheet/global_filters/components/filter_value/filter_value\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { SpreadsheetShareButton } from \"@spreadsheet/components/share_button/share_button\";\nimport { useSpreadsheetPrint } from \"@spreadsheet/hooks\";\nimport { Registry } from \"@odoo/o-spreadsheet\";\nimport { router } from \"@web/core/browser/router\";\n\nimport { Component, onWillStart, useState, useEffect } from \"@odoo/owl\";\n\nexport const dashboardActionRegistry = new Registry();\n\nexport class SpreadsheetDashboardAction extends Component {\n    static template = \"spreadsheet_dashboard.DashboardAction\";\n    static components = {\n        ControlPanel,\n        SpreadsheetComponent,\n        FilterValue,\n        DashboardMobileSearchPanel,\n        MobileFigureContainer,\n        SpreadsheetShareButton,\n    };\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.Status = Status;\n        this.controlPanelDisplay = {};\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        // Use the non-protected orm service (`this.env.services.orm` instead of `useService(\"orm\")`)\n        // because spreadsheets models are preserved across multiple components when navigating\n        // with the breadcrumb\n        // TODO write a test\n        /** @type {DashboardLoader}*/\n        this.loader = useState(new DashboardLoader(this.env, this.env.services.orm));\n        onWillStart(async () => {\n            if (this.props.state && this.props.state.dashboardLoader) {\n                const { groups, dashboards } = this.props.state.dashboardLoader;\n                this.loader.restoreFromState(groups, dashboards);\n            } else {\n                await this.loader.load();\n            }\n            const activeDashboardId = this.getInitialActiveDashboard();\n            if (activeDashboardId) {\n                this.openDashboard(activeDashboardId);\n            }\n        });\n        useEffect(\n            () => router.pushState({ dashboard_id: this.activeDashboardId }),\n            () => [this.activeDashboardId]\n        );\n        useEffect(\n            () => {\n                const dashboard = this.state.activeDashboard;\n                if (dashboard && dashboard.status === Status.Loaded) {\n                    const render = () => this.render(true);\n                    dashboard.model.on(\"update\", this, render);\n                    return () => dashboard.model.off(\"update\", this, render);\n                }\n            },\n            () => {\n                const dashboard = this.state.activeDashboard;\n                return [dashboard?.model, dashboard?.status];\n            }\n        );\n        useSetupAction({\n            getLocalState: () => {\n                return {\n                    activeDashboardId: this.activeDashboardId,\n                    dashboardLoader: this.loader.getState(),\n                };\n            },\n        });\n        useSpreadsheetPrint(() => this.state.activeDashboard?.model);\n        /** @type {{ activeDashboard: import(\"./dashboard_loader\").Dashboard}} */\n        this.state = useState({ activeDashboard: undefined, sidebarExpanded: true });\n    }\n\n    get dashboardButton() {\n        return dashboardActionRegistry.getAll()[0];\n    }\n\n    /**\n     * @returns {number | undefined}\n     */\n    get activeDashboardId() {\n        return this.state.activeDashboard ? this.state.activeDashboard.id : undefined;\n    }\n\n    /**\n     * @returns {object[]}\n     */\n    get filters() {\n        const dashboard = this.state.activeDashboard;\n        if (!dashboard || dashboard.status !== Status.Loaded) {\n            return [];\n        }\n        return dashboard.model.getters.getGlobalFilters();\n    }\n\n    /**\n     * @private\n     * @returns {number | undefined}\n     */\n    getInitialActiveDashboard() {\n        if (this.props.state && this.props.state.activeDashboardId) {\n            return this.props.state.activeDashboardId;\n        }\n        const params = this.props.action.params || this.props.action.context.params;\n        if (params && params.dashboard_id) {\n            return params.dashboard_id;\n        }\n        const [firstSection] = this.getDashboardGroups();\n        if (firstSection && firstSection.dashboards.length) {\n            return firstSection.dashboards[0].id;\n        }\n    }\n\n    getDashboardGroups() {\n        return this.loader.getDashboardGroups();\n    }\n\n    /**\n     * @param {number} dashboardId\n     */\n    openDashboard(dashboardId) {\n        this.state.activeDashboard = this.loader.getDashboard(dashboardId);\n    }\n\n    /**\n     * @param {number} id - The ID of the dashboard to be edited.\n     * @returns {Promise<void>}\n     */\n    async editDashboard(id) {\n        const action = await this.env.services.orm.call(\n            \"spreadsheet.dashboard\",\n            \"action_edit_dashboard\",\n            [id]\n        );\n        this.actionService.doAction(action);\n    }\n\n    async shareSpreadsheet(data, excelExport) {\n        const url = await this.orm.call(\"spreadsheet.dashboard.share\", \"action_get_share_url\", [\n            {\n                dashboard_id: this.activeDashboardId,\n                spreadsheet_data: JSON.stringify(data),\n                excel_files: excelExport.files,\n            },\n        ]);\n        return url;\n    }\n\n    toggleSidebar() {\n        this.state.sidebarExpanded = !this.state.sidebarExpanded;\n    }\n\n    get activeDashboardGroupName() {\n        return this.getDashboardGroups().find((group) =>\n            group.dashboards.some((d) => d.id === this.activeDashboardId)\n        )?.name;\n    }\n}\n\nregistry\n    .category(\"actions\")\n    .add(\"action_spreadsheet_dashboard\", SpreadsheetDashboardAction, { force: true });\n", "/** @odoo-module */\n\nimport { Model } from \"@odoo/o-spreadsheet\";\nimport { OdooDataProvider } from \"@spreadsheet/data_sources/odoo_data_provider\";\nimport { createDefaultCurrency } from \"@spreadsheet/currency/helpers\";\n\n/**\n * @type {{\n *  NotLoaded: \"NotLoaded\",\n *  Loading: \"Loading\",\n *  Loaded: \"Loaded\",\n *  Error: \"Error\",\n * }}\n */\nexport const Status = {\n    NotLoaded: \"NotLoaded\",\n    Loading: \"Loading\",\n    Loaded: \"Loaded\",\n    Error: \"Error\",\n};\n\n/**\n * @typedef Dashboard\n * @property {number} id\n * @property {string} displayName\n * @property {string} status\n * @property {Model} [model]\n * @property {Error} [error]\n *\n * @typedef DashboardGroupData\n * @property {number} id\n * @property {string} name\n * @property {Array<{id: number, name: string}>} dashboards\n *\n * @typedef DashboardGroup\n * @property {number} id\n * @property {string} name\n * @property {Array<Dashboard>} dashboards\n *\n * @typedef {import(\"@web/env\").OdooEnv} OdooEnv\n *\n * @typedef {import(\"@web/core/orm_service\").ORM} ORM\n */\n\nexport class DashboardLoader {\n    /**\n     * @param {OdooEnv} env\n     * @param {ORM} orm\n     */\n    constructor(env, orm) {\n        /** @private */\n        this.env = env;\n        /** @private */\n        this.orm = orm;\n        /** @private @type {Array<DashboardGroupData>} */\n        this.groups = [];\n        /** @private @type {Object<number, Dashboard>} */\n        this.dashboards = {};\n    }\n\n    /**\n     * @param {Array<DashboardGroupData>} groups\n     * @param {Object<number, Dashboard>} dashboards\n     */\n    restoreFromState(groups, dashboards) {\n        this.groups = groups;\n        this.dashboards = dashboards;\n    }\n\n    /**\n     * Return data needed to restore a dashboard loader\n     */\n    getState() {\n        return {\n            groups: this.groups,\n            dashboards: this.dashboards,\n        };\n    }\n\n    async load() {\n        const groups = await this._fetchGroups();\n        this.groups = groups\n            .filter((group) => group.published_dashboard_ids.length)\n            .map((group) => ({\n                id: group.id,\n                name: group.name,\n                dashboards: group.published_dashboard_ids,\n            }));\n        const dashboards = this.groups.map((group) => group.dashboards).flat();\n        for (const dashboard of dashboards) {\n            this.dashboards[dashboard.id] = {\n                id: dashboard.id,\n                displayName: dashboard.name,\n                status: Status.NotLoaded,\n            };\n        }\n    }\n\n    /**\n     * @param {number} dashboardId\n     * @returns {Dashboard}\n     */\n    getDashboard(dashboardId) {\n        const dashboard = this._getDashboard(dashboardId);\n        if (dashboard.status === Status.NotLoaded) {\n            dashboard.promise = this._loadDashboardData(dashboardId);\n        }\n        return dashboard;\n    }\n\n    /**\n     * @returns {Array<DashboardGroup>}\n     */\n    getDashboardGroups() {\n        return this.groups.map((section) => ({\n            id: section.id,\n            name: section.name,\n            dashboards: section.dashboards.map((dashboard) => ({\n                id: dashboard.id,\n                displayName: dashboard.name,\n                status: this._getDashboard(dashboard.id).status,\n            })),\n        }));\n    }\n\n    /**\n     * @private\n     * @returns {Promise<{id: number, name: string, published_dashboard_ids: number[]}[]>}\n     */\n    async _fetchGroups() {\n        const groups = await this.orm.webSearchRead(\n            \"spreadsheet.dashboard.group\",\n            [[\"published_dashboard_ids\", \"!=\", false]],\n            {\n                specification: {\n                    name: {},\n                    published_dashboard_ids: { fields: { name: {} } },\n                },\n            }\n        );\n        return groups.records;\n    }\n\n    /**\n     * @private\n     * @param {number} id\n     * @returns {Dashboard}\n     */\n    _getDashboard(id) {\n        if (!this.dashboards[id]) {\n            this.dashboards[id] = { status: Status.NotLoaded, id, displayName: \"\" };\n        }\n        return this.dashboards[id];\n    }\n\n    /**\n     * @private\n     * @param {number} dashboardId\n     */\n    async _loadDashboardData(dashboardId) {\n        const dashboard = this._getDashboard(dashboardId);\n        dashboard.status = Status.Loading;\n        try {\n            const { snapshot, revisions, default_currency, is_sample } = await this.orm.call(\n                \"spreadsheet.dashboard\",\n                \"get_readonly_dashboard\",\n                [dashboardId]\n            );\n            dashboard.model = this._createSpreadsheetModel(snapshot, revisions, default_currency);\n            dashboard.status = Status.Loaded;\n            dashboard.isSample = is_sample;\n        } catch (error) {\n            dashboard.error = error;\n            dashboard.status = Status.Error;\n            throw error;\n        }\n    }\n\n    /**\n     * Activate the first sheet of a model\n     *\n     * @param {Model} model\n     */\n    _activateFirstSheet(model) {\n        const sheetId = model.getters.getActiveSheetId();\n        const firstSheetId = model.getters.getSheetIds()[0];\n        if (firstSheetId !== sheetId) {\n            model.dispatch(\"ACTIVATE_SHEET\", {\n                sheetIdFrom: sheetId,\n                sheetIdTo: firstSheetId,\n            });\n        }\n    }\n\n    /**\n     * @private\n     * @param {object} snapshot\n     * @param {object[]} revisions\n     * @param {object} [defaultCurrency]\n     * @returns {Model}\n     */\n    _createSpreadsheetModel(snapshot, revisions = [], currency) {\n        const odooDataProvider = new OdooDataProvider(this.env);\n        const model = new Model(\n            snapshot,\n            {\n                custom: { env: this.env, orm: this.orm, odooDataProvider },\n                mode: \"dashboard\",\n                defaultCurrency: createDefaultCurrency(currency),\n            },\n            revisions\n        );\n        this._activateFirstSheet(model);\n        odooDataProvider.addEventListener(\"data-source-updated\", () =>\n            model.dispatch(\"EVALUATE_CELLS\")\n        );\n        return model;\n    }\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nimport { Component, useSubEnv } from \"@odoo/owl\";\nconst { registries } = spreadsheet;\nconst { figureRegistry } = registries;\n\nexport class MobileFigureContainer extends Component {\n    static template = \"documents_spreadsheet.MobileFigureContainer\";\n    static props = {\n        spreadsheetModel: Object,\n    };\n\n    setup() {\n        useSubEnv({\n            model: this.props.spreadsheetModel,\n            isDashboard: () => this.props.spreadsheetModel.getters.isDashboard(),\n            openSidePanel: () => {},\n        });\n    }\n\n    get figures() {\n        const sheetId = this.props.spreadsheetModel.getters.getActiveSheetId();\n        return this.props.spreadsheetModel.getters\n            .getFigures(sheetId)\n            .sort((f1, f2) => (this.isBefore(f1, f2) ? -1 : 1))\n            .map((figure) => ({\n                ...figure,\n                width: window.innerWidth,\n                height: 300,\n            }));\n    }\n\n    getFigureComponent(figure) {\n        return figureRegistry.get(figure.tag).Component;\n    }\n\n    isBefore(f1, f2) {\n        // TODO be smarter\n        return f1.x < f2.x ? f1.y < f2.y : f1.y < f2.y;\n    }\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class DashboardMobileSearchPanel extends Component {\n    static template = \"spreadsheet_dashboard.DashboardMobileSearchPanel\";\n    static props = {\n        /**\n         * (dashboardId: number) => void\n         */\n        onDashboardSelected: Function,\n        groups: Object,\n        activeDashboard: {\n            type: Object,\n            optional: true,\n        },\n    };\n\n    setup() {\n        this.state = useState({ isOpen: false });\n    }\n\n    get searchBarText() {\n        return this.props.activeDashboard\n            ? this.props.activeDashboard.displayName\n            : _t(\"Choose a dashboard....\");\n    }\n\n    onDashboardSelected(dashboardId) {\n        this.props.onDashboardSelected(dashboardId);\n        this.state.isOpen = false;\n    }\n\n    openDashboardSelection() {\n        const dashboards = this.props.groups.map((group) => group.dashboards).flat();\n        if (dashboards.length > 1) {\n            this.state.isOpen = true;\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { SEE_RECORD_LIST, SEE_RECORD_LIST_VISIBLE } from \"@spreadsheet/list/list_actions\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nconst { clickableCellRegistry } = spreadsheet.registries;\n\nclickableCellRegistry.add(\"list\", {\n    condition: SEE_RECORD_LIST_VISIBLE,\n    execute: SEE_RECORD_LIST,\n    sequence: 10,\n});\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport {\n    SEE_RECORDS_PIVOT,\n    SEE_RECORDS_PIVOT_VISIBLE,\n    SET_FILTER_MATCHING,\n    SET_FILTER_MATCHING_CONDITION,\n} from \"@spreadsheet/pivot/pivot_actions\";\n\nconst { clickableCellRegistry } = spreadsheet.registries;\n\nclickableCellRegistry.add(\"pivot\", {\n    condition: SEE_RECORDS_PIVOT_VISIBLE,\n    execute: SEE_RECORDS_PIVOT,\n    sequence: 3,\n});\n\nclickableCellRegistry.add(\"pivot_set_filter_matching\", {\n    condition: SET_FILTER_MATCHING_CONDITION,\n    execute: SET_FILTER_MATCHING,\n    sequence: 2,\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { onMounted, onWillStart, useState, Component, useSubEnv, onWillUnmount } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { downloadFile } from \"@web/core/network/download\";\nimport { user } from \"@web/core/user\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nimport { UNTITLED_SPREADSHEET_NAME, DEFAULT_LINES_NUMBER } from \"@spreadsheet/helpers/constants\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { initCallbackRegistry } from \"@spreadsheet/o_spreadsheet/init_callbacks\";\nimport { RecordFileStore } from \"../image/record_file_store\";\nimport { useSpreadsheetCurrencies, useSpreadsheetLocales, useSpreadsheetThumbnail } from \"../hooks\";\nimport { useSpreadsheetPrint } from \"@spreadsheet/hooks\";\nimport { InputDialog } from \"./input_dialog/input_dialog\";\nimport { OdooDataProvider } from \"@spreadsheet/data_sources/odoo_data_provider\";\nimport { CommentsStore } from \"../comments/comments_store\";\nimport { waitForDataLoaded } from \"@spreadsheet/helpers/model\";\nimport { createDefaultCurrency } from \"@spreadsheet/currency/helpers\";\n\nimport { SpreadsheetNavbar } from \"@spreadsheet_edition/bundle/components/spreadsheet_navbar/spreadsheet_navbar\";\nimport { SpreadsheetComponent } from \"@spreadsheet/actions/spreadsheet_component\";\n\nconst uuidGenerator = new spreadsheet.helpers.UuidGenerator();\n\nconst { Model } = spreadsheet;\nconst { useStoreProvider, ModelStore, SidePanelStore } = spreadsheet.stores;\n\n/**\n * @typedef SpreadsheetData\n * @property {number} id\n * @property {string} name\n * @property {string} data\n * @property {Object[]} revisions\n * @property {boolean} snapshot_requested\n * @property {Boolean} isReadonly\n * @property {string} [writable_rec_name_field]\n */\n\nexport class AbstractSpreadsheetAction extends Component {\n    static template = \"\";\n    static props = { ...standardActionServiceProps };\n    static components = {\n        SpreadsheetComponent,\n        SpreadsheetNavbar,\n    };\n    static target = \"fullscreen\";\n\n    setup() {\n        if (!this.props.action.params) {\n            // the action is coming from a this.trigger(\"do-action\", ... ) of owl (not wowl and not legacy)\n            this.params = this.props.action.context;\n        } else {\n            // the action is coming from wowl\n            this.params = this.props.action.params;\n        }\n        this.isEmptySpreadsheet = this.params.is_new_spreadsheet || false;\n        this.resId =\n            this.params.resId ||\n            this.params.spreadsheet_id || // backward compatibility. res_id used to be spreadsheet_id\n            this.params.active_id || // backward compatibility. spreadsheet_id used to be active_id\n            (this.props.state && this.props.state.resId); // used when going back to a spreadsheet via breadcrumb\n        this.shareId = this.params.share_id || this.props.state?.shareId;\n        this.accessToken = this.params.access_token || this.props.state?.accessToken;\n        this.actionService = useService(\"action\");\n        this.notifications = useService(\"notification\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.http = useService(\"http\");\n        this.ui = useService(\"ui\");\n        this.loadLocales = useSpreadsheetLocales();\n        this.loadCurrencies = useSpreadsheetCurrencies();\n        this.getThumbnail = useSpreadsheetThumbnail();\n        this.fileStore = new RecordFileStore(this.resModel, this.resId, this.http, this.orm);\n        this.spreadsheetService = useService(\"spreadsheet_collaborative\");\n        this.stores = useStoreProvider();\n        this.threadId = this.params?.thread_id;\n        useSetupAction({\n            beforeLeave: this._leaveSpreadsheet.bind(this),\n            beforeUnload: this._leaveSpreadsheet.bind(this),\n            getLocalState: () => {\n                return {\n                    resId: this.resId,\n                    shareId: this.shareId,\n                    accessToken: this.accessToken,\n                    data: this.data,\n                    model: this.model,\n                };\n            },\n        });\n\n        const print = useSpreadsheetPrint(() => this.model);\n        useSubEnv({\n            download: this.download.bind(this),\n            downloadAsJson: this.downloadAsJson.bind(this),\n            showHistory: this.showHistory.bind(this),\n            insertThreadInSheet: this.insertThreadInSheet.bind(this),\n            print,\n            getLinesNumber: this._getLinesNumber.bind(this),\n            getUserLocale: () => this.data && this.data.user_locale,\n        });\n        this.state = useState({\n            spreadsheetName: UNTITLED_SPREADSHEET_NAME,\n        });\n\n        onWillStart(async () => {\n            if (this.props.state?.model && this.props.state?.data) {\n                this._initializeWith(this.props.state.data);\n                this.model = this.props.state.model;\n                this.model.joinSession();\n                this.stores.inject(ModelStore, this.model);\n            } else {\n                await this.fetchData();\n                this.createModel();\n                this.stores.inject(ModelStore, this.model);\n                await this.execInitCallbacks();\n            }\n        });\n        onMounted(() => {\n            const commentsStore = this.stores.get(CommentsStore);\n            this.props.updateActionState({\n                resId: this.resId,\n                access_token: this.accessToken,\n                share_id: this.shareId,\n            });\n            this.env.config.setDisplayName(this.state.spreadsheetName);\n            this.model.on(\"unexpected-revision-id\", this, this.onUnexpectedRevisionId.bind(this));\n            if (this.threadId) {\n                // necessary atm - we need at least one frame to have the right viewport height/width\n                setTimeout(() => commentsStore.openCommentThread(this.threadId), 0);\n                const sidePanel = this.stores.get(SidePanelStore);\n                sidePanel.open(\"Comments\");\n            }\n        });\n        onWillUnmount(() => {\n            this.model.off(\"unexpected-revision-id\", this);\n        });\n    }\n\n    get navbarProps() {\n        return {\n            isReadonly: this.isReadonly,\n            onSpreadsheetNameChanged: this._onSpreadSheetNameChanged.bind(this),\n            spreadsheetName: this.state.spreadsheetName,\n        };\n    }\n\n    async fetchData() {\n        // if we are returning to the spreadsheet via the breadcrumb, we don't want\n        // to do all the \"creation\" options of the actions\n        if (!this.props.state) {\n            await this._setupPreProcessingCallbacks();\n        }\n        const data = await this._fetchData();\n        this._initializeWith(data);\n    }\n\n    createModel() {\n        this.model = new Model(\n            this.spreadsheetData,\n            this.getModelConfig(),\n            this.stateUpdateMessages\n        );\n        if (this.env.debug) {\n            // eslint-disable-next-line no-import-assign\n            spreadsheet.__DEBUG__ = spreadsheet.__DEBUG__ || {};\n            spreadsheet.__DEBUG__.model = this.model;\n        }\n    }\n\n    getModelConfig() {\n        const transportService = this.spreadsheetService.makeCollaborativeChannel(\n            this.resModel,\n            this.resId,\n            this.shareId,\n            this.accessToken\n        );\n        const odooDataProvider = new OdooDataProvider(this.env);\n        odooDataProvider.addEventListener(\"data-source-updated\", () => {\n            this.model.dispatch(\"EVALUATE_CELLS\");\n        });\n        return {\n            custom: { env: this.env, orm: this.orm, odooDataProvider },\n            external: {\n                fileStore: this.fileStore,\n                loadCurrencies: this.loadCurrencies,\n                loadLocales: this.loadLocales,\n            },\n            defaultCurrency: createDefaultCurrency(this.data.default_currency),\n            transportService,\n            client: {\n                id: uuidGenerator.uuidv4(),\n                name: user.name,\n                userId: user.userId,\n            },\n            mode: this.isReadonly ? \"readonly\" : \"normal\",\n            snapshotRequested: this.snapshotRequested,\n            customColors: this.data.company_colors,\n        };\n    }\n\n    async execInitCallbacks() {\n        if (this.asyncInitCallback) {\n            await this.asyncInitCallback(this.model, this.stores);\n        }\n        if (this.initCallback) {\n            this.initCallback(this.model, this.stores);\n        }\n    }\n\n    async _setupPreProcessingCallbacks() {\n        if (this.params.preProcessingAction) {\n            const initCallbackGenerator = initCallbackRegistry\n                .get(this.params.preProcessingAction)\n                .bind(this);\n            this.initCallback = await initCallbackGenerator(this.params.preProcessingActionData);\n        }\n        if (this.params.preProcessingAsyncAction) {\n            const initCallbackGenerator = initCallbackRegistry\n                .get(this.params.preProcessingAsyncAction)\n                .bind(this);\n            this.asyncInitCallback = await initCallbackGenerator(\n                this.params.preProcessingAsyncActionData\n            );\n        }\n    }\n\n    /**\n     * @protected\n     * @abstract\n     * @param {SpreadsheetData} data\n     */\n    _initializeWith(data) {\n        this.state.spreadsheetName = data.name;\n        this.spreadsheetData = data.data;\n        this.stateUpdateMessages = data.revisions;\n        this.snapshotRequested = data.snapshot_requested;\n        this.isReadonly = data.isReadonly;\n        this.data = data;\n    }\n\n    /**\n     * Make a copy of the current document\n     * @protected\n     */\n    async makeCopy() {\n        const thumbnail = this.getThumbnail();\n        const data = this.model.exportData();\n        const defaultValues = {\n            spreadsheet_data: JSON.stringify(data),\n            spreadsheet_snapshot: false,\n            spreadsheet_revision_ids: [],\n            thumbnail,\n        };\n        const ids = await this.orm.call(this.resModel, \"copy\", [[this.resId]], {\n            default: defaultValues,\n        });\n        const id = ids[0];\n        this._openSpreadsheet(id);\n    }\n\n    /**\n     * @private\n     */\n    async _leaveSpreadsheet() {\n        await this.model.leaveSession();\n        this.model.off(\"update\", this);\n        if (!this.isReadonly) {\n            return this.onSpreadsheetLeft();\n        }\n    }\n\n    async _onSpreadSheetNameChanged(detail) {\n        const { name } = detail;\n        if (name && name !== this.state.spreadsheetName) {\n            this.state.spreadsheetName = name;\n            this.env.config.setDisplayName(this.state.spreadsheetName);\n            if (this.data.writable_rec_name_field) {\n                await this.orm.write(this.resModel, [this.resId], {\n                    [this.data.writable_rec_name_field]: name,\n                });\n            }\n        }\n    }\n\n    async createNewSpreadsheet() {\n        throw new Error(\"not implemented by children\");\n    }\n\n    async onSpreadsheetLeft() {\n        if (this.accessToken) {\n            return;\n        }\n        await this.orm.write(this.resModel, [this.resId], this.onSpreadsheetLeftUpdateVals());\n    }\n\n    onSpreadsheetLeftUpdateVals() {\n        return { thumbnail: this.getThumbnail() };\n    }\n\n    /**\n     * @returns {Promise<SpreadsheetData>}\n     */\n    async _fetchData() {\n        return this.orm.call(this.resModel, \"join_spreadsheet_session\", [\n            this.resId,\n            this.accessToken,\n        ]);\n    }\n\n    /**\n     * @protected\n     */\n    _notifyCreation() {\n        this.notifications.add(this.notificationMessage, {\n            type: \"info\",\n            sticky: false,\n        });\n    }\n\n    /**\n     * Open a spreadsheet\n     * @private\n     */\n    _openSpreadsheet(spreadsheetId) {\n        this._notifyCreation();\n        this.actionService.doAction(\n            {\n                type: \"ir.actions.client\",\n                tag: this.props.action.tag,\n                params: { spreadsheet_id: spreadsheetId },\n            },\n            { clear_breadcrumbs: true }\n        );\n    }\n\n    showHistory() {\n        this.actionService.doAction(\n            {\n                type: \"ir.actions.client\",\n                tag: \"action_open_spreadsheet_history\",\n                params: {\n                    spreadsheet_id: this.resId,\n                    res_model: this.resModel,\n                },\n            },\n            { clear_breadcrumbs: true }\n        );\n    }\n\n    /**\n     * Reload the spreadsheet if an unexpected revision id is triggered.\n     */\n    onUnexpectedRevisionId() {\n        this.actionService.doAction(\"reload_context\");\n    }\n\n    /**\n     * Downloads the spreadsheet in xlsx format\n     */\n    async download() {\n        this.ui.block();\n        try {\n            await waitForDataLoaded(this.model);\n            await this.actionService.doAction({\n                type: \"ir.actions.client\",\n                tag: \"action_download_spreadsheet\",\n                params: {\n                    name: this.state.spreadsheetName,\n                    xlsxData: this.model.exportXLSX(),\n                },\n            });\n        } finally {\n            this.ui.unblock();\n        }\n    }\n\n    /**\n     * Downloads the spreadsheet in json format\n     */\n    async downloadAsJson() {\n        this.ui.block();\n        try {\n            const data = JSON.stringify(this.model.exportData());\n            await downloadFile(\n                data,\n                `${this.state.spreadsheetName}.osheet.json`,\n                \"application/json\"\n            );\n        } finally {\n            this.ui.unblock();\n        }\n    }\n\n    _getLinesNumber(callback) {\n        this.dialog.add(InputDialog, {\n            body: _t(\"Select the number of records to insert\"),\n            confirm: callback,\n            title: _t(\"Re-insert list\"),\n            inputValue: DEFAULT_LINES_NUMBER,\n            inputType: \"number\",\n        });\n    }\n\n    async insertThreadInSheet({ sheetId, col, row }) {\n        const [threadId] = await this.env.services.orm.create(\"spreadsheet.cell.thread\", [\n            { [this.threadField]: this.resId },\n        ]);\n        this.model.dispatch(\"ADD_COMMENT_THREAD\", {\n            sheetId,\n            col,\n            row,\n            threadId,\n        });\n        return threadId;\n    }\n}\n", "/** @odoo-module **/\n\nimport { UNTITLED_SPREADSHEET_NAME } from \"@spreadsheet/helpers/constants\";\n\nimport { Component, onMounted, useState, useRef, onWillUpdateProps } from \"@odoo/owl\";\n\nconst WIDTH_MARGIN = 3;\nconst PADDING_RIGHT = 5;\nconst PADDING_LEFT = PADDING_RIGHT - WIDTH_MARGIN;\n\nexport class SpreadsheetName extends Component {\n    static template = \"spreadsheet_edition.SpreadsheetName\";\n    static props = {\n        name: String,\n        isReadonly: Boolean,\n        onSpreadsheetNameChanged: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        onSpreadsheetNameChanged: () => {},\n    };\n\n    setup() {\n        this.placeholder = UNTITLED_SPREADSHEET_NAME;\n        this.state = useState({\n            inputSize: 1,\n            isUntitled: this._isUntitled(this.props.name),\n            name: this.props.name,\n        });\n        this.input = useRef(\"speadsheetNameInput\");\n\n        onMounted(() => {\n            this._setInputSize(this.state.name);\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.name !== this.props.name) {\n                this.state.name = nextProps.name;\n                this.state.isUntitled = this._isUntitled(nextProps.name);\n            }\n        });\n    }\n\n    /**\n     * @private\n     * @param {string} text in the input element\n     */\n    _setInputSize(text) {\n        const { fontFamily, fontSize } = window.getComputedStyle(this.input.el);\n        const font = `${fontSize} ${fontFamily}`;\n        this.state.inputSize =\n            this._computeTextWidth(text || this.placeholder, font) + PADDING_RIGHT + PADDING_LEFT;\n    }\n\n    /**\n     * Return the width in pixels of a text with the given font.\n     * @private\n     * @param {string} text\n     * @param {string} font css font attribute value\n     * @returns {number} width in pixels\n     */\n    _computeTextWidth(text, font) {\n        const canvas = document.createElement(\"canvas\");\n        const context = canvas.getContext(\"2d\");\n        context.font = font;\n        const width = context.measureText(text).width;\n        // add a small extra margin, otherwise the text jitters in\n        // the input because it overflows very slightly for some\n        // letters (?).\n        return Math.ceil(width) + WIDTH_MARGIN;\n    }\n\n    /**\n     * Check if the name is empty or is the generic name\n     * for untitled spreadsheets.\n     * @param {string} name\n     * @returns {boolean}\n     */\n    _isUntitled(name) {\n        name = name.trim();\n        return !name || name === UNTITLED_SPREADSHEET_NAME.toString();\n    }\n\n    /**\n     * @private\n     * @param {InputEvent} ev\n     */\n    _onFocus(ev) {\n        if (this._isUntitled(ev.target.value)) {\n            ev.target.value = this.placeholder;\n            ev.target.select();\n        }\n    }\n\n    /**\n     * @private\n     * @param {InputEvent} ev\n     */\n    _onInput(ev) {\n        const value = ev.target.value;\n        this.state.isUntitled = this._isUntitled(value);\n        this.state.name = value;\n        this._setInputSize(value);\n    }\n\n    /**\n     * @private\n     * @param {InputEvent} ev\n     */\n    _onNameChanged(ev) {\n        const value = ev.target.value.trim();\n        this.state.name = value || this.props.name;\n        this._setInputSize(this.state.name);\n        this.props.onSpreadsheetNameChanged({\n            name: this.state.name,\n        });\n        ev.target.blur();\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class InputDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        close: Function, // injected by the dialog service\n        body: String,\n        inputType: { type: String, optional: true },\n        inputValue: { type: [String, Number], optional: true },\n        confirm: { type: Function, optional: true },\n        title: { type: String, optional: true },\n    };\n    static template = \"spreadsheet_edition.InputDialog\";\n\n    setup() {\n        this.state = useState({\n            inputValue: this.props.inputValue,\n            error: \"\",\n        });\n    }\n\n    get defaultTitle() {\n        return _t(\"Odoo Spreadsheet\");\n    }\n\n    confirm() {\n        const convertedValue = this.convertInputValue(this.state.inputValue);\n\n        if (this.props.inputType === \"number\" && isNaN(convertedValue)) {\n            this.state.error = _t(\"Please enter a valid number.\");\n            return;\n        }\n\n        this.props.close();\n        this.props.confirm?.(convertedValue);\n    }\n\n    convertInputValue(value) {\n        if (this.props.inputType === \"number\") {\n            return parseInt(value, 10);\n        }\n        return value;\n    }\n}\n", "/** @odoo-module **/\nimport { onMounted, onWillStart, useState, Component, useSubEnv } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pyToJsLocale } from \"@web/core/l10n/utils\";\nimport { registry } from \"@web/core/registry\";\n\nimport { UNTITLED_SPREADSHEET_NAME } from \"@spreadsheet/helpers/constants\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { Model, stores } from \"@odoo/o-spreadsheet\";\n\nimport { loadSpreadsheetDependencies } from \"@spreadsheet/assets_backend/helpers\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { SpreadsheetComponent } from \"@spreadsheet/actions/spreadsheet_component\";\nimport { SpreadsheetName } from \"../control_panel/spreadsheet_name\";\nimport {\n    useSpreadsheetCurrencies,\n    useSpreadsheetLocales,\n    useSpreadsheetThumbnail,\n} from \"../../hooks\";\nimport { formatToLocaleString } from \"../../helpers/misc\";\nimport { router } from \"@web/core/browser/router\";\nimport { RestoreVersionConfirmationDialog } from \"../../version_history/restore_version_dialog/restore_version_dialog\";\nimport { OdooDataProvider } from \"@spreadsheet/data_sources/odoo_data_provider\";\nimport { SpreadsheetNavbar } from \"../../components/spreadsheet_navbar/spreadsheet_navbar\";\n\nconst { ModelStore, useStoreProvider, SidePanelStore } = stores;\n\nexport class VersionHistoryAction extends Component {\n    static template = \"spreadsheet_edition.VersionHistoryAction\";\n    static components = {\n        SpreadsheetComponent,\n        SpreadsheetName,\n        SpreadsheetNavbar,\n    };\n    static props = { ...standardActionServiceProps };\n    static target = \"fullscreen\";\n\n    setup() {\n        this.params = this.props.action.params;\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n        this.actionService = useService(\"action\");\n        this.resId = this.params.spreadsheet_id || (this.props.state && this.props.state.resId); // used when going back to a spreadsheet via breadcrumb\n        this.resModel = this.params.res_model || (this.props.state && this.props.state.remoModel); // used when going back to a spreadsheet via breadcrumb\n        this.fromSnapshot =\n            this.params.from_snapshot || (this.props.state && this.props.state.fromSnapshot);\n        this.loadLocales = useSpreadsheetLocales();\n        this.loadCurrencies = useSpreadsheetCurrencies();\n        this.getThumbnail = useSpreadsheetThumbnail();\n\n        useSubEnv({\n            historyManager: {\n                getRevisions: this.getRevisions.bind(this),\n                forkHistory: this.forkHistory.bind(this),\n                restoreRevision: this.restoreRevision.bind(this),\n                renameRevision: this.renameRevision.bind(this),\n            },\n        });\n\n        this.state = useState({\n            spreadsheetName: UNTITLED_SPREADSHEET_NAME,\n            revisions: [],\n            restorableRevisions: [],\n        });\n\n        const stores = useStoreProvider();\n\n        onWillStart(async () => {\n            await this.fetchData();\n            this.createModel();\n            stores.inject(ModelStore, this.model);\n        });\n\n        onMounted(() => {\n            router.pushState({\n                spreadsheet_id: this.resId,\n                res_model: this.resModel,\n                from_snapshot: this.fromSnapshot,\n            });\n            this.env.config.setDisplayName(this.state.spreadsheetName);\n            const sidePanel = stores.get(SidePanelStore);\n            sidePanel.open(\"VersionHistory\", {\n                onCloseSidePanel: async () => {\n                    const action = await this.env.services.orm.call(\n                        this.resModel,\n                        \"action_open_spreadsheet\",\n                        [this.resId]\n                    );\n                    this.env.services.action.doAction(action, {\n                        clearBreadcrumbs: true,\n                    });\n                },\n            });\n        });\n    }\n\n    getRevisions() {\n        return this.state.restorableRevisions;\n    }\n\n    async renameRevision(revisionId, name) {\n        this.state.revisions.find((el) => el.id === revisionId).name = name;\n        this.generateRestorableRevisions();\n        await this.orm.call(this.resModel, \"rename_revision\", [this.resId, revisionId, name]);\n    }\n\n    async forkHistory(revisionId) {\n        const data = this.model.exportData();\n        const revision = this.state.restorableRevisions.find((rev) => rev.id === revisionId);\n        data.revisionId = revision.nextRevisionId;\n        const code = pyToJsLocale(this.model.getters.getLocale().code);\n        const timestamp = formatToLocaleString(revision.timestamp, code);\n        const name = _t(\"%(name)s (restored from %(timestamp)s)\", {\n            name: this.state.spreadsheetName,\n            timestamp,\n        });\n        const defaultValues = {\n            thumbnail: this.getThumbnail(),\n            name,\n        };\n        const action = await this.orm.call(this.resModel, \"fork_history\", [this.resId], {\n            revision_id: revisionId,\n            spreadsheet_snapshot: data,\n            default: defaultValues,\n        });\n        // Redirect to the forked spreadsheet\n        this.actionService.doAction(action, { clearBreadcrumbs: true });\n    }\n\n    async restoreRevision(revisionId) {\n        const revision = this.state.restorableRevisions.find((rev) => rev.id === revisionId);\n        const code = pyToJsLocale(this.model.getters.getLocale().code);\n        const timestamp = formatToLocaleString(revision.timestamp, code);\n        this.dialog.add(RestoreVersionConfirmationDialog, {\n            title: _t(\"Heads up!\"),\n            body: _t(\n                \"If you go ahead, your document will go back to the version from %s.\\nAny changes you've made after that time will disappear. Ready to proceed?\",\n                timestamp\n            ),\n            makeACopy: () => this.forkHistory(revisionId),\n            confirm: async () => {\n                const data = this.model.exportData();\n                const action = await this.orm.call(\n                    this.resModel,\n                    \"restore_spreadsheet_version\",\n                    [this.resId],\n                    {\n                        revision_id: revisionId,\n                        spreadsheet_snapshot: data,\n                    }\n                );\n                this.actionService.doAction(action, { clearBreadcrumbs: true });\n            },\n            cancel: () => {},\n        });\n    }\n\n    async fetchData() {\n        const [spreadsheetHistoryData] = await Promise.all([\n            this._fetchData(),\n            loadSpreadsheetDependencies(),\n        ]);\n        this.spreadsheetData = spreadsheetHistoryData.data;\n        this.spreadsheetDataLastDate = spreadsheetHistoryData.initial_date;\n        this.state.revisions = spreadsheetHistoryData.revisions;\n        this.generateRestorableRevisions();\n        this.state.spreadsheetName = spreadsheetHistoryData.name;\n        this.currentRevisionId =\n            spreadsheetHistoryData.revisions.at(-1)?.nextRevisionId ||\n            spreadsheetHistoryData.data.revisionId ||\n            \"START_REVISION\";\n        this.odooDataProvider = new OdooDataProvider(this.env);\n    }\n\n    generateRestorableRevisions() {\n        const revs = this.state.revisions\n            .slice()\n            .filter((el) => el.type !== \"SNAPSHOT_CREATED\")\n            .reverse();\n        const firstRevision = revs.at(-1);\n        if (firstRevision) {\n            revs.push({\n                id: 0,\n                nextRevisionId: firstRevision.serverRevisionId,\n                name: _t(\"Original data\"),\n                timestamp: this.spreadsheetDataLastDate,\n            });\n        }\n        this.state.restorableRevisions = revs;\n    }\n\n    /**\n     * @returns {Promise<SpreadsheetRecord>}\n     */\n    async _fetchData() {\n        const record = await this.orm.call(this.resModel, \"get_spreadsheet_history\", [\n            this.resId,\n            !!this.fromSnapshot,\n        ]);\n        return record;\n    }\n\n    /**\n     * @private\n     */\n    _dataSourceBind() {\n        const sheetId = this.model.getters.getActiveSheetId();\n        this.model.dispatch(\"EVALUATE_CELLS\", { sheetId });\n    }\n\n    reloadFromSnapshot() {\n        this.actionService.doAction(\n            {\n                type: \"ir.actions.client\",\n                tag: this.props.action.tag,\n                params: {\n                    spreadsheet_id: this.resId,\n                    res_model: this.resModel,\n                    from_snapshot: true,\n                },\n            },\n            { clearBreadcrumbs: true }\n        );\n    }\n\n    async loadEditAction() {\n        const action = await this.env.services.orm.call(this.resModel, \"action_open_spreadsheet\", [\n            this.resId,\n        ]);\n        this.actionService.doAction(action, {\n            clearBreadcrumbs: true,\n        });\n    }\n\n    createModel() {\n        this.odooDataProvider.addEventListener(\n            \"data-source-updated\",\n            this._dataSourceBind.bind(this)\n        );\n        const data = this.spreadsheetData;\n        this.model = new Model(\n            data,\n            {\n                custom: {\n                    env: this.env,\n                    orm: this.orm,\n                    odooDataProvider: this.odooDataProvider,\n                },\n                external: {\n                    loadCurrencies: this.loadCurrencies,\n                    loadLocales: this.loadLocales,\n                },\n                mode: \"readonly\",\n            },\n            this.state.revisions\n        );\n\n        if (this.model.session.serverRevisionId !== this.currentRevisionId) {\n            this.model = new Model({});\n            if (!this.fromSnapshot) {\n                this.dialog.add(ConfirmationDialog, {\n                    title: _t(\"Odoo Spreadsheet\"),\n                    body: _t(\n                        \"There are missing revisions that prevent to restore the whole edition history.\\n\\\nWould you like to load the more recent modifications?\"\n                    ),\n                    confirm: () => {\n                        this.reloadFromSnapshot();\n                    },\n                    close: () => {\n                        this.loadEditAction();\n                    },\n                });\n            } else {\n                this.dialog.add(ConfirmationDialog, {\n                    title: _t(\"Odoo Spreadsheet\"),\n                    body: _t(\n                        \"The history of your spreadsheet is corrupted and you are likely missing recent revisions. This feature cannot be used.\"\n                    ),\n                    confirm: () => {\n                        this.loadEditAction();\n                    },\n                });\n            }\n        }\n        if (this.env.debug) {\n            // eslint-disable-next-line no-import-assign\n            spreadsheet.__DEBUG__ = spreadsheet.__DEBUG__ || {};\n            spreadsheet.__DEBUG__.model = this.model;\n        }\n    }\n}\n\nregistry.category(\"actions\").add(\"action_open_spreadsheet_history\", VersionHistoryAction, {\n    force: true,\n});\n", "import { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\n\nimport { AbstractFigureClipboardHandler, registries } from \"@odoo/o-spreadsheet\";\n\nconst { clipboardHandlersRegistries } = registries;\n\nclass OdooChartFieldMatchingClipboardHandler extends AbstractFigureClipboardHandler {\n    copy({ figureId }) {\n        if (!this.getters.getChart(figureId)?.type.startsWith(\"odoo\")) {\n            return;\n        }\n        return {\n            odooChartFieldMatching: this.getters.getChartFieldMatch(figureId),\n        };\n    }\n\n    paste(target, clippedContent, options) {\n        const { figureId: newFigureId } = target;\n        const clippedMatchings = clippedContent.odooChartFieldMatching;\n        if (!clippedMatchings) {\n            return;\n        }\n\n        const odooChartIds = globalFiltersFieldMatchers[\"chart\"].getIds();\n        for (const filterId in clippedMatchings) {\n            const copiedFieldMatching = clippedMatchings[filterId];\n            const filter = this.getters.getGlobalFilter(filterId);\n            const currentChartMatchings = {};\n            // copy existing matching of other chars for this filter\n            for (const chartId of odooChartIds) {\n                currentChartMatchings[chartId] = this.getters.getOdooChartFieldMatching(\n                    chartId,\n                    filterId\n                );\n            }\n            if (options?.isCutOperation) {\n                delete currentChartMatchings[clippedContent.figureId];\n            }\n            if (copiedFieldMatching.chain === currentChartMatchings[newFigureId]?.chain) {\n                // avoid dispatching a command if the automatic field matching already set\n                // the same matching\n                continue;\n            }\n            currentChartMatchings[newFigureId] = copiedFieldMatching;\n            this.dispatch(\"EDIT_GLOBAL_FILTER\", {\n                filter,\n                chart: currentChartMatchings,\n            });\n        }\n    }\n}\n\nclipboardHandlersRegistries.figureHandlers.add(\n    \"odoo_chart_field_matching\",\n    OdooChartFieldMatchingClipboardHandler\n);\n", "import { AbstractFigureClipboardHandler, registries } from \"@odoo/o-spreadsheet\";\nconst { clipboardHandlersRegistries } = registries;\n\nclass OdooLinkClipboardHandler extends AbstractFigureClipboardHandler {\n    copy(data) {\n        const sheetId = this.getters.getActiveSheetId();\n        const figure = this.getters.getFigure(sheetId, data.figureId);\n        if (!figure) {\n            throw new Error(`No figure for the given id: ${data.figureId}`);\n        }\n        if (figure.tag !== \"chart\") {\n            return;\n        }\n        const odooMenuId = this.getters.getChartOdooMenu(data.figureId);\n        if (odooMenuId) {\n            return { odooMenuId };\n        }\n    }\n    paste(target, clippedContent, options) {\n        if (!target.figureId || !clippedContent.odooMenuId) {\n            return;\n        }\n        const { figureId } = target;\n        const { odooMenuId } = clippedContent;\n        this.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n            chartId: figureId,\n            odooMenuId: odooMenuId.xmlid || odooMenuId.id,\n        });\n    }\n}\n\nclipboardHandlersRegistries.figureHandlers.add(\"odoo_menu_link\", OdooLinkClipboardHandler);\n", "/** @odoo-module **/\n\nimport { helpers, stores } from \"@odoo/o-spreadsheet\";\nimport { initCallbackRegistry } from \"@spreadsheet/o_spreadsheet/init_callbacks\";\nimport { Domain } from \"@web/core/domain\";\n\nconst uuidGenerator = new helpers.UuidGenerator();\n\nconst { SidePanelStore } = stores;\n\nexport function insertChart(chartData) {\n    const chartType = `odoo_${chartData.metaData.mode}`;\n    const definition = {\n        metaData: {\n            groupBy: chartData.metaData.groupBy,\n            measure: chartData.metaData.measure,\n            order: chartData.metaData.order,\n            resModel: chartData.metaData.resModel,\n        },\n        searchParams: {\n            ...chartData.searchParams,\n            domain: new Domain(chartData.searchParams.domain).toJson(),\n        },\n        stacked: chartData.metaData.stacked,\n        fillArea: chartType === \"odoo_line\",\n        cumulative: chartData.metaData.cumulated,\n        title: { text: chartData.name },\n        background: \"#FFFFFF\",\n        legendPosition: \"top\",\n        verticalAxisPosition: \"left\",\n        type: chartType,\n        dataSourceId: uuidGenerator.uuidv4(),\n        id: uuidGenerator.uuidv4(),\n        actionXmlId: chartData.actionXmlId,\n    };\n    return (model, stores) => {\n        model.dispatch(\"CREATE_CHART\", {\n            sheetId: model.getters.getActiveSheetId(),\n            id: definition.id,\n            position: {\n                x: 10,\n                y: 10,\n            },\n            definition,\n        });\n        const sidePanel = stores.get(SidePanelStore);\n        sidePanel.open(\"ChartPanel\", { figureId: definition.id });\n    };\n}\n\ninitCallbackRegistry.add(\"insertChart\", insertChart);\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { IrMenuSelector } from \"@spreadsheet_edition/bundle/ir_menu_selector/ir_menu_selector\";\n\nconst { GenericChartConfigPanel, ScorecardChartConfigPanel, GaugeChartConfigPanel, Section } =\n    spreadsheet.components;\n\n/**\n * Patch the chart configuration panel to add an input to\n * link the chart to an Odoo menu.\n */\nfunction patchChartPanelWithMenu(PanelComponent) {\n    patch(PanelComponent.prototype, {\n        get odooMenuId() {\n            const menu = this.env.model.getters.getChartOdooMenu(this.props.figureId);\n            return menu ? menu.id : undefined;\n        },\n        /**\n         * @param {number | undefined} odooMenuId\n         */\n        updateOdooLink(odooMenuId) {\n            if (!odooMenuId) {\n                this.env.model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n                    chartId: this.props.figureId,\n                    odooMenuId: undefined,\n                });\n                return;\n            }\n            const menu = this.env.model.getters.getIrMenu(odooMenuId);\n            this.env.model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n                chartId: this.props.figureId,\n                odooMenuId: menu.xmlid || menu.id,\n            });\n        },\n    });\n    PanelComponent.components = {\n        ...PanelComponent.components,\n        IrMenuSelector,\n        Section,\n    };\n}\npatchChartPanelWithMenu(GenericChartConfigPanel);\npatchChartPanelWithMenu(GaugeChartConfigPanel);\npatchChartPanelWithMenu(ScorecardChartConfigPanel);\n", "/** @odoo-module */\n\nimport { IrMenuSelector } from \"@spreadsheet_edition/bundle/ir_menu_selector/ir_menu_selector\";\nimport { Domain } from \"@web/core/domain\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { components } from \"@odoo/o-spreadsheet\";\n\nimport { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nconst { Section } = components;\n\nexport class CommonOdooChartConfigPanel extends Component {\n    static template = \"spreadsheet_edition.CommonOdooChartConfigPanel\";\n    static components = { IrMenuSelector, DomainSelector, Section };\n    static props = {\n        figureId: String,\n        definition: Object,\n        updateChart: Function,\n        canUpdateChart: Function,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        const loadData = async (figureId) => {\n            const dataSource = this.env.model.getters.getChartDataSource(figureId);\n            this.modelDisplayName = await dataSource.getModelLabel();\n        };\n        onWillStart(() => loadData(this.props.figureId));\n        onWillUpdateProps((nextProps) => loadData(nextProps.figureId));\n    }\n\n    get model() {\n        const definition = this.env.model.getters.getChartDefinition(this.props.figureId);\n        return definition.metaData.resModel;\n    }\n\n    get domain() {\n        const definition = this.env.model.getters.getChartDefinition(this.props.figureId);\n        return new Domain(definition.searchParams.domain).toString();\n    }\n\n    onNameChanged(title) {\n        const definition = {\n            ...this.env.model.getters.getChartDefinition(this.props.figureId),\n            title,\n        };\n        this.env.model.dispatch(\"UPDATE_CHART\", {\n            id: this.props.figureId,\n            sheetId: this.env.model.getters.getFigureSheetId(this.props.figureId),\n            definition,\n        });\n    }\n\n    /**\n     * Get the last update date, formatted\n     *\n     * @returns {string} date formatted\n     */\n    getLastUpdate() {\n        const dataSource = this.env.model.getters.getChartDataSource(this.props.figureId);\n        const lastUpdate = dataSource.lastUpdate;\n        if (lastUpdate) {\n            return new Date(lastUpdate).toLocaleTimeString();\n        }\n        return _t(\"never\");\n    }\n\n    openDomainEdition() {\n        this.dialog.add(DomainSelectorDialog, {\n            resModel: this.model,\n            domain: new Domain(this.domain).toString(),\n            isDebugMode: !!this.env.debug,\n            onConfirm: (domain) => {\n                const definition = this.env.model.getters.getChartDefinition(this.props.figureId);\n                const updatedDefinition = {\n                    ...definition,\n                    searchParams: {\n                        ...definition.searchParams,\n                        domain: new Domain(domain).toJson(),\n                    },\n                };\n                this.env.model.dispatch(\"UPDATE_CHART\", {\n                    id: this.props.figureId,\n                    sheetId: this.env.model.getters.getFigureSheetId(this.props.figureId),\n                    definition: updatedDefinition,\n                });\n            },\n        });\n    }\n\n    get odooMenuId() {\n        const menu = this.env.model.getters.getChartOdooMenu(this.props.figureId);\n        return menu ? menu.id : undefined;\n    }\n    /**\n     * @param {number | undefined} odooMenuId\n     */\n    updateOdooLink(odooMenuId) {\n        if (!odooMenuId) {\n            this.env.model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n                chartId: this.props.figureId,\n                odooMenuId: undefined,\n            });\n            return;\n        }\n        const menu = this.env.model.getters.getIrMenu(odooMenuId);\n        this.env.model.dispatch(\"LINK_ODOO_MENU_TO_CHART\", {\n            chartId: this.props.figureId,\n            odooMenuId: menu.xmlid || menu.id,\n        });\n    }\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { CommonOdooChartConfigPanel } from \"./common/config_panel\";\nimport { OdooBarChartConfigPanel } from \"./odoo_bar/odoo_bar_config_panel\";\nimport { OdooLineChartConfigPanel } from \"./odoo_line/odoo_line_config_panel\";\nimport { OdooChartWithAxisDesignPanel } from \"./odoo_chart_with_axis/design_panel\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst { chartSidePanelComponentRegistry, chartSubtypeRegistry } = spreadsheet.registries;\nconst { PieChartDesignPanel } = spreadsheet.components;\n\nchartSidePanelComponentRegistry\n    .add(\"odoo_line\", {\n        configuration: OdooLineChartConfigPanel,\n        design: OdooChartWithAxisDesignPanel,\n    })\n    .add(\"odoo_bar\", {\n        configuration: OdooBarChartConfigPanel,\n        design: OdooChartWithAxisDesignPanel,\n    })\n    .add(\"odoo_pie\", {\n        configuration: CommonOdooChartConfigPanel,\n        design: PieChartDesignPanel,\n    });\n\nchartSubtypeRegistry.add(\"odoo_line\", {\n    matcher: (definition) =>\n        definition.type === \"odoo_line\" && !definition.stacked && !definition.fillArea,\n    subtypeDefinition: { stacked: false, fillArea: false },\n    displayName: _t(\"Line\"),\n    chartSubtype: \"odoo_line\",\n    chartType: \"odoo_line\",\n    category: \"line\",\n    preview: \"o-spreadsheet-ChartPreview.LINE_CHART\",\n});\nchartSubtypeRegistry.add(\"odoo_stacked_line\", {\n    matcher: (definition) =>\n        definition.type === \"odoo_line\" && definition.stacked && !definition.fillArea,\n    subtypeDefinition: { stacked: true, fillArea: false },\n    displayName: _t(\"Stacked Line\"),\n    chartSubtype: \"odoo_stacked_line\",\n    chartType: \"odoo_line\",\n    category: \"line\",\n    preview: \"o-spreadsheet-ChartPreview.STACKED_LINE_CHART\",\n});\nchartSubtypeRegistry.add(\"odoo_area\", {\n    matcher: (definition) =>\n        definition.type === \"odoo_line\" && !definition.stacked && definition.fillArea,\n    subtypeDefinition: { stacked: false, fillArea: true },\n    displayName: _t(\"Area\"),\n    chartSubtype: \"odoo_area\",\n    chartType: \"odoo_line\",\n    category: \"area\",\n    preview: \"o-spreadsheet-ChartPreview.AREA_CHART\",\n});\nchartSubtypeRegistry.add(\"odoo_stacked_area\", {\n    matcher: (definition) =>\n        definition.type === \"odoo_line\" && definition.stacked && definition.fillArea,\n    subtypeDefinition: { stacked: true, fillArea: true },\n    displayName: _t(\"Stacked Area\"),\n    chartSubtype: \"odoo_stacked_area\",\n    chartType: \"odoo_line\",\n    category: \"area\",\n    preview: \"o-spreadsheet-ChartPreview.STACKED_AREA_CHART\",\n});\nchartSubtypeRegistry.add(\"odoo_bar\", {\n    matcher: (definition) => definition.type === \"odoo_bar\" && !definition.stacked,\n    subtypeDefinition: { stacked: false },\n    displayName: _t(\"Column\"),\n    chartSubtype: \"odoo_bar\",\n    chartType: \"odoo_bar\",\n    category: \"column\",\n    preview: \"o-spreadsheet-ChartPreview.COLUMN_CHART\",\n});\nchartSubtypeRegistry.add(\"odoo_stacked_bar\", {\n    matcher: (definition) => definition.type === \"odoo_bar\" && definition.stacked,\n    subtypeDefinition: { stacked: true },\n    displayName: _t(\"Stacked Column\"),\n    chartSubtype: \"odoo_stacked_bar\",\n    chartType: \"odoo_bar\",\n    category: \"column\",\n    preview: \"o-spreadsheet-ChartPreview.STACKED_COLUMN_CHART\",\n});\nchartSubtypeRegistry.add(\"odoo_pie\", {\n    displayName: _t(\"Pie\"),\n    chartSubtype: \"odoo_pie\",\n    chartType: \"odoo_pie\",\n    category: \"pie\",\n    preview: \"o-spreadsheet-ChartPreview.PIE_CHART\",\n});\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { onWillUpdateProps } from \"@odoo/owl\";\n\nconst { chartSubtypeRegistry } = spreadsheet.registries;\nconst { ChartTypePicker } = spreadsheet.components;\n\n/**\n * This patch is necessary to ensure that the chart type cannot be changed\n * between odoo charts and spreadsheet charts.\n */\n\npatch(ChartTypePicker.prototype, {\n    setup() {\n        super.setup();\n        this.updateChartTypeByCategories(this.props);\n        onWillUpdateProps((nexProps) => this.updateChartTypeByCategories(nexProps));\n    },\n\n    /**\n     * @param {boolean} isOdoo\n     */\n    getChartTypes(isOdoo) {\n        const result = {};\n        for (const key of chartSubtypeRegistry.getKeys()) {\n            if ((isOdoo && key.startsWith(\"odoo_\")) || (!isOdoo && !key.startsWith(\"odoo_\"))) {\n                result[key] = chartSubtypeRegistry.get(key).name;\n            }\n        }\n        return result;\n    },\n\n    onTypeChange(type) {\n        if (this.getChartDefinition(this.props.figureId).type.startsWith(\"odoo_\")) {\n            const newChartInfo = chartSubtypeRegistry.get(type);\n            const definition = {\n                verticalAxisPosition: \"left\",\n                ...this.env.model.getters.getChartDefinition(this.props.figureId),\n                ...newChartInfo.subtypeDefinition,\n                type: newChartInfo.chartType,\n            };\n            this.env.model.dispatch(\"UPDATE_CHART\", {\n                definition,\n                id: this.props.figureId,\n                sheetId: this.env.model.getters.getActiveSheetId(),\n            });\n            this.closePopover();\n        } else {\n            super.onTypeChange(type);\n        }\n    },\n    updateChartTypeByCategories(props) {\n        const definition = this.env.model.getters.getChartDefinition(props.figureId);\n        const isOdoo = definition.type.startsWith(\"odoo_\");\n        const registryItems = chartSubtypeRegistry.getAll().filter((item) => {\n            return isOdoo\n                ? item.chartType.startsWith(\"odoo_\")\n                : !item.chartType.startsWith(\"odoo_\");\n        });\n\n        this.chartTypeByCategories = {};\n        for (const chartInfo of registryItems) {\n            if (this.chartTypeByCategories[chartInfo.category]) {\n                this.chartTypeByCategories[chartInfo.category].push(chartInfo);\n            } else {\n                this.chartTypeByCategories[chartInfo.category] = [chartInfo];\n            }\n        }\n    },\n});\n", "/** @odoo-module */\n\nimport { CommonOdooChartConfigPanel } from \"../common/config_panel\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { components } from \"@odoo/o-spreadsheet\";\n\nconst { Checkbox } = components;\n\nexport class OdooBarChartConfigPanel extends CommonOdooChartConfigPanel {\n    static template = \"spreadsheet_edition.OdooBarChartConfigPanel\";\n\n    static components = {\n        ...CommonOdooChartConfigPanel.components,\n        Checkbox,\n    };\n\n    get stackedLabel() {\n        return _t(\"Stacked linechart\");\n    }\n\n    onUpdateStacked(stacked) {\n        this.props.updateChart(this.props.figureId, {\n            stacked,\n        });\n    }\n}\n", "import { components, constants } from \"@odoo/o-spreadsheet\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst { ChartWithAxisDesignPanel } = components;\nconst { CHART_AXIS_CHOICES } = constants;\n\nexport class OdooChartWithAxisDesignPanel extends ChartWithAxisDesignPanel {\n    static template = \"spreadsheet_edition.OdooChartWithAxisDesignPanel\";\n\n    axisChoices = CHART_AXIS_CHOICES;\n\n    get axesList() {\n        return [\n            { id: \"x\", name: _t(\"Horizontal axis\") },\n            { id: \"y\", name: _t(\"Vertical axis\") },\n        ];\n    }\n\n    updateVerticalAxisPosition(verticalAxisPosition) {\n        this.props.updateChart(this.props.figureId, {\n            verticalAxisPosition,\n        });\n    }\n\n    toggleDataTrend(display) {\n        const trend = {\n            type: \"polynomial\",\n            order: 1,\n            ...this.props.definition.trend,\n            display,\n        };\n        this.props.updateChart(this.props.figureId, { trend });\n    }\n\n    getTrendLineConfiguration() {\n        return this.props.definition.trend;\n    }\n\n    getTrendType(config) {\n        if (!config) {\n            return \"\";\n        }\n        return config.type === \"polynomial\" && config.order === 1 ? \"linear\" : config.type;\n    }\n\n    onChangeTrendType(ev) {\n        const type = ev.target.value;\n        let config;\n        switch (type) {\n            case \"linear\":\n            case \"polynomial\":\n                config = {\n                    type: \"polynomial\",\n                    order: type === \"linear\" ? 1 : 2,\n                };\n                break;\n            case \"exponential\":\n            case \"logarithmic\":\n                config = { type };\n                break;\n            default:\n                return;\n        }\n        this.updateTrendLineValue(config);\n    }\n\n    onChangePolynomialDegree(ev) {\n        const element = ev.target;\n        const order = parseInt(element.value || \"1\");\n        if (order < 2) {\n            element.value = `${this.getTrendLineConfiguration()?.order ?? 2}`;\n            return;\n        }\n        this.updateTrendLineValue({ order });\n    }\n\n    updateTrendLineValue(config) {\n        const trend = {\n            ...this.props.definition.trend,\n            ...config,\n        };\n        this.props.updateChart(this.props.figureId, { trend });\n    }\n}\n", "/** @odoo-module */\n\nimport { CommonOdooChartConfigPanel } from \"../common/config_panel\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { components } from \"@odoo/o-spreadsheet\";\n\nconst { Checkbox } = components;\n\nexport class OdooLineChartConfigPanel extends CommonOdooChartConfigPanel {\n    static template = \"spreadsheet_edition.OdooLineChartConfigPanel\";\n    static components = {\n        ...CommonOdooChartConfigPanel.components,\n        Checkbox,\n    };\n\n    get stackedLabel() {\n        return _t(\"Stacked linechart\");\n    }\n\n    get cumulativeLabel() {\n        return _t(\"Cumulative data\");\n    }\n\n    onUpdateStacked(stacked) {\n        this.props.updateChart(this.props.figureId, {\n            stacked,\n        });\n    }\n    onUpdateCumulative(cumulative) {\n        this.props.updateChart(this.props.figureId, {\n            cumulative,\n        });\n    }\n}\n", "import { AbstractCellClipboardHandler, helpers } from \"@odoo/o-spreadsheet\";\n\nconst { isInside } = helpers;\n\nexport class CellThreadsClipboardHandler extends AbstractCellClipboardHandler {\n    copy(data) {\n        if (!data.zones.length) {\n            return;\n        }\n\n        const zones = data.zones;\n        const sheetId = this.getters.getActiveSheetId();\n        const sheetThreads = this.getters.getSpreadsheetThreads([sheetId]);\n        // we only support cut which means one 1 zone - no need to fetch the entire planet\n        const threads = sheetThreads.filter((thread) => isInside(thread.col, thread.row, zones[0]));\n        return { threads, zones };\n    }\n\n    /**\n     * Paste the clipboard content in the given target\n     */\n    paste(target, content, options) {\n        if (!content.threads?.length || !target.zones.length) {\n            return;\n        }\n        if (options?.isCutOperation && !options?.pasteOption) {\n            const zones = target.zones;\n            const sheetId = this.getters.getActiveSheetId();\n            this.pasteFromCut(sheetId, zones, content);\n        }\n    }\n\n    pasteFromCut(sheetId, target, content) {\n        const cutZone = content.zones[0];\n\n        const deltaCol = target[0].left - cutZone.left;\n        const deltaRow = target[0].top - cutZone.top;\n        for (const thread of content.threads) {\n            this.dispatch(\"DELETE_COMMENT_THREAD\", {\n                sheetId: thread.sheetId,\n                col: thread.col,\n                row: thread.row,\n                threadId: thread.threadId,\n            });\n\n            this.dispatch(\"ADD_COMMENT_THREAD\", {\n                sheetId,\n                col: thread.col + deltaCol,\n                row: thread.row + deltaRow,\n                threadId: thread.threadId,\n            });\n        }\n    }\n}\n", "import { helpers, stores } from \"@odoo/o-spreadsheet\";\nconst { positionToZone, isInside } = helpers;\n\nconst { SpreadsheetStore, CellPopoverStore } = stores;\n\nconst ACTIVE_BACKGROUND_COLOR = \"#FFD73333\";\n\nexport class CommentsStore extends SpreadsheetStore {\n    mutators = [\"toggleComments\", \"openCommentThread\"];\n    selectedThreadId = undefined;\n    /** @private */\n    areCommentsActive = true;\n    cellPopoverStore = this.get(CellPopoverStore);\n    drawResolvedBackground = false;\n\n    constructor(get) {\n        super(get);\n        this.model.selection.observe(this, {\n            handleEvent: this.handleEvent.bind(this),\n        });\n    }\n\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"SET_VIEWPORT_OFFSET\":\n                if (this.cellPopoverStore.persistentCellPopover?.type === \"OdooCellComment\") {\n                    this.cellPopoverStore.close();\n                }\n                break;\n        }\n    }\n\n    get renderingLayers() {\n        return [\"Triangle\"];\n    }\n\n    handleEvent(event) {\n        if (this.getters.isGridSelectionActive() && !this.getters.isReadonly()) {\n            this.drawResolvedBackground = false;\n            const position = this.getters.getActivePosition();\n            const threads = this.getters.getCellThreads(position);\n            if (threads && threads.length) {\n                const thread =\n                    threads.find((thread) => thread.threadId === this.selectedThreadId) ||\n                    threads.at(-1);\n                this.selectedThreadId = thread.threadId;\n                if (!thread.isResolved) {\n                    this.cellPopoverStore.open(position, \"OdooCellComment\");\n                    this.drawResolvedBackground = true;\n                } else {\n                    if (this.cellPopoverStore.persistentCellPopover?.type === \"OdooCellComment\") {\n                        this.cellPopoverStore.close();\n                    }\n                }\n            } else {\n                this.selectedThreadId = undefined;\n            }\n        }\n    }\n\n    toggleComments() {\n        this.areCommentsActive = !this.areCommentsActive;\n        if (\n            !this.areCommentsActive &&\n            this.cellPopoverStore.persistentCellPopover?.type === \"OdooCellComment\"\n        ) {\n            this.cellPopoverStore.close();\n            this.selectedThreadId = undefined;\n        }\n    }\n\n    openCommentThread(threadId) {\n        const threadInfo = this.getters.getThreadInfo(threadId);\n        if (!threadInfo || this.getters.isReadonly()) {\n            return;\n        }\n        this.areCommentsActive = true;\n        const { sheetId, col, row } = threadInfo;\n        const activeSheetId = this.getters.getActiveSheetId();\n        if (sheetId !== activeSheetId) {\n            this.model.dispatch(\"ACTIVATE_SHEET\", {\n                sheetIdFrom: activeSheetId,\n                sheetIdTo: sheetId,\n            });\n        }\n        this.selectedThreadId = threadId;\n        this.model.selection.selectCell(col, row);\n        this.drawResolvedBackground = true;\n    }\n\n    /**\n     *\n     * @param {*} ctx Grid rendering context\n     */\n    drawLayer({ ctx }, layer) {\n        if (!this.areCommentsActive || layer !== \"Triangle\" || this.getters.isReadonly()) {\n            return;\n        }\n\n        const sheetId = this.getters.getActiveSheetId();\n        const threadInfos = this.getters.getThreadInfosInSheet(sheetId);\n\n        for (const { col, row, isResolved } of threadInfos) {\n            const zone = this.getters.expandZone(sheetId, positionToZone({ col, row }));\n            if (zone.left !== col || zone.top !== row) {\n                continue;\n            }\n            const { x, y, width, height } = this.getters.getVisibleRect(zone);\n            if (width > 0 && height > 0) {\n                const { col: activeCol, row: activeRow } = this.getters.getActivePosition();\n                if (\n                    isInside(activeCol, activeRow, zone) &&\n                    (this.drawResolvedBackground || !isResolved)\n                ) {\n                    ctx.fillStyle = ACTIVE_BACKGROUND_COLOR;\n                    ctx.fillRect(x, y, width, height);\n                }\n                if (!isResolved) {\n                    ctx.fillStyle = \"orange\";\n                    ctx.beginPath();\n                    ctx.moveTo(x + 5, y);\n                    ctx.lineTo(x, y + 5);\n                    ctx.lineTo(x, y);\n                    ctx.fill();\n                }\n            }\n        }\n    }\n}\n", "import { Component, useState, onWillUpdateProps, useChildSubEnv, onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { SpreadsheetCommentComposer } from \"./spreadsheet_comment_composer\";\n\nexport class CellThread extends Component {\n    static template = \"spreadsheet_edition.CellThread\";\n    static components = { Thread, Composer, SpreadsheetCommentComposer };\n\n    static props = {\n        threadId: Number,\n        edit: Boolean,\n        autofocus: { type: Number, optional: true },\n    };\n    static defaultProps = { autofocus: 0 };\n    static threadModel = \"spreadsheet.cell.thread\";\n\n    setup() {\n        useChildSubEnv({\n            inChatWindow: true,\n            chatter: {},\n        });\n        /** @type {import(\"models\").Store} */\n        this.mailStore = useService(\"mail.store\");\n        this.state = useState({\n            /** @type {import(\"models\").Thread} */\n            thread: undefined,\n        });\n        onWillStart(() => this.loadThread(this.props.threadId));\n\n        onWillUpdateProps(async (nextProps) => {\n            if (this.props.threadId !== nextProps.threadId) {\n                await this.loadThread(nextProps.threadId);\n            }\n        });\n    }\n\n    async loadThread(threadId) {\n        this.state.thread = this.mailStore.Thread.insert({\n            model: CellThread.threadModel,\n            id: threadId,\n        });\n        await this.state.thread.fetchNewMessages();\n    }\n}\n", "import { Component, useState, onWillUpdateProps, useChildSubEnv } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { CellThread } from \"./cell_thread\";\nimport { SpreadsheetCommentComposer } from \"./spreadsheet_comment_composer\";\nimport { CommentsStore } from \"../comments_store\";\nimport { stores } from \"@odoo/o-spreadsheet\";\n\nconst { useStore } = stores;\n\nexport class CellThreadPopover extends Component {\n    static template = \"spreadsheet_edition.CellThreadPopover\";\n    static components = { Thread, CellThread, SpreadsheetCommentComposer };\n\n    static props = {\n        threadId: {\n            optional: true,\n            type: Number,\n        },\n        onClosed: {\n            optional: true,\n            type: Function,\n        },\n        focused: Boolean,\n        position: Object,\n    };\n\n    static defaultProps = { focus: false };\n    static threadModel = \"spreadsheet.cell.thread\";\n\n    setup() {\n        useChildSubEnv({\n            inChatWindow: true,\n        });\n\n        /** @type {import(\"models\").Store} */\n        this.mailStore = useService(\"mail.store\");\n        this.state = useState({\n            /** @type {import(\"models\").Thread} */\n            thread: undefined,\n            isValid: true,\n        });\n        this.loadThread(this.props.threadId);\n\n        this.commentsStore = useStore(CommentsStore);\n\n        onWillUpdateProps((nextProps) => {\n            this.loadThread(nextProps.threadId);\n        });\n    }\n\n    onFocused() {\n        if (this.props.threadId && !this.props.focused) {\n            this.commentsStore.openCommentThread(this.props.threadId);\n        }\n    }\n\n    showAllComments() {\n        this.env.openSidePanel(\"Comments\");\n    }\n\n    loadThread(threadId) {\n        this.state.thread = this.mailStore.Thread.insert({\n            model: CellThreadPopover.threadModel,\n            id: threadId,\n        });\n    }\n\n    async insertNewThread(value, postData) {\n        if (!value) {\n            return;\n        }\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const threadId = await this.env.insertThreadInSheet({ sheetId, ...this.props.position });\n        this.loadThread(threadId);\n        await this.state.thread.post(value, postData);\n        this.commentsStore.openCommentThread(this.props.threadId);\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(Composer.prototype, {\n    /**\n     * This function overrides the original method so that when the user tries to open a the record\n     * from a starred discussion linked to a spreadsheet cell thread, it can be redirected to the corresponding\n     * spreadsheet record.\n     * @override\n     */\n    get SEND_TEXT() {\n        if (this.props.composer?.thread?.model === \"spreadsheet.cell.thread\") {\n            return _t(\"Send\");\n        } else {\n            return super.SEND_TEXT;\n        }\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer\";\n\n/**\n * This Component is an extension of the classic Composer used inside the chatter. It is called when a\n * user is just creating a new comment => when we set the id of the Thread to 0.\n * This enables us to limit the creation of void comments inside the DB and lessen journal entries in\n * it.\n * After comment creation, this component is destroyed and is replaced with the regular Composer.\n */\n\nexport class SpreadsheetCommentComposer extends Composer {\n    constructor(...args) {\n        super(...args);\n    }\n    /**\n     * @override\n     */\n    async _sendMessage(value, postData) {\n        await this.props.onPostCallback(value, postData);\n        this.props.composer.clear();\n        return;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { CellThreadsPlugin } from \"./plugins/comments_core_plugin\";\n\nimport { coreTypes, registries, stores, addRenderingLayer } from \"@odoo/o-spreadsheet\";\nimport { CommentThreadsSidePanel } from \"./side_panel/comment_threads_side_panel\";\nimport { CellThreadPopover } from \"./components/cell_thread_popover\";\nimport { CommentsStore } from \"./comments_store\";\nimport { CellThreadsClipboardHandler } from \"./clipboard_handler\";\n\nconst { CellPopoverStore } = stores;\n\nconst {\n    cellPopoverRegistry,\n    cellMenuRegistry,\n    corePluginRegistry,\n    clipboardHandlersRegistries,\n    inverseCommandRegistry,\n    topbarMenuRegistry,\n    sidePanelRegistry,\n    otRegistry,\n} = registries;\n\ncorePluginRegistry.add(\"odooCellThreadsPlugin\", CellThreadsPlugin);\nclipboardHandlersRegistries.cellHandlers.add(\"commentThreads\", CellThreadsClipboardHandler);\n\nfunction identity(cmd) {\n    return [cmd];\n}\n\ncoreTypes.add(\"ADD_COMMENT_THREAD\");\ncoreTypes.add(\"DELETE_COMMENT_THREAD\");\ncoreTypes.add(\"EDIT_COMMENT_THREAD\");\n\ninverseCommandRegistry.add(\"ADD_COMMENT_THREAD\", identity);\ninverseCommandRegistry.add(\"DELETE_COMMENT_THREAD\", identity);\ninverseCommandRegistry.add(\"EDIT_COMMENT_THREAD\", identity);\n\notRegistry.addTransformation(\n    \"DELETE_COMMENT_THREAD\",\n    [\"ADD_COMMENT_THREAD\", \"EDIT_COMMENT_THREAD\"],\n    (toTransform, executed) => {\n        if (toTransform.threadId === executed.threadId) {\n            return undefined;\n        }\n        return toTransform;\n    }\n);\n\ncellPopoverRegistry.add(\"OdooCellComment\", {\n    onOpen: (position, getters) => {\n        if (getters.isReadonly()) {\n            return;\n        }\n        const sheetId = getters.getActiveSheetId();\n        const thread =\n            getters\n                .getCellThreads({ sheetId, ...position })\n                ?.filter((thread) => !thread.isResolved)\n                ?.at(-1) || {};\n        return {\n            Component: CellThreadPopover,\n            cellCorner: \"TopRight\",\n            props: {\n                threadId: thread.threadId,\n                position,\n                focused: true,\n            },\n            isOpen: true,\n            positioning: \"TopRight\",\n        };\n    },\n    onHover: (position, getters) => {\n        if (getters.isReadonly()) {\n            return;\n        }\n        const sheetId = getters.getActiveSheetId();\n        const threads = getters\n            .getCellThreads({ sheetId, ...position })\n            ?.filter((thread) => !thread.isResolved);\n        if (!threads || !threads.length) {\n            return undefined;\n        }\n        return {\n            Component: CellThreadPopover,\n            cellCorner: \"TopRight\",\n            props: {\n                threadId: threads.at(-1).threadId,\n                position,\n                focused: false,\n            },\n            isOpen: true,\n            positioning: \"TopRight\",\n        };\n    },\n});\n\nconst INSERT_COMMENT_ACTION = {\n    name: _t(\"Insert comment\"),\n    isVisible: (env) => env.insertThreadInSheet && env.getStore(CommentsStore).areCommentsActive,\n    execute: async (env) => {\n        const { col, row } = env.model.getters.getActivePosition();\n        env.getStore(CellPopoverStore).open({ col, row }, \"OdooCellComment\");\n    },\n    icon: \"o-spreadsheet-Icon.COMMENTS\",\n};\n\ncellMenuRegistry.add(\"insert_comment\", {\n    ...INSERT_COMMENT_ACTION,\n    sequence: 150,\n    separator: true,\n});\n\ntopbarMenuRegistry.addChild(\"insert_comment\", [\"insert\"], {\n    ...INSERT_COMMENT_ACTION,\n    sequence: 150,\n});\n\ntopbarMenuRegistry.addChild(\"show_comments\", [\"view\", \"show\"], {\n    name: _t(\"Comments\"),\n    sequence: 1500,\n    execute: (env) => env.getStore(CommentsStore).toggleComments(),\n    isActive: (env) => env.getStore(CommentsStore).areCommentsActive,\n    isVisible: (env) => env.insertThreadInSheet,\n});\n\ntopbarMenuRegistry.addChild(\"view_comments\", [\"view\"], {\n    name: _t(\"All Comments\"),\n    sequence: 1500,\n    execute: (env) => env.openSidePanel(\"Comments\"),\n    icon: \"o-spreadsheet-Icon.COMMENTS\",\n    isVisible: (env) => env.insertThreadInSheet,\n});\n\nsidePanelRegistry.add(\"Comments\", {\n    title: _t(\"Comments\"),\n    Body: CommentThreadsSidePanel,\n});\n\n// after the grid but before the highlights\naddRenderingLayer(\"Triangle\", 0.5);\n", "// @ts-check\n\nimport { helpers } from \"@odoo/o-spreadsheet\";\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\n\nconst { toXC, toCartesian, expandZoneOnInsertion, reduceZoneOnDeletion } = helpers;\n\n/**\n * @typedef {import(\"@spreadsheet\").UID} UID\n *\n * @typedef ThreadInfo\n * @property {number} col\n * @property {number} row\n * @property {string} sheetId\n * @property {number} threadId\n * @property {boolean} isResolved\n *\n * @typedef ThreadEntry\n * @property {number} threadId\n * @property {boolean} isResolved\n * @property {number} col\n * @property {number} row\n *\n */\n\nexport class CellThreadsPlugin extends OdooCorePlugin {\n    static getters = [\n        \"getCellThreads\",\n        \"getThreadInfosInSheet\",\n        \"getThreadInfo\",\n        \"getSpreadsheetThreads\",\n    ];\n\n    /**\n     * @readonly\n     * @type {Object<UID, Object<string, Array<ThreadEntry>>>}\n     */\n    cellThreads = {};\n\n    /**\n     * @param {import(\"@spreadsheet\").AllCoreCommand} cmd\n     */\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_COMMENT_THREAD\":\n                {\n                    const { sheetId, col, row, threadId } = cmd;\n                    const xc = toXC(col, row);\n                    if (!this.cellThreads[sheetId]) {\n                        this.cellThreads[sheetId] = {};\n                    }\n                    const threads = this.cellThreads[sheetId]?.[xc] || [];\n                    const newThreads = [...threads, { threadId, isResolved: false }];\n                    this.history.update(\"cellThreads\", sheetId, xc, newThreads);\n                }\n                break;\n            case \"EDIT_COMMENT_THREAD\":\n                {\n                    const { threadId, isResolved, sheetId, col, row } = cmd;\n                    const xc = toXC(col, row);\n                    const threads = this.cellThreads[sheetId][xc];\n                    const newThreads = threads.map((thread) =>\n                        thread.threadId === threadId ? { ...thread, isResolved } : thread\n                    );\n                    this.history.update(\"cellThreads\", sheetId, xc, newThreads);\n                }\n                break;\n            case \"DELETE_COMMENT_THREAD\": {\n                const { threadId, sheetId, col, row } = cmd;\n                const xc = toXC(col, row);\n                const currentThreadIds = this.cellThreads[sheetId]?.[xc] || [];\n                const newThreadIds = currentThreadIds.filter(\n                    (thread) => thread.threadId !== threadId\n                );\n                this.history.update(\"cellThreads\", sheetId, xc, newThreadIds);\n                break;\n            }\n            case \"ADD_COLUMNS_ROWS\":\n                this.onAddColumnsRows(cmd);\n                break;\n            case \"REMOVE_COLUMNS_ROWS\":\n                this.onDeleteColumnsRows(cmd);\n                break;\n            case \"CREATE_SHEET\":\n                this.history.update(\"cellThreads\", cmd.sheetId, {});\n                break;\n            case \"DUPLICATE_SHEET\":\n                this.history.update(\"cellThreads\", cmd.sheetIdTo, {});\n                break;\n            case \"DELETE_SHEET\":\n                {\n                    const threads = { ...this.cellThreads };\n                    delete threads[cmd.sheetId];\n                    this.history.update(\"cellThreads\", threads);\n                }\n                break;\n        }\n    }\n\n    /**\n     * @param {number} threadId\n     * @returns {ThreadInfo}\n     */\n    getThreadInfo(threadId) {\n        for (const [sheetId, threadXcs] of Object.entries(this.cellThreads)) {\n            for (const [xc, xcThreads] of Object.entries(threadXcs)) {\n                const thread = xcThreads?.find((thread) => thread.threadId === threadId);\n                if (thread) {\n                    return { sheetId, ...toCartesian(xc), ...thread };\n                }\n            }\n        }\n    }\n\n    /**\n     * @returns {Array<number> | undefined} threadIds\n     */\n    getCellThreads({ sheetId, col, row }) {\n        return (this.cellThreads[sheetId] || {})[toXC(col, row)];\n    }\n\n    /**\n     * @param {string} sheetId\n     * @returns {Array<ThreadInfo>}\n     */\n    getThreadInfosInSheet(sheetId) {\n        return this.getSpreadsheetThreads([sheetId]);\n    }\n\n    /**\n     *\n     * @param {Array<UID>} sheetIds\n     * @returns {Array<ThreadInfo>}\n     */\n    getSpreadsheetThreads(sheetIds) {\n        const spreadsheetThreads = [];\n        for (const sheetId of sheetIds) {\n            const sheetThreads = this.cellThreads[sheetId];\n            if (sheetThreads) {\n                for (const [xc, threads] of Object.entries(sheetThreads)) {\n                    const { col, row } = toCartesian(xc);\n                    for (const thread of threads) {\n                        spreadsheetThreads.push({ sheetId, col, row, ...thread });\n                    }\n                }\n            }\n        }\n        return spreadsheetThreads;\n    }\n    // ---------------------------------------------------------------------------\n    // Import/Export\n    // ---------------------------------------------------------------------------\n\n    export(data) {\n        for (const sheet of data.sheets) {\n            sheet.comments = this.cellThreads[sheet.id];\n        }\n    }\n\n    import(data) {\n        for (const sheet of data.sheets) {\n            this.cellThreads[sheet.id] = {};\n            if (sheet.comments) {\n                this.cellThreads[sheet.id] = sheet.comments;\n            }\n        }\n    }\n\n    onAddColumnsRows({ sheetId, base, dimension, position, quantity }) {\n        const newCellThreadIds = {};\n        for (const xc of Object.keys(this.cellThreads[sheetId])) {\n            const { col, row } = toCartesian(xc);\n            const zone = { left: col, right: col, top: row, bottom: row };\n            const newZone = expandZoneOnInsertion(\n                zone,\n                dimension === \"COL\" ? \"left\" : \"top\",\n                base,\n                position,\n                quantity\n            );\n            if (newZone) {\n                const { left, top } = newZone;\n                const threads = this.cellThreads[sheetId][xc];\n                const newXc = toXC(left, top);\n                newCellThreadIds[newXc] = threads;\n            }\n        }\n        this.history.update(\"cellThreads\", sheetId, newCellThreadIds);\n    }\n\n    onDeleteColumnsRows({ sheetId, elements, dimension }) {\n        const newCellThreadIds = {};\n        for (const xc of Object.keys(this.cellThreads[sheetId])) {\n            const { col, row } = toCartesian(xc);\n            const zone = { left: col, right: col, top: row, bottom: row };\n            const newZone = reduceZoneOnDeletion(\n                zone,\n                dimension === \"COL\" ? \"left\" : \"top\",\n                elements\n            );\n            if (newZone) {\n                const { left, top } = newZone;\n                const threads = this.cellThreads[sheetId][xc];\n                const newXc = toXC(left, top);\n                newCellThreadIds[newXc] = threads;\n            }\n        }\n        this.history.update(\"cellThreads\", sheetId, newCellThreadIds);\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { CellThread } from \"../components/cell_thread\";\nimport { helpers, stores, components } from \"@odoo/o-spreadsheet\";\nimport { CommentsStore } from \"../comments_store\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst { toXC, createActions } = helpers;\nconst { useStore } = stores;\nconst { Menu, Section } = components;\n\nexport class CommentThreadsSidePanel extends Component {\n    static template = \"documents_spreadsheet.CommentThreadsSidePanel\";\n    static props = { onCloseSidePanel: Function };\n    static components = { CellThread, Menu, Section };\n\n    setup() {\n        this.state = useState({\n            mode: \"allSheets\",\n        });\n        this.menuState = useState({\n            isOpen: false,\n            position: null,\n            threadId: undefined,\n        });\n        this.commentsStore = useStore(CommentsStore);\n    }\n\n    get sheetIds() {\n        return this.state.mode === \"allSheets\"\n            ? this.env.model.getters.getSheetIds()\n            : [this.env.model.getters.getActiveSheetId()];\n    }\n\n    get selectedThreadId() {\n        return this.commentsStore.selectedThreadId;\n    }\n\n    get spreadsheetThreads() {\n        const sheetIds =\n            this.state.mode === \"activeSheet\"\n                ? [this.env.model.getters.getActiveSheetId()]\n                : this.env.model.getters.getSheetIds();\n        return this.env.model.getters.getSpreadsheetThreads(sheetIds);\n    }\n\n    /**\n     * @param {number} numberOfComments\n     */\n    getNumberOfCommentsLabel(numberOfComments) {\n        return numberOfComments === 1 ? _t(\"1 thread\") : _t(\"%s threads\", numberOfComments);\n    }\n\n    getThreadTitle(threadId) {\n        const { col, row } = this.env.model.getters.getThreadInfo(threadId);\n        const xc = toXC(col, row);\n        return `${xc}`;\n    }\n\n    selectThread(threadId) {\n        this.commentsStore.openCommentThread(threadId);\n    }\n\n    get menuItems() {\n        if (!this.menuState.threadId) {\n            return [];\n        }\n        const { sheetId, col, row, isResolved } = this.env.model.getters.getThreadInfo(\n            this.menuState.threadId\n        );\n        return createActions([\n            {\n                name: isResolved ? _t(\"Re-open this thread\") : _t(\"Resolve this thread\"),\n                execute: () => {\n                    this.env.model.dispatch(\"EDIT_COMMENT_THREAD\", {\n                        sheetId,\n                        col,\n                        row,\n                        threadId: this.menuState.threadId,\n                        isResolved: !isResolved,\n                    });\n                    this.commentsStore.openCommentThread(this.menuState.threadId);\n                },\n            },\n        ]);\n    }\n\n    openMenu(ev, threadId) {\n        this.selectThread(threadId);\n        const { x, y, height } = ev.target.getBoundingClientRect();\n        this.menuState.isOpen = true;\n        this.menuState.position = { x, y: y + height };\n        this.menuState.threadId = threadId;\n    }\n\n    closeMenu() {\n        this.menuState.isOpen = false;\n        this.menuState.position = null;\n        this.menuState.threadId = undefined;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { registries } from \"@spreadsheet/o_spreadsheet/o_spreadsheet\";\nconst { topbarComponentRegistry } = registries;\n\nexport class CollaborativeStatus extends Component {\n    static template = \"spreadsheet_edition.CollaborativeStatus\";\n    static props = {};\n\n    get isSynced() {\n        return this.env.model.getters.isFullySynchronized();\n    }\n\n    get connectedUsers() {\n        const connectedUsers = [];\n        for (const client of this.env.model.getters.getConnectedClients()) {\n            if (!connectedUsers.some((user) => user.id === client.userId)) {\n                connectedUsers.push({\n                    id: client.userId,\n                    name: client.name,\n                });\n            }\n        }\n        return connectedUsers;\n    }\n\n    get tooltipInfo() {\n        return JSON.stringify({\n            users: this.connectedUsers.map((/**@type User*/ user) => {\n                return {\n                    name: user.name,\n                    avatar: `/web/image?model=res.users&field=avatar_128&id=${user.id}`,\n                };\n            }),\n        });\n    }\n}\n\ntopbarComponentRegistry.add(\"collaborative_status\", {\n    component: CollaborativeStatus,\n    sequence: 10,\n});\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registries, helpers } from \"@spreadsheet/o_spreadsheet/o_spreadsheet\";\nconst { topbarComponentRegistry } = registries;\n\nfunction getLocalesComparison(spreadsheetLocale, userLocale) {\n    const differences = [];\n    if (spreadsheetLocale.dateFormat !== userLocale.dateFormat) {\n        differences.push(_t(\"- dates: %s\", spreadsheetLocale.dateFormat));\n    }\n\n    if (\n        spreadsheetLocale.thousandsSeparator !== userLocale.thousandsSeparator ||\n        spreadsheetLocale.decimalSeparator !== userLocale.decimalSeparator\n    ) {\n        differences.push(\n            _t(\n                \"- numbers: %s\",\n                helpers.formatValue(1234567.89, {\n                    format: \"#,##0.00\",\n                    locale: spreadsheetLocale,\n                })\n            )\n        );\n    }\n\n    return differences.join(\"\\n\");\n}\n\nexport class LocaleStatus extends Component {\n    static template = \"spreadsheet_edition.LocaleStatus\";\n    static props = {};\n\n    get mismatchedLocaleTitle() {\n        const spreadsheetLocale = this.env.model.getters.getLocale();\n        const userLocale = this.env.getUserLocale();\n\n        const title = _t(\n            \"Difference between user locale (%(user_locale)s) and spreadsheet locale (%(spreadsheet_locale)s). This spreadsheet is using the formats below:\",\n            {\n                user_locale: userLocale.code,\n                spreadsheet_locale: spreadsheetLocale.code,\n            }\n        );\n        const comparison = getLocalesComparison(spreadsheetLocale, userLocale);\n\n        return comparison ? title + \"\\n\" + comparison : \"\";\n    }\n}\n\ntopbarComponentRegistry.add(\"locale_status\", {\n    component: LocaleStatus,\n    isVisible: (env) =>\n        env.getUserLocale?.() &&\n        getLocalesComparison(env.model.getters.getLocale(), env.getUserLocale()) !== \"\",\n    sequence: 12,\n});\n", "import { Component } from \"@odoo/owl\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { Domain } from \"@web/core/domain\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { components } from \"@odoo/o-spreadsheet\";\n\nconst { Section } = components;\n\nexport class SidePanelDomain extends Component {\n    static template = \"spreadsheet_edition.SidePanelDomain\";\n    static components = {\n        DomainSelector,\n        Section,\n    };\n    static props = {\n        resModel: String,\n        domain: [Domain, String, Array],\n        onUpdate: Function,\n    };\n\n    getStringifiedDomain() {\n        return new Domain(this.props.domain).toString();\n    }\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n    }\n\n    openDomainEdition() {\n        this.dialog.add(DomainSelectorDialog, {\n            resModel: this.props.resModel,\n            domain: this.getStringifiedDomain(),\n            isDebugMode: !!this.env.debug,\n            onConfirm: (domain) => {\n                this.props.onUpdate(new Domain(domain).toJson());\n            },\n        });\n    }\n}\n", "import { EnterpriseNavBar } from \"@web_enterprise/webclient/navbar/navbar\";\nimport { SpreadsheetName } from \"../../actions/control_panel/spreadsheet_name\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useState } from \"@odoo/owl\";\n\nexport class SpreadsheetNavbar extends EnterpriseNavBar {\n    static template = \"spreadsheet_edition.SpreadsheetNavbar\";\n    static components = { ...EnterpriseNavBar.components, SpreadsheetName };\n    static props = {\n        spreadsheetName: String,\n        isReadonly: {\n            type: Boolean,\n            optional: true,\n        },\n        onSpreadsheetNameChanged: {\n            type: Function,\n            optional: true,\n        },\n        slots: {\n            type: Object,\n            optional: true,\n        },\n    };\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.breadcrumbs = useState(this.env.config.breadcrumbs);\n    }\n\n    get breadcrumbTitle() {\n        if (this.breadcrumbs.length > 1) {\n            return this.breadcrumbs.at(-2).name;\n        }\n        return \"\";\n    }\n\n    onBreadcrumbClicked() {\n        if (this.breadcrumbs.length > 1) {\n            this.actionService.restore(this.breadcrumbs.at(-2).id);\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { registries } from \"@spreadsheet/o_spreadsheet/o_spreadsheet\";\nimport { SpreadsheetShareButton } from \"@spreadsheet/components/share_button/share_button\";\n\nconst { topbarComponentRegistry } = registries;\n\nexport class TopbarShareButton extends Component {\n    static template = \"spreadsheet_edition.TopbarShareButton\";\n    static components = {\n        SpreadsheetShareButton,\n    };\n    static props = {};\n\n    get buttonProps() {\n        return {\n            onSpreadsheetShared: this.env.onSpreadsheetShared.bind(this),\n            model: this.env.model,\n        };\n    }\n}\n\ntopbarComponentRegistry.add(\"share_button\", {\n    component: TopbarShareButton,\n    isVisible: (env) => env.onSpreadsheetShared,\n    sequence: 20,\n});\n", "/** @ts-check */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FilterFieldOffset } from \"../filter_field_offset\";\nimport { RELATIVE_DATE_RANGE_TYPES } from \"@spreadsheet/helpers/constants\";\nimport { AbstractFilterEditorSidePanel } from \"./filter_editor_side_panel\";\nimport { FilterEditorFieldMatching } from \"./filter_editor_field_matching\";\nimport { useState } from \"@odoo/owl\";\n\nconst RANGE_TYPES = [\n    { type: \"fixedPeriod\", description: _t(\"Month / Quarter\") },\n    { type: \"relative\", description: _t(\"Relative Period\") },\n    { type: \"from_to\", description: _t(\"From / To\") },\n];\n\n/**\n * @typedef {import(\"@spreadsheet\").GlobalFilter} GlobalFilter\n * @typedef {import(\"@spreadsheet\").OdooField} OdooField\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n * @typedef {import(\"@spreadsheet\").FixedPeriods} FixedPeriods\n *\n *\n * @typedef DateState\n * @property {Object} defaultValue\n * @property {\"fixedPeriod\" | \"relative\" | \"from_to\"} type type of the filter\n * @property {FixedPeriods[]} disabledPeriods\n */\n\nclass DateFilterEditorFieldMatching extends FilterEditorFieldMatching {\n    static components = {\n        ...FilterEditorFieldMatching.components,\n        FilterFieldOffset,\n    };\n    static template = \"spreadsheet_edition.DateFilterEditorFieldMatching\";\n    static props = {\n        ...FilterEditorFieldMatching.props,\n        onOffsetSelected: Function,\n    };\n}\n\n/**\n * This is the side panel to define/edit a global filter of type \"date\".\n */\nexport class DateFilterEditorSidePanel extends AbstractFilterEditorSidePanel {\n    static template = \"spreadsheet_edition.DateFilterEditorSidePanel\";\n    static components = {\n        ...AbstractFilterEditorSidePanel.components,\n        DateFilterEditorFieldMatching,\n    };\n\n    /**\n     * @constructor\n     */\n    setup() {\n        super.setup();\n\n        this.type = \"date\";\n        /** @type {DateState} */\n        this.dateState = useState({\n            defaultValue: undefined,\n            type: \"fixedPeriod\",\n            disabledPeriods: [],\n        });\n\n        this.relativeDateRangesTypes = RELATIVE_DATE_RANGE_TYPES;\n        this.dateRangeTypes = RANGE_TYPES;\n\n        this.ALLOWED_FIELD_TYPES = [\"datetime\", \"date\"];\n    }\n\n    /**\n     * @override\n     */\n    get filterValues() {\n        const values = super.filterValues;\n        return {\n            ...values,\n            defaultValue: this.dateState.defaultValue,\n            rangeType: this.dateState.type,\n            disabledPeriods: this.dateState.disabledPeriods,\n        };\n    }\n\n    shouldDisplayFieldMatching() {\n        return this.fieldMatchings.length;\n    }\n\n    isDateTypeSelected(dateType) {\n        return dateType === this.dateState.type;\n    }\n\n    /**\n     * @override\n     * @param {GlobalFilter} globalFilter\n     */\n    loadSpecificFilterValues(globalFilter) {\n        if(globalFilter.type !== \"date\"){\n            return;\n        }\n        this.dateState.type = globalFilter.rangeType;\n        this.dateState.defaultValue = globalFilter.defaultValue;\n        if (globalFilter.rangeType === \"fixedPeriod\") {\n            this.dateState.disabledPeriods = globalFilter.disabledPeriods || [];\n        }\n    }\n\n    /**\n     * @override\n     * @param {number} index\n     * @param {string|undefined} chain\n     * @param {OdooField|undefined} field\n     */\n    onSelectedField(index, chain, field) {\n        super.onSelectedField(index, chain, field);\n        this.fieldMatchings[index].fieldMatch.offset = 0;\n    }\n\n    /**\n     * @param {number} index\n     * @param {number} offset\n     */\n    onOffsetSelected(index, offset) {\n        this.fieldMatchings[index].fieldMatch.offset = offset;\n    }\n\n    onTimeRangeChanged(defaultValue) {\n        this.dateState.defaultValue = defaultValue;\n    }\n\n    onDateOptionChange(ev) {\n        this.dateState.type = ev.target.value;\n        this.dateState.defaultValue = undefined;\n    }\n\n    toggleDateDefaultValue(checked) {\n        const defaultValue = this.dateState.disabledPeriods.includes(\"month\")\n            ? \"this_year\"\n            : \"this_month\";\n        this.dateState.defaultValue = checked ? defaultValue : undefined;\n    }\n\n    toggleAllowedPeriod(period) {\n        const disabledPeriods = this.dateState.disabledPeriods;\n        if (disabledPeriods.includes(period)) {\n            this.dateState.disabledPeriods = disabledPeriods.filter((p) => p !== period);\n        } else {\n            this.dateState.disabledPeriods = [...disabledPeriods, period];\n        }\n\n        if (\n            this.dateState.defaultValue === \"this_month\" &&\n            this.dateState.disabledPeriods.includes(\"month\")\n        ) {\n            this.dateState.defaultValue = \"this_year\";\n        } else if (\n            this.dateState.defaultValue === \"this_quarter\" &&\n            this.dateState.disabledPeriods.includes(\"quarter\")\n        ) {\n            this.dateState.defaultValue = \"this_year\";\n        }\n    }\n\n    get allowedAutomaticValues() {\n        const values = [{ value: \"this_year\", label: _t(\"Year\") }];\n        if (!this.dateState.disabledPeriods.includes(\"month\")) {\n            values.push({ value: \"this_month\", label: _t(\"Month\") });\n        }\n        if (!this.dateState.disabledPeriods.includes(\"quarter\")) {\n            values.push({ value: \"this_quarter\", label: _t(\"Quarter\") });\n        }\n        return values;\n    }\n}\n", "/** @odoo-module */\n\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n */\n\nexport class FilterEditorFieldMatching extends Component {\n    static template = \"spreadsheet_edition.FilterEditorFieldMatching\";\n    static components = {\n        ModelFieldSelector,\n    };\n\n    static props = {\n        // See AbstractFilterEditorSidePanel fieldMatchings\n        fieldMatchings: Array,\n        wrongFieldMatchings: Array,\n        selectField: Function,\n        filterModelFieldSelectorField: Function,\n    };\n\n    /**\n     *\n     * @param {FieldMatching} fieldMatch\n     * @returns {string}\n     */\n    getModelField(fieldMatch) {\n        if (!fieldMatch || !fieldMatch.chain) {\n            return \"\";\n        }\n        return fieldMatch.chain;\n    }\n}\n", "/** @odoo-module */\n\nimport { Component, onMounted, useRef } from \"@odoo/owl\";\n\nexport class FilterEditorLabel extends Component {\n    static template = \"spreadsheet_edition.FilterEditorLabel\";\n    static props = {\n        label: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n        inputClass: { type: String, optional: true },\n        setLabel: Function,\n    };\n\n    setup() {\n        this.labelInput = useRef(\"labelInput\");\n        onMounted(this.onMounted);\n    }\n\n    onMounted() {\n        this.labelInput.el.focus();\n    }\n}\n", "/** @ts-check */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { CommandResult } from \"@spreadsheet/o_spreadsheet/cancelled_reason\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { globalFiltersFieldMatchers } from \"@spreadsheet/global_filters/plugins/global_filters_core_plugin\";\nimport { FilterEditorLabel } from \"./filter_editor_label\";\n\nimport { onWillStart, Component, useRef, useState, toRaw } from \"@odoo/owl\";\n\nconst { Checkbox, Section, SidePanelCollapsible } = spreadsheet.components;\nconst { toNumber } = spreadsheet.helpers;\nconst uuidGenerator = new spreadsheet.helpers.UuidGenerator();\n\n/**\n * @typedef {import(\"@spreadsheet\").OdooField} OdooField\n * @typedef {import(\"@spreadsheet\").FieldMatching} FieldMatching\n * @typedef {import(\"@spreadsheet\").GlobalFilter} GlobalFilter\n *\n * @typedef State\n * @property {boolean} saved\n * @property {string} label label of the filter\n */\n\n/**\n * This is the side panel to define/edit a global filter.\n * It can be of 3 different type: text, date and relation.\n */\nexport class AbstractFilterEditorSidePanel extends Component {\n    static template = \"\";\n    static components = {\n        FilterEditorLabel,\n        SidePanelCollapsible,\n        Checkbox,\n        Section,\n    };\n    static props = {\n        id: { type: String, optional: true },\n        onCloseSidePanel: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.id = undefined;\n        this.type = \"\";\n        /** @type {State} */\n        this.genericState = useState({\n            saved: false,\n            label: undefined,\n        });\n        this.fieldMatchings = useState([]);\n        this._wrongFieldMatchingsSet = useState(new Set());\n        this.getters = this.env.model.getters;\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.labelInput = useRef(\"labelInput\");\n\n        /** @type {string[]} */\n        this.ALLOWED_FIELD_TYPES = [];\n\n        onWillStart(this.onWillStart);\n    }\n\n    /**\n     * Retrieve the placeholder of the label\n     */\n    get placeholder() {\n        return _t(\"New %s filter\", this.type);\n    }\n\n    get missingLabel() {\n        return this.genericState.saved && !this.genericState.label;\n    }\n\n    get wrongFieldMatchings() {\n        return this.genericState.saved ? [...this._wrongFieldMatchingsSet] : [];\n    }\n\n    get missingRequired() {\n        return !!this.missingLabel || this.wrongFieldMatchings.length !== 0;\n    }\n\n    get filterValues() {\n        const id = this.id || uuidGenerator.uuidv4();\n        return {\n            id,\n            type: this.type,\n            label: this.genericState.label,\n        };\n    }\n\n    get isNewFilter() {\n        return this.props.id === undefined;\n    }\n\n    /**\n     * @param {Event & { target: HTMLInputElement }} ev\n     */\n    setLabel(ev) {\n        this.genericState.label = ev.target.value;\n    }\n\n    shouldDisplayFieldMatching() {\n        throw new Error(\"Not implemented by children\");\n    }\n\n    loadValues() {\n        this.id = this.props.id;\n        const globalFilter = this.id && this.getters.getGlobalFilter(this.id);\n        if (globalFilter) {\n            this.genericState.label = _t(globalFilter.label);\n            this.loadSpecificFilterValues(globalFilter);\n        }\n    }\n\n    /**\n     * @param {GlobalFilter} globalFilter\n     */\n    loadSpecificFilterValues(globalFilter) {\n        return;\n    }\n\n    /**\n     * @private\n     */\n    async _loadFieldMatchings() {\n        for (const [type, el] of Object.entries(globalFiltersFieldMatchers)) {\n            for (const objectId of el.getIds()) {\n                const tag = await el.getTag(objectId);\n                this.fieldMatchings.push({\n                    name: el.getDisplayName(objectId),\n                    tag,\n                    fieldMatch: el.getFieldMatching(objectId, this.id) || {},\n                    fields: () => el.getFields(objectId),\n                    model: () => el.getModel(objectId),\n                    payload: () => ({ id: objectId, type }),\n                });\n            }\n        }\n    }\n\n    async onWillStart() {\n        this.loadValues();\n        const proms = [];\n        proms.push(\n            ...Object.values(globalFiltersFieldMatchers)\n                .map((el) => el.waitForReady())\n                .flat()\n        );\n        await this._loadFieldMatchings();\n        await Promise.all(proms);\n    }\n\n    /**\n     * @param {OdooField} field\n     * @returns {boolean}\n     */\n    isFieldValid(field) {\n        return !!field.searchable;\n    }\n\n    /**\n     * Function that will be called by ModelFieldSelector on each fields, to\n     * filter the ones that should be displayed\n     * @param {OdooField} field\n     * @returns {boolean}\n     */\n    filterModelFieldSelectorField(field) {\n        if (!field.searchable) {\n            return false;\n        }\n        if (field.name === \"id\") {\n            return true;\n        }\n        return this.ALLOWED_FIELD_TYPES.includes(field.type) || !!field.relation;\n    }\n\n    /**\n     *\n     * @param {Object} field\n     * @returns {boolean}\n     */\n    matchingRelation(field) {\n        return !field.relation;\n    }\n\n    /**\n     * @param {number} index\n     * @param {string|undefined} chain\n     * @param {OdooField|undefined} field\n     */\n    onSelectedField(index, chain, field) {\n        //ensure index type to use it in a set\n        index = toNumber(index);\n        if (!chain) {\n            this._wrongFieldMatchingsSet.delete(index);\n            this.fieldMatchings[index].fieldMatch = {};\n            return;\n        }\n        if (!field) {\n            this._wrongFieldMatchingsSet.add(index);\n        }\n        const fieldName = chain;\n        this.fieldMatchings[index].fieldMatch = {\n            chain: fieldName,\n            type: field?.type || \"\",\n        };\n        if (!field || (field.name !== \"id\" && !this.matchingRelation(field)) || !field.searchable) {\n            this._wrongFieldMatchingsSet.add(index);\n        } else {\n            this._wrongFieldMatchingsSet.delete(index);\n        }\n    }\n\n    onSave() {\n        this.genericState.saved = true;\n        if (this.missingRequired) {\n            this.notification.add(_t(\"Some required fields are not valid\"), {\n                type: \"danger\",\n                sticky: false,\n            });\n            return;\n        }\n        const cmd = this.id ? \"EDIT_GLOBAL_FILTER\" : \"ADD_GLOBAL_FILTER\";\n        const filter = this.filterValues;\n        // Populate the command a bit more with a key chart, pivot or list\n        const additionalPayload = {};\n        this.fieldMatchings.forEach((fm) => {\n            const { type, id } = fm.payload();\n            additionalPayload[type] = additionalPayload[type] || {};\n            //remove reactivity\n            additionalPayload[type][id] = toRaw(fm.fieldMatch);\n        });\n        const result = this.env.model.dispatch(cmd, {\n            filter,\n            ...additionalPayload,\n        });\n        if (result.isCancelledBecause(CommandResult.DuplicatedFilterLabel)) {\n            this.notification.add(_t(\"Duplicated Label\"), {\n                type: \"danger\",\n                sticky: false,\n            });\n            return;\n        }\n        this.env.openSidePanel(\"GLOBAL_FILTERS_SIDE_PANEL\", {});\n    }\n\n    onCancel() {\n        this.env.openSidePanel(\"GLOBAL_FILTERS_SIDE_PANEL\", {});\n    }\n\n    onDelete() {\n        if (this.id) {\n            this.env.model.dispatch(\"REMOVE_GLOBAL_FILTER\", { id: this.id });\n        }\n        this.env.openSidePanel(\"GLOBAL_FILTERS_SIDE_PANEL\", {});\n    }\n}\n", "/** @ts-check */\n\nimport { ModelSelector } from \"@web/core/model_selector/model_selector\";\nimport { AbstractFilterEditorSidePanel } from \"./filter_editor_side_panel\";\nimport { FilterEditorFieldMatching } from \"./filter_editor_field_matching\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\nimport { Domain } from \"@web/core/domain\";\nimport { user } from \"@web/core/user\";\n\nimport { useState } from \"@odoo/owl\";\nimport { SidePanelDomain } from \"../../../components/side_panel_domain/side_panel_domain\";\n\n/**\n * @typedef {import(\"@spreadsheet\").OdooField} OdooField\n * @typedef {import(\"@spreadsheet\").GlobalFilter} GlobalFilter\n\n *\n * @typedef RelationState\n * @property {GlobalFilter[\"defaultValue\"]} defaultValue\n * @property {Array} displayNames\n * @property {{label?: string, technical?: string}} relatedModel\n * @property {boolean} [includeChildren]\n */\n\n/**\n * This is the side panel to define/edit a global filter of type \"relation\".\n */\nexport class RelationFilterEditorSidePanel extends AbstractFilterEditorSidePanel {\n    static template = \"spreadsheet_edition.RelationFilterEditorSidePanel\";\n    static components = {\n        ...AbstractFilterEditorSidePanel.components,\n        ModelSelector,\n        MultiRecordSelector,\n        FilterEditorFieldMatching,\n        SidePanelDomain,\n    };\n    setup() {\n        super.setup();\n\n        this.type = \"relation\";\n        /** @type {RelationState} */\n        this.relationState = useState({\n            defaultValue: [],\n            displayNames: [],\n            includeChildren: undefined,\n            relatedModel: {\n                label: undefined,\n                technical: undefined,\n            },\n        });\n        this.nameService = useService(\"name\");\n\n        this.ALLOWED_FIELD_TYPES = [\"many2one\", \"many2many\", \"one2many\"];\n    }\n\n    get missingModel() {\n        return this.genericState.saved && !this.relationState.relatedModel.technical;\n    }\n\n    get missingRequired() {\n        return super.missingRequired || this.missingModel;\n    }\n\n    /**\n     * @override\n     */\n    get filterValues() {\n        const values = super.filterValues;\n        return {\n            ...values,\n            defaultValue: this.relationState.defaultValue,\n            defaultValueDisplayNames: this.relationState.displayNames,\n            modelName: this.relationState.relatedModel.technical,\n            includeChildren: this.relationState.includeChildren,\n            domainOfAllowedValues: this.relationState.domainOfAllowedValues,\n        };\n    }\n\n    shouldDisplayFieldMatching() {\n        return this.fieldMatchings.length && this.relationState.relatedModel.technical;\n    }\n\n    /**\n     * List of model names of all related models of all pivots\n     * @returns {Array<string>}\n     */\n    get relatedModels() {\n        const all = this.fieldMatchings.map((object) => Object.values(object.fields()));\n        // Add the model to allow to filter on id.\n        const set = new Set(\n            all\n                .flat()\n                .filter((field) => field.relation)\n                .map((field) => field.relation)\n        );\n        this.fieldMatchings.forEach((object) => {\n            set.add(object.model());\n        });\n        return [...set];\n    }\n\n    /**\n     * @override\n     * @param {GlobalFilter} globalFilter\n     */\n    loadSpecificFilterValues(globalFilter) {\n        this.relationState.defaultValue = globalFilter.defaultValue;\n        this.relationState.relatedModel.technical = globalFilter.modelName;\n        this.relationState.includeChildren = globalFilter.includeChildren;\n        this.relationState.restrictValuesToDomain = !!globalFilter.domainOfAllowedValues?.length;\n        this.relationState.domainOfAllowedValues = globalFilter.domainOfAllowedValues;\n    }\n\n    async onWillStart() {\n        await super.onWillStart();\n        const promises = [this.fetchModelFromName()];\n        if (this.relationState.includeChildren) {\n            this.relationState.relatedModel.hasParentRelation = true;\n        } else {\n            promises.push(this.fetchModelRelation());\n        }\n        await Promise.all(promises);\n    }\n\n    /**\n     * Get the first field which could be a relation of the current related\n     * model\n     *\n     * @param {string} model\n     * @param {Object.<string, OdooField>} fields Fields to look in\n     * @returns {field|undefined}\n     */\n    _findRelation(model, fields) {\n        if (this.relationState.relatedModel.technical === model) {\n            return Object.values(fields).find((field) => field.name === \"id\");\n        }\n        const field = Object.values(fields).find(\n            (field) =>\n                field.searchable && field.relation === this.relationState.relatedModel.technical\n        );\n        return field;\n    }\n\n    async onModelSelected({ technical, label }) {\n        if (!this.genericState.label) {\n            this.genericState.label = label;\n        }\n        if (this.relationState.relatedModel.technical !== technical) {\n            this.relationState.defaultValue = [];\n        }\n        this.relationState.relatedModel.technical = technical;\n        this.relationState.relatedModel.label = label;\n        this.relationState.domainOfAllowedValues = [];\n\n        this.fieldMatchings.forEach((object, index) => {\n            const field = this._findRelation(object.model(), object.fields());\n            this.onSelectedField(index, field ? field.name : undefined, field);\n        });\n        await this.fetchModelRelation();\n        this.relationState.includeChildren = this.relationState.relatedModel.hasParentRelation;\n    }\n\n    async fetchModelFromName() {\n        if (!this.relationState.relatedModel.technical) {\n            return;\n        }\n        const result = await this.orm.call(\"ir.model\", \"display_name_for\", [\n            [this.relationState.relatedModel.technical],\n        ]);\n        this.relationState.relatedModel.label = result[0] && result[0].display_name;\n        if (!this.genericState.label) {\n            this.genericState.label = this.relationState.relatedModel.label;\n        }\n    }\n\n    async fetchModelRelation() {\n        const technicalName = this.relationState.relatedModel.technical;\n        const hasParentRelation = await this.orm.call(\n            \"ir.model\",\n            \"has_searchable_parent_relation\",\n            [technicalName]\n        );\n        this.relationState.relatedModel.hasParentRelation = hasParentRelation;\n    }\n\n    /**\n     * @param {OdooField} field\n     * @returns {boolean}\n     */\n    isFieldValid(field) {\n        const relatedModel = this.relationState.relatedModel.technical;\n        return super.isFieldValid(field) && (!relatedModel || field.relation === relatedModel);\n    }\n\n    /**\n     * @override\n     * @param {OdooField} field\n     * @returns {boolean}\n     */\n    matchingRelation(field) {\n        return field.relation === this.relationState.relatedModel.technical;\n    }\n\n    /**\n     * @param {Number[]} value\n     */\n    async onValuesSelected(resIds) {\n        const displayNames = await this.nameService.loadDisplayNames(\n            this.relationState.relatedModel.technical,\n            resIds\n        );\n        this.relationState.defaultValue = resIds;\n        this.relationState.displayNames = Object.values(displayNames);\n    }\n\n    toggleDefaultsToCurrentUser(ev) {\n        this.relationState.defaultValue = ev.target.checked ? \"current_user\" : undefined;\n    }\n\n    toggleDomainRestriction(isChecked) {\n        this.relationState.restrictValuesToDomain = isChecked;\n        this.relationState.domainOfAllowedValues = [];\n    }\n\n    onDomainUpdate(domain) {\n        this.relationState.domainOfAllowedValues = domain;\n    }\n\n    getEvaluatedDomain() {\n        const domain = this.relationState.domainOfAllowedValues;\n        if (domain) {\n            return new Domain(domain).toList(user.context);\n        }\n        return [];\n    }\n}\n", "/** @ts-check */\n\nimport { AbstractFilterEditorSidePanel } from \"./filter_editor_side_panel\";\nimport { FilterEditorFieldMatching } from \"./filter_editor_field_matching\";\nimport { TextFilterValue } from \"@spreadsheet/global_filters/components/filter_text_value/filter_text_value\";\n\nimport { components } from \"@odoo/o-spreadsheet\";\nimport { useState } from \"@odoo/owl\";\n\nconst { SelectionInput } = components;\n\n/**\n * @typedef {import(\"@spreadsheet\").GlobalFilter} GlobalFilter\n *\n * @typedef TextState\n * @property {string} defaultValue\n\n */\n\n/**\n * This is the side panel to define/edit a global filter of type \"text\".\n */\nexport class TextFilterEditorSidePanel extends AbstractFilterEditorSidePanel {\n    static template = \"spreadsheet_edition.TextFilterEditorSidePanel\";\n    static components = {\n        ...AbstractFilterEditorSidePanel.components,\n        FilterEditorFieldMatching,\n        TextFilterValue,\n        SelectionInput,\n    };\n\n    setup() {\n        super.setup();\n\n        this.type = \"text\";\n        /** @type {TextState} */\n        this.textState = useState({\n            defaultValue: \"\",\n            restrictValuesToRange: false,\n            rangeOfAllowedValues: undefined,\n        });\n        this.ALLOWED_FIELD_TYPES = [\"many2one\", \"text\", \"char\"];\n    }\n\n    get rangesForSelectionInput() {\n        // SelectionInput expects an array of ranges\n        if (!this.textState.rangeOfAllowedValues) {\n            return [];\n        }\n        return [this.textState.rangeOfAllowedValues];\n    }\n\n    get textOptionsFromRange() {\n        if (!this.textState.restrictValuesToRange) {\n            return [];\n        }\n        const range = this.env.model.getters.getRangeFromSheetXC(\n            this.env.model.getters.getActiveSheetId(),\n            this.textState.rangeOfAllowedValues\n        );\n        return this.env.model.getters.getTextFilterOptionsFromRange(range, [\n            this.textState.defaultValue,\n        ]);\n    }\n\n    /**\n     * @override\n     */\n    shouldDisplayFieldMatching() {\n        return this.fieldMatchings.length;\n    }\n\n    /**\n     * @override\n     */\n    get filterValues() {\n        const values = super.filterValues;\n        const sheetId = this.env.model.getters.getActiveSheetId();\n        const { restrictValuesToRange, rangeOfAllowedValues, defaultValue } = this.textState;\n        const rangeString = restrictValuesToRange && rangeOfAllowedValues;\n        const range = rangeString\n            ? this.env.model.getters.getRangeDataFromXc(sheetId, rangeString)\n            : undefined;\n        return {\n            ...values,\n            defaultValue: defaultValue,\n            rangeOfAllowedValues: range,\n        };\n    }\n\n    /**\n     * @override\n     * @param {GlobalFilter} globalFilter\n     */\n    loadSpecificFilterValues(globalFilter) {\n        const { rangeOfAllowedValues, defaultValue } = globalFilter;\n        this.textState.defaultValue = defaultValue;\n        this.textState.restrictValuesToRange = !!rangeOfAllowedValues;\n        if (rangeOfAllowedValues) {\n            const rangeString = this.env.model.getters.getRangeString(\n                rangeOfAllowedValues,\n                this.env.model.getters.getActiveSheetId()\n            );\n            this.textState.rangeOfAllowedValues = rangeString;\n        }\n    }\n\n    onRangeChanged(ranges) {\n        this.textState.rangeOfAllowedValues = ranges[0];\n    }\n\n    onRangeConfirmed() {}\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst FIELD_OFFSETS = [\n    { value: 0, description: \"\" },\n    { value: -1, description: _t(\"Previous\") },\n    { value: -2, description: _t(\"Before previous\") },\n    { value: 1, description: _t(\"Next\") },\n    { value: 2, description: _t(\"After next\") },\n];\n\nexport class FilterFieldOffset extends Component {\n    static template = \"spreadsheet_edition.FilterFieldOffset\";\n    static props = {\n        onOffsetSelected: Function,\n        selectedOffset: Number,\n        active: Boolean,\n    };\n\n    setup() {\n        this.fieldsOffsets = FIELD_OFFSETS;\n    }\n\n    /**\n     * @param {Event & { target: HTMLSelectElement }} ev\n     */\n    onOffsetSelected(ev) {\n        this.props.onOffsetSelected(parseInt(ev.target.value));\n    }\n\n    get title() {\n        return this.props.active\n            ? _t(\"Period offset applied to this source\")\n            : _t(\"Requires a selected field\");\n    }\n}\n", "/** @odoo-module */\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nimport { Component } from \"@odoo/owl\";\nconst { Menu } = spreadsheet;\n\nexport class FilterComponent extends Component {\n    static template = \"spreadsheet_edition.FilterComponent\";\n    static components = { Menu };\n    static props = {};\n\n    get activeFilter() {\n        return this.env.model.getters.getActiveFilterCount();\n    }\n\n    toggleDropdown() {\n        this.env.toggleSidePanel(\"GLOBAL_FILTERS_SIDE_PANEL\");\n    }\n}\n", "/** @odoo-module */\n\nimport { registries, tokenColors, helpers } from \"@odoo/o-spreadsheet\";\nconst { insertTokenAfterLeftParenthesis } = helpers;\n\nregistries.autoCompleteProviders.add(\"global_filters\", {\n    sequence: 50,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        const functionContext = tokenAtCursor.functionContext;\n        if (\n            functionContext?.parent.toUpperCase() === \"ODOO.FILTER.VALUE\" &&\n            functionContext.argPosition === 0\n        ) {\n            const labels = this.getters.getGlobalFilters().map((filter) => filter.label);\n            return labels.map((label) => {\n                const escapedLabel = label.replaceAll('\"', '\\\\\"');\n                const quotedLabel = `\"${escapedLabel}\"`;\n                return {\n                    text: quotedLabel,\n                    htmlContent: [{ value: quotedLabel, color: tokenColors.STRING }],\n                };\n            });\n        }\n        return;\n    },\n    selectProposal: insertTokenAfterLeftParenthesis,\n});\n", "/** @odoo-module */\n\nimport { FilterValue } from \"@spreadsheet/global_filters/components/filter_value/filter_value\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, useRef } from \"@odoo/owl\";\nimport { hooks, components } from \"@odoo/o-spreadsheet\";\n\nconst { Section } = components;\n\n/**\n * This is the side panel to define/edit a global filter.\n * It can be of 3 different type: text, date and relation.\n */\nexport class GlobalFiltersSidePanel extends Component {\n    static template = \"spreadsheet_edition.GlobalFiltersSidePanel\";\n    static components = { FilterValue, Section };\n    static props = {\n        onCloseSidePanel: { type: Function, optional: true },\n    };\n\n    dnd = hooks.useDragAndDropListItems();\n    filtersListRef = useRef(\"filtersList\");\n\n    setup() {\n        this.getters = this.env.model.getters;\n    }\n\n    get isReadonly() {\n        return this.env.model.getters.isReadonly();\n    }\n\n    get filters() {\n        return this.env.model.getters.getGlobalFilters();\n    }\n\n    _t(...args) {\n        return _t(...args);\n    }\n\n    hasDataSources() {\n        return (\n            this.env.model.getters.getPivotIds().length +\n            this.env.model.getters.getListIds().length +\n            this.env.model.getters.getOdooChartIds().length\n        );\n    }\n\n    newText() {\n        this.env.openSidePanel(\"TEXT_FILTER_SIDE_PANEL\");\n    }\n\n    newDate() {\n        this.env.openSidePanel(\"DATE_FILTER_SIDE_PANEL\");\n    }\n\n    newRelation() {\n        this.env.openSidePanel(\"RELATION_FILTER_SIDE_PANEL\");\n    }\n\n    /**\n     * @param {string} id\n     */\n    onEdit(id) {\n        const filter = this.env.model.getters.getGlobalFilter(id);\n        if (!filter) {\n            return;\n        }\n        switch (filter.type) {\n            case \"text\":\n                this.env.openSidePanel(\"TEXT_FILTER_SIDE_PANEL\", { id });\n                break;\n            case \"date\":\n                this.env.openSidePanel(\"DATE_FILTER_SIDE_PANEL\", { id });\n                break;\n            case \"relation\":\n                this.env.openSidePanel(\"RELATION_FILTER_SIDE_PANEL\", { id });\n                break;\n        }\n    }\n\n    startDragAndDrop(filter, event) {\n        if (event.button !== 0) {\n            return;\n        }\n\n        const rects = this.getFiltersElementsRects();\n        const filtersItems = this.filters.map((filter, index) => ({\n            id: filter.id,\n            size: rects[index].height,\n            position: rects[index].y,\n        }));\n        this.dnd.start(\"vertical\", {\n            draggedItemId: filter.id,\n            initialMousePosition: event.clientY,\n            items: filtersItems,\n            containerEl: this.filtersListRef.el,\n            onDragEnd: (filterId, finalIndex) => this.onDragEnd(filterId, finalIndex),\n        });\n    }\n\n    getFiltersElementsRects() {\n        return Array.from(this.filtersListRef.el.children).map((filterEl) =>\n            filterEl.getBoundingClientRect()\n        );\n    }\n\n    getFilterItemStyle(filter) {\n        return this.dnd.itemsStyle[filter.id] || \"\";\n    }\n\n    onDragEnd(filterId, finalIndex) {\n        const originalIndex = this.filters.findIndex((filter) => filter.id === filterId);\n        const delta = finalIndex - originalIndex;\n        if (filterId && delta !== 0) {\n            this.env.model.dispatch(\"MOVE_GLOBAL_FILTER\", {\n                id: filterId,\n                delta,\n            });\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nimport {\n    SET_FILTER_MATCHING,\n    SET_FILTER_MATCHING_CONDITION,\n} from \"@spreadsheet/pivot/pivot_actions\";\n\nimport { GlobalFiltersSidePanel } from \"./global_filters_side_panel\";\nimport { FilterComponent } from \"./filter_component\";\n\nimport \"./operational_transform\";\nimport { DateFilterEditorSidePanel } from \"./components/filter_editor/date_filter_editor_side_panel\";\nimport { TextFilterEditorSidePanel } from \"./components/filter_editor/text_filter_editor_side_panel\";\nimport { RelationFilterEditorSidePanel } from \"./components/filter_editor/relation_filter_editor_side_panel\";\n\nconst { sidePanelRegistry, topbarComponentRegistry, cellMenuRegistry } = spreadsheet.registries;\n\nsidePanelRegistry.add(\"DATE_FILTER_SIDE_PANEL\", {\n    title: _t(\"Filter properties\"),\n    Body: DateFilterEditorSidePanel,\n});\n\nsidePanelRegistry.add(\"TEXT_FILTER_SIDE_PANEL\", {\n    title: _t(\"Filter properties\"),\n    Body: TextFilterEditorSidePanel,\n});\n\nsidePanelRegistry.add(\"RELATION_FILTER_SIDE_PANEL\", {\n    title: _t(\"Filter properties\"),\n    Body: RelationFilterEditorSidePanel,\n});\n\nsidePanelRegistry.add(\"GLOBAL_FILTERS_SIDE_PANEL\", {\n    title: _t(\"Filters\"),\n    Body: GlobalFiltersSidePanel,\n});\n\ntopbarComponentRegistry.add(\"filter_component\", {\n    component: FilterComponent,\n    isVisible: (env) => {\n        return !env.model.getters.isReadonly() || env.model.getters.getGlobalFilters().length;\n    },\n    sequence: 30,\n});\n\ncellMenuRegistry.add(\"use_global_filter\", {\n    name: _t(\"Set as filter\"),\n    sequence: 175,\n    execute(env) {\n        const position = env.model.getters.getActivePosition();\n        SET_FILTER_MATCHING(position, env);\n    },\n    isVisible: (env) => {\n        const position = env.model.getters.getActivePosition();\n        return SET_FILTER_MATCHING_CONDITION(position, env.model.getters);\n    },\n    icon: \"o-spreadsheet-Icon.SEARCH\",\n});\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nconst { otRegistry } = spreadsheet.registries;\nconst { transformRangeData } = spreadsheet.helpers;\n\notRegistry.addTransformation(\n    \"REMOVE_GLOBAL_FILTER\",\n    [\"EDIT_GLOBAL_FILTER\"],\n    (toTransform, executed) => (toTransform.filter.id === executed.id ? undefined : toTransform)\n);\n\notRegistry.addTransformation(\n    \"REMOVE_COLUMNS_ROWS\",\n    [\"EDIT_GLOBAL_FILTER\", \"ADD_GLOBAL_FILTER\"],\n    transformTextFilterRange\n);\notRegistry.addTransformation(\n    \"ADD_COLUMNS_ROWS\",\n    [\"EDIT_GLOBAL_FILTER\", \"ADD_GLOBAL_FILTER\"],\n    transformTextFilterRange\n);\n\nfunction transformTextFilterRange(toTransform, executed) {\n    const filter = toTransform.filter;\n    if (filter.type === \"text\" && filter.rangeOfAllowedValues) {\n        const transformedRange = transformRangeData(filter.rangeOfAllowedValues, executed);\n        if (transformedRange) {\n            return {\n                ...toTransform,\n                filter: {\n                    ...filter,\n                    rangeOfAllowedValues: transformedRange,\n                },\n            };\n        }\n    }\n    return toTransform;\n}\n", "/** @odoo-module */\n\nimport { deserializeDateTime } from \"@web/core/l10n/dates\";\n\nconst { DateTime } = luxon;\n\nexport function formatToLocaleString(ISOdatetime, code) {\n    return deserializeDateTime(ISOdatetime).setLocale(code).toLocaleString(DateTime.DATETIME_MED);\n}\n\nexport function addToRegistryWithCleanup(cleanUpHook, registry, name, item) {\n    registry.add(name, item);\n    cleanUpHook(() => {\n        registry.remove(name);\n    });\n}\n", "/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SPREADSHEET_DIMENSIONS } from \"@odoo/o-spreadsheet\";\n\n/**\n * @returns {Promise<Array>}\n */\nexport function useSpreadsheetLocales() {\n    const orm = useService(\"orm\");\n    async function loadLocales() {\n        return orm.call(\"res.lang\", \"get_locales_for_spreadsheet\", []);\n    }\n    return loadLocales;\n}\n\n/**\n * @returns {Promise<Array>}\n */\nexport function useSpreadsheetCurrencies() {\n    const orm = useService(\"orm\");\n    async function loadCurrencies() {\n        const odooCurrencies = await orm.searchRead(\n            \"res.currency\", // model\n            [], // domain\n            [\"symbol\", \"full_name\", \"position\", \"name\", \"decimal_places\"], // fields\n            {\n                // opts\n                order: \"active DESC, full_name ASC\",\n                context: { active_test: false },\n            }\n        );\n        return odooCurrencies.map((currency) => {\n            return {\n                code: currency.name,\n                symbol: currency.symbol,\n                position: currency.position || \"after\",\n                name: currency.full_name || _t(\"Currency\"),\n                decimalPlaces: currency.decimal_places || 2,\n            };\n        });\n    }\n    return loadCurrencies;\n}\n\n/**\n * @returns {String}\n */\nexport function useSpreadsheetThumbnail() {\n    return () => {\n        const dimensions = SPREADSHEET_DIMENSIONS;\n        const canvas = document.querySelector(\".o-grid canvas:not(.o-figure-canvas)\");\n        const canvasResizer = document.createElement(\"canvas\");\n        const size = 750;\n        canvasResizer.width = size;\n        canvasResizer.height = size;\n        const canvasCtx = canvasResizer.getContext(\"2d\");\n        // use only 25 first rows in thumbnail\n        const sourceSize = Math.min(\n            25 * dimensions.DEFAULT_CELL_HEIGHT,\n            canvas.width,\n            canvas.height\n        );\n        if (canvas.width !== 0 && canvas.height !== 0) {\n            canvasCtx.drawImage(\n                canvas,\n                dimensions.HEADER_WIDTH - 1,\n                dimensions.HEADER_HEIGHT - 1,\n                sourceSize,\n                sourceSize,\n                0,\n                0,\n                size,\n                size\n            );\n        }\n        return canvasResizer.toDataURL().replace(\"data:image/png;base64,\", \"\");\n    };\n}\n", "/** @odoo-module **/\n\n/**\n * @typedef {import(\"@web/core/orm_service\").ORM} ORM\n */\n\n/**\n *\n * Upload files on the server and link the files to a record as attachment.\n *\n * Implements the `FileStore` interface defined by o-spreadsheet.\n * https://github.com/odoo/o-spreadsheet/blob/300da461b23b5f3db017270192893d4a972bacf0/src/types/files.ts#L4\n *\n */\nexport class RecordFileStore {\n    /**\n     *\n     * @param {string} resModel\n     * @param {number} resId\n     * @param {*} http\n     * @param {ORM} orm\n     */\n    constructor(resModel, resId, http, orm) {\n        this.resModel = resModel;\n        this.resId = resId;\n        this.http = http;\n        this.orm = orm;\n    }\n\n    /**\n     * Upload a file on the server and returns the path to the file.\n     */\n    async upload(file) {\n        const route = \"/web/binary/upload_attachment\";\n        const params = {\n            ufile: [file],\n            csrf_token: odoo.csrf_token,\n            model: this.resModel,\n            id: this.resId,\n        };\n        const fileData = JSON.parse(await this.http.post(route, params, \"text\"))[0];\n        const [accessToken] = await this.orm.call(\"ir.attachment\", \"generate_access_token\", [\n            fileData.id,\n        ]);\n        return `/web/image/${fileData.id}?access_token=${accessToken}`;\n    }\n\n    /**\n     * @param {string} path\n     * @returns {Promise<void>}\n     */\n    async delete(path) {\n        const attachmentId = path.split(\"/\").pop();\n        if (Number.isNaN(attachmentId)) {\n            throw new Error(\"Invalid path: \" + path);\n        }\n        await this.orm.unlink(\"ir.attachment\", [parseInt(attachmentId)]);\n    }\n}\n", "/** @odoo-module */\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { computeAppsAndMenuItems } from \"@web/webclient/menus/menu_helpers\";\n\nimport { Component, useState, useExternalListener, useRef, onMounted } from \"@odoo/owl\";\n\nexport class IrMenuSelector extends Component {\n    static components = { Many2XAutocomplete };\n    static template = \"spreadsheet_edition.IrMenuSelector\";\n    static props = {\n        menuId: { type: Number, optional: true },\n        onValueChanged: Function,\n        autoFocus: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.ref = useRef(\"menuSelectorRef\");\n        this.menus = useService(\"menu\");\n        onMounted(() => {\n            if (this.props.autoFocus) {\n                this.ref.el.querySelector(\"input\")?.focus();\n            }\n        });\n    }\n\n    get many2XAutocompleteProps() {\n        return {\n            resModel: \"ir.ui.menu\",\n            fieldString: _t(\"Menu Items\"),\n            getDomain: this.getDomain.bind(this),\n            update: this.updateMenu.bind(this),\n            activeActions: {},\n            placeholder: _t(\"Select a menu...\"),\n            value: this._getMenuPath(this.props.menuId),\n        };\n    }\n\n    updateMenu(selectedMenus) {\n        this.props.onValueChanged(selectedMenus[0]?.id);\n    }\n\n    getDomain() {\n        return [\n            \"|\",\n            [\"id\", \"in\", this.availableAppMenuIds],\n            \"&\",\n            [\"action\", \"!=\", false],\n            [\"id\", \"in\", this.availableMenuIds],\n        ];\n    }\n\n    get availableMenuIds() {\n        return this.menus\n            .getAll()\n            .map((menu) => menu.id)\n            .filter((menuId) => menuId !== \"root\");\n    }\n\n    get availableAppMenuIds() {\n        return this.menus\n            .getAll()\n            .filter((menu) => menu.id === menu.appID)\n            .map((menu) => menu.id)\n            .filter((menuId) => menuId !== \"root\");\n    }\n\n    /**\n     * Get the path of the given menu as a string of the form \"App/Menu/Submenu\".\n     * @private\n     */\n    _getMenuPath(menuId) {\n        if (menuId === undefined) {\n            return \"\";\n        }\n        const menuTree = this.menus.getMenuAsTree(\"root\");\n        const computedTree = computeAppsAndMenuItems(menuTree);\n        const app = computedTree.apps.find((app) => app.id === menuId);\n        if (app) {\n            return app.label;\n        }\n        const menu = computedTree.menuItems.find((menu) => menu.id === menuId);\n        if (!menu) {\n            return \"\";\n        }\n        const path = menu.parents.replace(/ \\/ /g, \"/\");\n        return path + \"/\" + menu.label;\n    }\n}\n\nexport class IrMenuSelectorDialog extends Component {\n    static components = { Dialog, IrMenuSelector };\n    static template = \"spreadsheet_edition.IrMenuSelectorDialog\";\n    static props = {\n        onMenuSelected: Function,\n        close: Function, // prop added by Dialog service\n    };\n\n    setup() {\n        this.selectedMenu = useState({\n            id: undefined,\n        });\n        // Clicking anywhere will close the link editor menu. It should be\n        // prevented otherwise the chain of event would be broken.\n        // A solution would be to listen all clicks coming from this dialog and stop\n        // their propagation.\n        // However, the autocomplete dropdown of the Many2OneField widget is *not*\n        // a child of this component. It's actually a direct child of \"body\" \u00af\\_(\u30c4)_/\u00af\n        // The following external listener handles this.\n        useExternalListener(document.body, \"click\", (ev) => {\n            ev.stopPropagation();\n            ev.preventDefault(); // stop jumping to odoo home page\n        });\n    }\n    _onConfirm() {\n        this.props.onMenuSelected(this.selectedMenu.id);\n    }\n    _onValueChanged(value) {\n        this.selectedMenu.id = value;\n    }\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { initCallbackRegistry } from \"@spreadsheet/o_spreadsheet/init_callbacks\";\nimport {\n    buildIrMenuIdLink,\n    buildViewLink,\n    buildIrMenuXmlLink,\n} from \"@spreadsheet/ir_ui_menu/odoo_menu_link_cell\";\nimport { IrMenuSelectorDialog } from \"@spreadsheet_edition/bundle/ir_menu_selector/ir_menu_selector\";\n\nconst { markdownLink } = spreadsheet.links;\nconst { linkMenuRegistry } = spreadsheet.registries;\n\n/**\n * Helper to get the function to be called when the spreadsheet is opened\n * in order to insert the link.\n * @param {import(\"@spreadsheet/ir_ui_menu/odoo_menu_link_cell\").ViewLinkDescription} actionToLink\n * @returns Function to call\n */\nfunction insertLink(actionToLink) {\n    return (model) => {\n        if (!this.isEmptySpreadsheet) {\n            const sheetId = model.uuidGenerator.uuidv4();\n            const sheetIdFrom = model.getters.getActiveSheetId();\n            model.dispatch(\"CREATE_SHEET\", {\n                sheetId,\n                position: model.getters.getSheetIds().length,\n            });\n            model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom, sheetIdTo: sheetId });\n        }\n        const viewLink = buildViewLink(actionToLink);\n        model.dispatch(\"UPDATE_CELL\", {\n            sheetId: model.getters.getActiveSheetId(),\n            content: markdownLink(actionToLink.name, viewLink),\n            col: 0,\n            row: 0,\n        });\n    };\n}\n\ninitCallbackRegistry.add(\"insertLink\", insertLink);\n\nlinkMenuRegistry.add(\"odooMenu\", {\n    name: _t(\"Link an Odoo menu\"),\n    sequence: 20,\n    execute: async (env) => {\n        return new Promise((resolve) => {\n            const closeDialog = env.services.dialog.add(IrMenuSelectorDialog, {\n                onMenuSelected: (menuId) => {\n                    closeDialog();\n                    const menu = env.services.menu.getMenu(menuId);\n                    const xmlId = menu && menu.xmlid;\n                    const url = xmlId ? buildIrMenuXmlLink(xmlId) : buildIrMenuIdLink(menuId);\n                    const label = menu.name;\n                    resolve(markdownLink(label, url));\n                },\n            });\n        });\n    },\n});\n", "/** @odoo-module */\n\nimport { getNumberOfListFormulas } from \"@spreadsheet/list/list_helpers\";\nimport { containsReferences } from \"@spreadsheet/helpers/helpers\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nconst { autofillModifiersRegistry, autofillRulesRegistry } = spreadsheet.registries;\n\n//--------------------------------------------------------------------------\n// Autofill Rules\n//--------------------------------------------------------------------------\n\nautofillRulesRegistry.add(\"autofill_list\", {\n    condition: (cell) =>\n        cell &&\n        cell.isFormula &&\n        getNumberOfListFormulas(cell.compiledFormula.tokens) === 1 &&\n        !containsReferences(cell),\n    generateRule: (cell, cells) => {\n        const increment = cells.filter(\n            (cell) =>\n                cell && cell.isFormula && getNumberOfListFormulas(cell.compiledFormula.tokens) === 1\n        ).length;\n        return { type: \"LIST_UPDATER\", increment, current: 0 };\n    },\n    sequence: 3,\n});\n\n//--------------------------------------------------------------------------\n// Autofill Modifier\n//--------------------------------------------------------------------------\n\nautofillModifiersRegistry.add(\"LIST_UPDATER\", {\n    apply: (rule, data, getters, direction) => {\n        rule.current += rule.increment;\n        let isColumn;\n        let steps;\n        switch (direction) {\n            case \"up\":\n                isColumn = false;\n                steps = -rule.current;\n                break;\n            case \"down\":\n                isColumn = false;\n                steps = rule.current;\n                break;\n            case \"left\":\n                isColumn = true;\n                steps = -rule.current;\n                break;\n            case \"right\":\n                isColumn = true;\n                steps = rule.current;\n        }\n        const content = getters.getNextListValue(data.cell.content, isColumn, steps);\n        let tooltip = {\n            props: {\n                content,\n            },\n        };\n        if (content && content !== data.content) {\n            tooltip = {\n                props: {\n                    content: getters.getTooltipListFormula(content, isColumn),\n                },\n            };\n        }\n        return {\n            cellData: {\n                style: undefined,\n                format: data.cell && data.cell.format,\n                border: undefined,\n                content,\n            },\n            tooltip,\n        };\n    },\n});\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { initCallbackRegistry } from \"@spreadsheet/o_spreadsheet/init_callbacks\";\n\nimport \"./autofill\";\nimport \"./operational_transform\";\n\nimport { ListDetailsSidePanel } from \"./side_panels/list_details_side_panel\";\nimport { ListAutofillPlugin } from \"./plugins/list_autofill_plugin\";\n\nimport { insertList } from \"./list_init_callback\";\n\nconst { featurePluginRegistry, sidePanelRegistry, cellMenuRegistry } = spreadsheet.registries;\n\nfeaturePluginRegistry.add(\"odooListAutofillPlugin\", ListAutofillPlugin);\n\nsidePanelRegistry.add(\"LIST_PROPERTIES_PANEL\", {\n    title: () => _t(\"List properties\"),\n    Body: ListDetailsSidePanel,\n    computeState(getters, initialProps) {\n        return {\n            isOpen: getters.isExistingList(initialProps.listId),\n            props: initialProps,\n            key: initialProps.listId,\n        };\n    },\n});\n\ninitCallbackRegistry.add(\"insertList\", insertList);\n\ncellMenuRegistry.add(\"listing_properties\", {\n    separator: true,\n    name: _t(\"See list properties\"),\n    sequence: 190,\n    execute(env) {\n        const position = env.model.getters.getActivePosition();\n        const listId = env.model.getters.getListIdFromPosition(position);\n        env.openSidePanel(\"LIST_PROPERTIES_PANEL\", { listId });\n    },\n    isVisible: (env) => {\n        const position = env.model.getters.getActivePosition();\n        return env.model.getters.isExistingList(env.model.getters.getListIdFromPosition(position));\n    },\n    icon: \"o-spreadsheet-Icon.ODOO_LIST\",\n});\n", "/** @odoo-module */\n\nexport const REINSERT_LIST_CHILDREN = (env) =>\n    env.model.getters.getListIds().map((listId, index) => {\n        return {\n            id: `reinsert_list_${listId}`,\n            name: env.model.getters.getListDisplayName(listId),\n            sequence: index,\n            execute: async (env) => {\n                const zone = env.model.getters.getSelectedZone();\n                const dataSource = await env.model.getters.getAsyncListDataSource(listId);\n                const list = env.model.getters.getListDefinition(listId);\n                const columns = list.columns.map((name) => ({\n                    name,\n                    type: dataSource.getField(name).type,\n                }));\n                env.getLinesNumber((linesNumber) => {\n                    env.model.dispatch(\"RE_INSERT_ODOO_LIST_WITH_TABLE\", {\n                        sheetId: env.model.getters.getActiveSheetId(),\n                        col: zone.left,\n                        row: zone.top,\n                        id: listId,\n                        linesNumber,\n                        columns: columns,\n                    });\n                });\n            },\n        };\n    });\n", "/** @odoo-module */\n\nimport { registries, tokenColors, helpers } from \"@odoo/o-spreadsheet\";\nimport { extractDataSourceId } from \"@spreadsheet/helpers/odoo_functions_helpers\";\n\nconst { insertTokenAfterArgSeparator, insertTokenAfterLeftParenthesis, makeFieldProposal } =\n    helpers;\n\nregistries.autoCompleteProviders.add(\"list_fields\", {\n    sequence: 50,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        if (\n            canAutoCompleteListField(tokenAtCursor) ||\n            canAutoCompleteListHeaderField(tokenAtCursor)\n        ) {\n            const listId = extractDataSourceId(tokenAtCursor);\n            if (!this.getters.isExistingList(listId)) {\n                return;\n            }\n            const dataSource = this.getters.getListDataSource(listId);\n            if (!dataSource.isMetaDataLoaded()) {\n                return;\n            }\n            const fields = Object.values(dataSource.getFields());\n            return fields.map((field) => makeFieldProposal(field));\n        }\n        return;\n    },\n    selectProposal: insertTokenAfterArgSeparator,\n});\n\nfunction canAutoCompleteListField(tokenAtCursor) {\n    const functionContext = tokenAtCursor.functionContext;\n    return (\n        functionContext?.parent.toUpperCase() === \"ODOO.LIST\" && functionContext.argPosition === 2 // the field is the third argument: =ODOO.LIST(1,2,\"email\")\n    );\n}\n\nfunction canAutoCompleteListHeaderField(tokenAtCursor) {\n    const functionContext = tokenAtCursor.functionContext;\n    return (\n        functionContext?.parent.toUpperCase() === \"ODOO.LIST.HEADER\" &&\n        functionContext.argPosition === 1 // the field is the second argument: =ODOO.LIST.HEADER(1,\"email\")\n    );\n}\n\nregistries.autoCompleteProviders.add(\"list_ids\", {\n    sequence: 50,\n    autoSelectFirstProposal: true,\n    getProposals(tokenAtCursor) {\n        const functionContext = tokenAtCursor.functionContext;\n        if (\n            [\"ODOO.LIST\", \"ODOO.LIST.HEADER\"].includes(functionContext?.parent.toUpperCase()) &&\n            functionContext.argPosition === 0\n        ) {\n            const listIds = this.getters.getListIds();\n            return listIds.map((listId) => {\n                const definition = this.getters.getListDefinition(listId);\n                const str = `${listId}`;\n                return {\n                    text: str,\n                    description: definition.name,\n                    htmlContent: [{ value: str, color: tokenColors.NUMBER }],\n                    fuzzySearchKey: str + definition.name,\n                };\n            });\n        }\n    },\n    selectProposal: insertTokenAfterLeftParenthesis,\n});\n", "import { helpers } from \"@odoo/o-spreadsheet\";\n\nconst { positionToZone, mergeContiguousZones } = helpers;\n\nexport function getListHighlights(getters, listId) {\n    const sheetId = getters.getActiveSheetId();\n    const listCellPositions = getVisibleListCellPositions(getters, listId);\n    const mergedZones = mergeContiguousZones(listCellPositions.map(positionToZone));\n    return mergedZones.map((zone) => ({ sheetId, zone, noFill: true }));\n}\n\nfunction getVisibleListCellPositions(getters, listId) {\n    const positions = [];\n    const sheetId = getters.getActiveSheetId();\n    for (const col of getters.getSheetViewVisibleCols()) {\n        for (const row of getters.getSheetViewVisibleRows()) {\n            const position = { sheetId, col, row };\n            const cellListId = getters.getListIdFromPosition(position);\n            if (listId === cellListId) {\n                positions.push(position);\n            }\n        }\n    }\n    return positions;\n}\n", "/** @odoo-module **/\nimport { helpers, stores } from \"@odoo/o-spreadsheet\";\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst { SidePanelStore } = stores;\n\nconst uuidGenerator = new helpers.UuidGenerator();\n\n/**\n * Get the function that have to be executed to insert the given list in the\n * given spreadsheet. The returned function has to be called with the model\n * of the spreadsheet and the dataSource of this list\n *\n * @private\n *\n * @param {import(\"@spreadsheet/list/plugins/list_core_plugin\").SpreadsheetList} list\n * @param {object} param\n * @param {number} param.threshold\n * @param {object} param.fields fields coming from list_model\n * @param {string} param.name Name of the list\n *\n * @returns {function}\n */\nexport function insertList({ list, threshold, fields, name }) {\n    const definition = {\n        metaData: {\n            resModel: list.model,\n            columns: list.columns.map((column) => column.name),\n            fields,\n        },\n        searchParams: {\n            domain: new Domain(list.domain).toJson(),\n            context: list.context,\n            orderBy: list.orderBy,\n        },\n        name,\n        actionXmlId: list.actionXmlId,\n    };\n    return async (model, stores) => {\n        const listId = model.getters.getNextListId();\n        let sheetName = _t(\"%(list_name)s (List #%(list_id)s)\", {\n            list_name: name,\n            list_id: listId,\n        });\n        if (!this.isEmptySpreadsheet) {\n            const sheetId = uuidGenerator.uuidv4();\n            const sheetIdFrom = model.getters.getActiveSheetId();\n            if (model.getters.getSheetIdByName(sheetName)) {\n                sheetName = undefined;\n            }\n            model.dispatch(\"CREATE_SHEET\", {\n                sheetId,\n                position: model.getters.getSheetIds().length,\n                name: sheetName,\n            });\n            model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom, sheetIdTo: sheetId });\n        } else {\n            model.dispatch(\"RENAME_SHEET\", {\n                sheetId: model.getters.getActiveSheetId(),\n                name: sheetName,\n            });\n        }\n        const defWithoutFields = JSON.parse(JSON.stringify(definition));\n        defWithoutFields.metaData.fields = undefined;\n        const sheetId = model.getters.getActiveSheetId();\n        const result = model.dispatch(\"INSERT_ODOO_LIST_WITH_TABLE\", {\n            sheetId,\n            col: 0,\n            row: 0,\n            id: listId,\n            definition: defWithoutFields,\n            linesNumber: threshold,\n            columns: list.columns,\n        });\n        if (!result.isSuccessful) {\n            throw new Error(`Couldn't insert list in spreadsheet. Reasons : ${result.reasons}`);\n        }\n        const dataSource = model.getters.getListDataSource(listId);\n        await dataSource.load();\n        const columns = [];\n        for (let col = 0; col < list.columns.length; col++) {\n            columns.push(col);\n        }\n        model.dispatch(\"AUTORESIZE_COLUMNS\", { sheetId, cols: columns });\n        const sidePanel = stores.get(SidePanelStore);\n        sidePanel.open(\"LIST_PROPERTIES_PANEL\", { listId });\n    };\n}\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nconst { otRegistry } = spreadsheet.registries;\n\notRegistry\n    .addTransformation(\n        \"INSERT_ODOO_LIST\",\n        [\"INSERT_ODOO_LIST\", \"DUPLICATE_ODOO_LIST\"],\n        transformNewListCommand\n    )\n    .addTransformation(\n        \"DUPLICATE_ODOO_LIST\",\n        [\"INSERT_ODOO_LIST\", \"DUPLICATE_ODOO_LIST\"],\n        transformNewListCommand\n    )\n    .addTransformation(\n        \"REMOVE_ODOO_LIST\",\n        [\"RENAME_ODOO_LIST\", \"UPDATE_ODOO_LIST_DOMAIN\", \"UPDATE_ODOO_LIST\"],\n        (toTransform, executed) => {\n            if (toTransform.listId === executed.listId) {\n                return undefined;\n            }\n            return toTransform;\n        }\n    )\n    .addTransformation(\n        \"REMOVE_ODOO_LIST\",\n        [\"RE_INSERT_ODOO_LIST\", \"DUPLICATE_ODOO_LIST\"],\n        (toTransform, executed) => {\n            if (toTransform.id === executed.listId) {\n                return undefined;\n            }\n            return toTransform;\n        }\n    );\n\nfunction transformNewListCommand(toTransform) {\n    const idKey = \"newListId\" in toTransform ? \"newListId\" : \"id\";\n    return {\n        ...toTransform,\n        [idKey]: (parseInt(toTransform[idKey], 10) + 1).toString(),\n    };\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { astToFormula, UIPlugin, tokenize } from \"@odoo/o-spreadsheet\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { getFirstListFunction, getNumberOfListFormulas } from \"@spreadsheet/list/list_helpers\";\n\nexport class ListAutofillPlugin extends UIPlugin {\n    // ---------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------\n\n    /**\n     * Get the next value to autofill of a list function\n     *\n     * @param {string} formula List formula\n     * @param {boolean} isColumn True if autofill is LEFT/RIGHT, false otherwise\n     * @param {number} increment number of steps\n     *\n     * @returns Autofilled value\n     */\n    getNextListValue(formula, isColumn, increment) {\n        const tokens = tokenize(formula);\n        if (getNumberOfListFormulas(tokens) !== 1) {\n            return formula;\n        }\n        const { functionName, args } = getFirstListFunction(tokens);\n        const evaluatedArgs = args\n            .map(astToFormula)\n            .map((arg) => this.getters.evaluateFormula(this.getters.getActiveSheetId(), arg));\n        const listId = evaluatedArgs[0];\n        if (!this.getters.isExistingList(listId)) {\n            return formula;\n        }\n        const columns = this.getters.getListDefinition(listId).columns;\n        if (functionName === \"ODOO.LIST\") {\n            const position = parseInt(evaluatedArgs[1], 10);\n            const field = evaluatedArgs[2];\n            if (isColumn) {\n                /** Change the field */\n                const index = columns.findIndex((col) => col === field) + increment;\n                if (index < 0 || index >= columns.length) {\n                    return \"\";\n                }\n                return this._getListFunction(listId, position, columns[index]);\n            } else {\n                /** Change the position */\n                const nextPosition = position + increment;\n                if (nextPosition === 0) {\n                    return this._getListHeaderFunction(listId, field);\n                }\n                if (nextPosition < 0) {\n                    return \"\";\n                }\n                return this._getListFunction(listId, nextPosition, field);\n            }\n        }\n        if (functionName === \"ODOO.LIST.HEADER\") {\n            const field = evaluatedArgs[1];\n            if (isColumn) {\n                /** Change the field */\n                const index = columns.findIndex((col) => col === field) + increment;\n                if (index < 0 || index >= columns.length) {\n                    return \"\";\n                }\n                return this._getListHeaderFunction(listId, columns[index]);\n            } else {\n                /** If down, set position */\n                if (increment > 0) {\n                    return this._getListFunction(listId, increment, field);\n                }\n                return \"\";\n            }\n        }\n        return formula;\n    }\n\n    /**\n     * Compute the tooltip to display from a Pivot formula\n     *\n     * @param {string} formula Pivot formula\n     * @param {boolean} isColumn True if the direction is left/right, false\n     *                           otherwise\n     */\n    getTooltipListFormula(formula, isColumn) {\n        if (!formula) {\n            return [];\n        }\n        const { functionName, args } = getFirstListFunction(tokenize(formula));\n        const evaluatedArgs = args\n            .map(astToFormula)\n            .map((arg) => this.getters.evaluateFormula(this.getters.getActiveSheetId(), arg));\n        const listId = evaluatedArgs[0];\n        if (!this.getters.isExistingList(listId)) {\n            return sprintf(_t(\"Missing list #%s\"), listId);\n        }\n        if (isColumn || functionName === \"ODOO.LIST.HEADER\") {\n            const fieldName = functionName === \"ODOO.LIST\" ? evaluatedArgs[2] : evaluatedArgs[1];\n            return this.getters.getListDataSource(listId).getListHeaderValue(fieldName);\n        }\n        return _t(\"Record #%(record_number)s\", { record_number: evaluatedArgs[1] });\n    }\n\n    _getListFunction(listId, position, field) {\n        return `=ODOO.LIST(${listId},${position},\"${field}\")`;\n    }\n\n    _getListHeaderFunction(listId, field) {\n        return `=ODOO.LIST.HEADER(${listId},\"${field}\")`;\n    }\n}\n\nListAutofillPlugin.getters = [\"getNextListValue\", \"getTooltipListFormula\"];\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { components } from \"@odoo/o-spreadsheet\";\nconst { AddDimensionButton, Section } = components;\n\nexport class EditListSortingSection extends Component {\n    static template = \"spreadsheet_edition.EditListSortingSection\";\n    static components = { Dialog, CheckBox, AddDimensionButton, Section };\n    static props = {\n        onUpdateSorting: Function,\n        orderBy: Array,\n        fields: Object,\n        resModel: String,\n    };\n\n    setup() {\n        this.mainRef = useRef(\"main\");\n        useSortable({\n            enable: true,\n            ref: this.mainRef,\n            elements: \".o_draggable\",\n            cursor: \"move\",\n            delay: 100,\n            tolerance: 10,\n            ignore: \"select\",\n            onDrop: (params) => this._onSortDrop(params),\n        });\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} params.previous\n     */\n    _onSortDrop({ element, previous }) {\n        const orderByArray = [...this.props.orderBy];\n        const elementIndex = orderByArray.findIndex((order) => order.name === element.dataset.id);\n        const orderBy = orderByArray[elementIndex];\n        orderByArray.splice(elementIndex, 1);\n        const newIndex = previous\n            ? orderByArray.findIndex((order) => order.name === previous.dataset.id) + 1\n            : 0;\n        orderByArray.splice(newIndex, 0, orderBy);\n        this.updateSorting(orderByArray);\n    }\n\n    updateSorting(orderBy) {\n        this.props.onUpdateSorting(orderBy);\n    }\n\n    isFieldAllowed(field) {\n        return field.sortable && !this.props.orderBy.map((el) => el.name).includes(field.name);\n    }\n\n    getAllowedFields() {\n        return Object.values(this.props.fields)\n            .filter((field) => this.isFieldAllowed(field))\n            .sort((a, b) => a.string.localeCompare(b.string));\n    }\n\n    onAddSortingRule(fieldName) {\n        const orderByArray = [...this.props.orderBy, { name: fieldName, asc: true }];\n        this.updateSorting(orderByArray);\n    }\n\n    onDeleteSortingRule(ruleIndex) {\n        const orderByArray = [...this.props.orderBy];\n        orderByArray.splice(ruleIndex, 1);\n        this.updateSorting(orderByArray);\n    }\n\n    toggleAscending(ruleIndex) {\n        const orderByArray = [...this.props.orderBy];\n        orderByArray[ruleIndex] = {\n            ...orderByArray[ruleIndex],\n            asc: !orderByArray[ruleIndex].asc,\n        };\n        this.updateSorting(orderByArray);\n    }\n}\n", "/** @odoo-module */\n\nimport { Domain } from \"@web/core/domain\";\nimport { EditListSortingSection } from \"./edit_list_sorting_section/edit_list_sorting_section\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { getListHighlights } from \"../list_highlight_helpers\";\n\nimport { hooks, components } from \"@odoo/o-spreadsheet\";\nimport { SidePanelDomain } from \"../../components/side_panel_domain/side_panel_domain\";\n\nconst { useHighlights } = hooks;\nconst { ValidationMessages, EditableName, CogWheelMenu, Section } = components;\n\nexport class ListDetailsSidePanel extends Component {\n    static template = \"spreadsheet_edition.ListDetailsSidePanel\";\n    static components = {\n        EditableName,\n        ValidationMessages,\n        CogWheelMenu,\n        Section,\n        SidePanelDomain,\n        EditListSortingSection,\n    };\n    static props = {\n        onCloseSidePanel: Function,\n        listId: String,\n    };\n\n    setup() {\n        this.getters = this.env.model.getters;\n        this.notification = useService(\"notification\");\n        const loadData = async (listId) => {\n            const dataSource = await this.env.model.getters.getAsyncListDataSource(listId);\n            this.modelDisplayName = await dataSource.getModelLabel();\n        };\n        onWillStart(async () => {\n            // it's assumed `this.props.listId` never changes (t-key is required when using this component)\n            await loadData(this.props.listId);\n            const dataSource = this.env.model.getters.getListDataSource(this.props.listId);\n            // Store the fields here because the data source can be reset when updating the list.\n            // Forcing a reload with onWillUpdateProps would introduce flickering\n            // and the fields never change anyway.\n            this.listFields = dataSource.getFields();\n        });\n        useHighlights(this);\n    }\n\n    get cogWheelMenuItems() {\n        return [\n            {\n                name: \"Duplicate\",\n                icon: \"o-spreadsheet-Icon.COPY\",\n                execute: () => this.duplicateList(),\n            },\n            {\n                name: \"Delete\",\n                icon: \"o-spreadsheet-Icon.TRASH\",\n                execute: () => this.deleteList(),\n            },\n        ];\n    }\n\n    get listDefinition() {\n        const listId = this.props.listId;\n        const def = this.getters.getListDefinition(listId);\n        return {\n            model: def.model,\n            modelDisplayName: this.modelDisplayName,\n            domain: new Domain(def.domain).toString(),\n            orderBy: def.orderBy,\n        };\n    }\n\n    getLastUpdate() {\n        const lastUpdate = this.env.model.getters.getListDataSource(this.props.listId).lastUpdate;\n        if (lastUpdate) {\n            return new Date(lastUpdate).toLocaleTimeString();\n        }\n        return _t(\"never\");\n    }\n\n    getColumnFields() {\n        return this.getters\n            .getListDefinition(this.props.listId)\n            .columns.map((col) => this.listFields[col]);\n    }\n\n    onNameChanged(name) {\n        this.env.model.dispatch(\"RENAME_ODOO_LIST\", {\n            listId: this.props.listId,\n            name,\n        });\n    }\n\n    onDomainUpdate(domain) {\n        const listDefinition = this.getters.getListModelDefinition(this.props.listId);\n        this.env.model.dispatch(\"UPDATE_ODOO_LIST\", {\n            listId: this.props.listId,\n            list: {\n                ...listDefinition,\n                searchParams: {\n                    ...listDefinition.searchParams,\n                    domain,\n                },\n            },\n        });\n    }\n\n    /**\n     * @param {{name: string, asc: boolean}[]} orderBy\n     */\n    onUpdateSorting(orderBy) {\n        const listDefinition = this.getters.getListModelDefinition(this.props.listId);\n        this.env.model.dispatch(\"UPDATE_ODOO_LIST\", {\n            listId: this.props.listId,\n            list: {\n                ...listDefinition,\n                searchParams: {\n                    ...listDefinition.searchParams,\n                    orderBy,\n                },\n            },\n        });\n    }\n\n    duplicateList() {\n        const newListId = this.env.model.getters.getNextListId();\n        const result = this.env.model.dispatch(\"DUPLICATE_ODOO_LIST\", {\n            listId: this.props.listId,\n            newListId,\n        });\n        const msg = result.isSuccessful\n            ? _t('List duplicated. Use the \"Re-insert list\" menu item to insert it in a sheet.')\n            : _t(\"List duplication failed\");\n        const type = result.isSuccessful ? \"success\" : \"danger\";\n        this.notification.add(msg, { sticky: false, type });\n        if (result.isSuccessful) {\n            this.env.openSidePanel(\"LIST_PROPERTIES_PANEL\", { listId: newListId });\n        }\n    }\n\n    deleteList() {\n        this.env.askConfirmation(_t(\"Are you sure you want to delete this list?\"), () => {\n            this.env.model.dispatch(\"REMOVE_ODOO_LIST\", { listId: this.props.listId });\n            this.props.onCloseSidePanel();\n        });\n    }\n\n    get unusedListWarning() {\n        return _t(\"This list is not used\");\n    }\n\n    get highlights() {\n        return getListHighlights(this.env.model.getters, this.props.listId);\n    }\n}\n", "/** @odoo-module **/\n\n/**\n * This class implements the `TransportService` interface defined\n * by o-spreadsheet. Its purpose is to communicate with other clients\n * by sending and receiving spreadsheet messages through the server.\n * @see https://github.com/odoo/o-spreadsheet\n *\n * It listens messages on the long polling bus and forwards spreadsheet messages\n * to the handler. (note: it is assumed there is only one handler)\n *\n * It uses the RPC protocol to send messages to the server which\n * push them in the long polling bus for other clients.\n */\nexport class SpreadsheetCollaborativeChannel {\n    static dependencies = [\"bus_service\", \"orm\"];\n    /**\n     * @param {Env} env\n     * @param {string} resModel model linked to the spreadsheet\n     * @param {number} resId Id of the spreadsheet\n     * @param {number} [shareId]\n     * @param {string} [accessToken] sharing token\n     */\n    constructor(env, resModel, resId, shareId, accessToken) {\n        this.env = env;\n        this.orm = env.services.orm.silent;\n        this.resId = resId;\n        this.resModel = resModel;\n        this.shareId = shareId;\n        this.accessToken = accessToken;\n        /**\n         * A callback function called to handle messages when they are received.\n         */\n        this._listener;\n        /**\n         * Messages are queued while there is no listener. They are forwarded\n         * once it registers.\n         */\n        this._queue = [];\n        this._channel = this._getChannel();\n        this.env.services.bus_service.addChannel(this._channel);\n        this.env.services.bus_service.subscribe(\"spreadsheet\", (payload) => {\n            if (payload.id === this.resId) {\n                this._handleNotification(payload);\n            }\n        });\n    }\n\n    /**\n     * Register a function that is called whenever a new spreadsheet revision\n     * message notification is received by server.\n     *\n     * @param {any} id\n     * @param {Function} callback\n     */\n    onNewMessage(id, callback) {\n        this._listener = callback;\n        for (const message of this._queue) {\n            callback(message);\n        }\n        this._queue = [];\n    }\n\n    /**\n     * Send a message to the server\n     *\n     * @param {Object} message\n     */\n    async sendMessage(message) {\n        const isAccepted = await this.orm.call(this.resModel, \"dispatch_spreadsheet_message\", [\n            this.resId,\n            message,\n            this.accessToken,\n        ]);\n        if (isAccepted) {\n            this._handleNotification(message);\n        }\n    }\n\n    /**\n     * Stop listening new messages\n     */\n    leave() {\n        this._listener = undefined;\n    }\n\n    /**\n     * Either forward the message to the listener if it's already registered,\n     * or put it in a queue.\n     *\n     * @private\n     * @param {Object} notifs\n     */\n    _handleNotification(payload) {\n        if (!this._listener) {\n            this._queue.push(payload);\n        } else {\n            this._listener(payload);\n        }\n    }\n\n    /**\n     * @private\n     * @returns {string}\n     */\n    _getChannel() {\n        // Listening this channel tells the server the spreadsheet is active\n        // but the server will actually push to channel [{dbname},  {resModel}, {resId}]\n        // The user can listen to this channel only if he has the required read access.\n        const channel = `spreadsheet_collaborative_session:${this.resModel}:${this.resId}`;\n        if (this.shareId && this.accessToken) {\n            return `${channel}:${this.shareId}:${this.accessToken}`;\n        }\n        return channel;\n    }\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { SpreadsheetCollaborativeChannel } from \"./spreadsheet_collaborative_channel\";\n\n/**\n * Creates a channel to handle collaborative edition of a spreadsheet.\n * This service can be mocked to mock the channel in tests.\n */\nconst spreadsheetCollaborativeService = {\n    dependencies: SpreadsheetCollaborativeChannel.dependencies,\n    start(env, dependencies) {\n        return {\n            makeCollaborativeChannel(resModel, resId, shareId, accessToken) {\n                return new SpreadsheetCollaborativeChannel(\n                    env,\n                    resModel,\n                    resId,\n                    shareId,\n                    accessToken\n                );\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"spreadsheet_collaborative\", spreadsheetCollaborativeService);\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registries, stores } from \"@odoo/o-spreadsheet\";\nimport { REINSERT_LIST_CHILDREN } from \"../list/list_actions\";\nimport {\n    REINSERT_DYNAMIC_PIVOT_CHILDREN,\n    REINSERT_STATIC_PIVOT_CHILDREN,\n} from \"../pivot/pivot_actions\";\nimport { getListHighlights } from \"../list/list_highlight_helpers\";\nconst { topbarMenuRegistry } = registries;\nconst { HighlightStore } = stores;\n\n//--------------------------------------------------------------------------\n// Spreadsheet context menu items\n//--------------------------------------------------------------------------\n\ntopbarMenuRegistry.addChild(\"new_sheet\", [\"file\"], {\n    name: _t(\"New\"),\n    sequence: 10,\n    isVisible: (env) => env.newSpreadsheet,\n    execute: (env) => env.newSpreadsheet(),\n    icon: \"o-spreadsheet-Icon.NEW\",\n});\ntopbarMenuRegistry.addChild(\"make_copy\", [\"file\"], {\n    name: _t(\"Make a copy\"),\n    sequence: 20,\n    isVisible: (env) => env.makeCopy,\n    execute: (env) => env.makeCopy(),\n    icon: \"o-spreadsheet-Icon.COPY_FILE\",\n});\ntopbarMenuRegistry.addChild(\"save_as_template\", [\"file\"], {\n    name: _t(\"Save as template\"),\n    sequence: 40,\n    isVisible: (env) => env.saveAsTemplate,\n    execute: (env) => env.saveAsTemplate(),\n    icon: \"o-spreadsheet-Icon.SAVE\",\n});\ntopbarMenuRegistry.addChild(\"download\", [\"file\"], {\n    name: _t(\"Download\"),\n    sequence: 50,\n    isVisible: (env) => env.download,\n    execute: (env) => env.download(),\n    isReadonlyAllowed: true,\n    icon: \"o-spreadsheet-Icon.DOWNLOAD\",\n});\n\ntopbarMenuRegistry.addChild(\"clear_history\", [\"file\"], {\n    name: _t(\"Snapshot\"),\n    sequence: 60,\n    isVisible: (env) => env.debug,\n    execute: (env) => {\n        env.model.session.snapshot(env.model.exportData());\n        env.model.garbageCollectExternalResources();\n        window.location.reload();\n    },\n    icon: \"o-spreadsheet-Icon.CAMERA\",\n});\n\ntopbarMenuRegistry.addChild(\"download_as_json\", [\"file\"], {\n    name: _t(\"Download as JSON\"),\n    sequence: 70,\n    isVisible: (env) => env.debug && env.downloadAsJson,\n    execute: (env) => env.downloadAsJson(),\n    isReadonlyAllowed: true,\n    icon: \"o-spreadsheet-Icon.DOWNLOAD_AS_JSON\",\n});\n\ntopbarMenuRegistry.addChild(\"data_sources_data\", [\"data\"], (env) => {\n    let sequence = 1000;\n    const lists_items = env.model.getters.getListIds().map((listId, index) => {\n        const highlightProvider = {\n            get highlights() {\n                return getListHighlights(env.model.getters, listId);\n            },\n        };\n        return {\n            id: `item_list_${listId}`,\n            name: env.model.getters.getListDisplayName(listId),\n            sequence: sequence++,\n            execute: (env) => {\n                env.openSidePanel(\"LIST_PROPERTIES_PANEL\", { listId });\n            },\n            onStartHover: (env) => env.getStore(HighlightStore).register(highlightProvider),\n            onStopHover: (env) => env.getStore(HighlightStore).unRegister(highlightProvider),\n            icon: \"o-spreadsheet-Icon.ODOO_LIST\",\n            separator: index === env.model.getters.getListIds().length - 1,\n            secondaryIcon: (env) =>\n                env.model.getters.isListUnused(listId)\n                    ? \"o-spreadsheet-Icon.UNUSED_LIST_WARNING\"\n                    : undefined,\n        };\n    });\n    const charts_items = env.model.getters.getOdooChartIds().map((chartId, index) => {\n        return {\n            id: `item_chart_${chartId}`,\n            name: env.model.getters.getOdooChartDisplayName(chartId),\n            sequence: sequence++,\n            execute: (env) => {\n                env.model.dispatch(\"SELECT_FIGURE\", { id: chartId });\n                env.openSidePanel(\"ChartPanel\");\n            },\n            icon: \"o-spreadsheet-Icon.INSERT_CHART\",\n            separator: index === env.model.getters.getOdooChartIds().length - 1,\n        };\n    });\n    return lists_items.concat(charts_items).concat([\n        {\n            id: \"refresh_all_data\",\n            name: _t(\"Refresh all data\"),\n            sequence: sequence++,\n            execute: (env) => {\n                env.model.dispatch(\"REFRESH_ALL_DATA_SOURCES\");\n            },\n            separator: true,\n            icon: \"o-spreadsheet-Icon.REFRESH_DATA\",\n        },\n    ]);\n});\n\nconst reinsertDynamicPivotMenu = {\n    id: \"reinsert_dynamic_pivot\",\n    name: _t(\"Re-insert dynamic pivot\"),\n    sequence: 1020,\n    children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],\n    isVisible: (env) =>\n        env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid()),\n    icon: \"o-spreadsheet-Icon.INSERT_PIVOT\",\n};\nconst reinsertStaticPivotMenu = {\n    id: \"reinsert_static_pivot\",\n    name: _t(\"Re-insert static pivot\"),\n    sequence: 1021,\n    children: [REINSERT_STATIC_PIVOT_CHILDREN],\n    isVisible: (env) =>\n        env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid()),\n    icon: \"o-spreadsheet-Icon.INSERT_PIVOT\",\n};\n\nconst reInsertListMenu = {\n    id: \"reinsert_list\",\n    name: _t(\"Re-insert list\"),\n    sequence: 1021,\n    children: [REINSERT_LIST_CHILDREN],\n    isVisible: (env) => env.model.getters.getListIds().length,\n    icon: \"o-spreadsheet-Icon.INSERT_LIST\",\n};\n\nconst printMenu = {\n    name: _t(\"Print\"),\n    sequence: 60,\n    isVisible: (env) => env.print,\n    execute: (env) => env.print(),\n    icon: \"o-spreadsheet-Icon.PRINT\",\n};\n\ntopbarMenuRegistry.addChild(\"print\", [\"file\"], printMenu);\ntopbarMenuRegistry.addChild(\"reinsert_list\", [\"data\"], reInsertListMenu);\ntopbarMenuRegistry.addChild(\"reinsert_dynamic_pivot\", [\"data\"], reinsertDynamicPivotMenu, {\n    force: true,\n});\ntopbarMenuRegistry.addChild(\"reinsert_static_pivot\", [\"data\"], reinsertStaticPivotMenu, {\n    force: true,\n});\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\n\nimport { Component } from \"@odoo/owl\";\nimport { containsReferences } from \"@spreadsheet/helpers/helpers\";\n\nconst { autofillModifiersRegistry, autofillRulesRegistry } = spreadsheet.registries;\nconst tokenize = spreadsheet.tokenize;\nconst { getNumberOfPivotFunctions } = spreadsheet.helpers;\n\nfunction isOdooPivotFormula(formula, getters) {\n    const tokens = tokenize(formula);\n    if (getNumberOfPivotFunctions(tokens) !== 1) {\n        return false;\n    }\n    const { args } = getters.getFirstPivotFunction(getters.getActiveSheetId(), tokens);\n    const argPivotId = args.length > 0 && args[0]?.toString();\n    if (!argPivotId) {\n        return false;\n    }\n    const pivotId = getters.getPivotId(args[0].toString());\n    if (!pivotId) {\n        return false;\n    }\n    return getters.getPivotCoreDefinition(pivotId).type === \"ODOO\";\n}\n\n//--------------------------------------------------------------------------\n// Autofill Component\n//--------------------------------------------------------------------------\nexport class AutofillTooltip extends Component {\n    static template = \"spreadsheet_edition.AutofillTooltip\";\n    static props = { content: Array };\n}\n\n//--------------------------------------------------------------------------\n// Autofill Rules\n//--------------------------------------------------------------------------\n\nautofillRulesRegistry\n    .add(\"autofill_pivot\", {\n        condition: (cell) =>\n            cell && cell.isFormula && cell.content.match(/=\\s*PIVOT/) && !containsReferences(cell),\n        generateRule: (cell, cells) => {\n            const increment = cells.filter(\n                (cell) => cell && cell.isFormula && cell.content.match(/=\\s*PIVOT/)\n            ).length;\n            return { type: \"PIVOT_UPDATER\", increment, current: 0 };\n        },\n        sequence: 2,\n    });\n\n//--------------------------------------------------------------------------\n// Autofill Modifier\n//--------------------------------------------------------------------------\n\nautofillModifiersRegistry\n    .add(\"PIVOT_UPDATER\", {\n        apply: (rule, data, getters, direction) => {\n            if (!isOdooPivotFormula(data.cell.content, getters)) {\n                return { cellData: data.cell, tooltip: undefined };\n            }\n            rule.current += rule.increment;\n            let isColumn;\n            let steps;\n            switch (direction) {\n                case \"up\":\n                    isColumn = false;\n                    steps = -rule.current;\n                    break;\n                case \"down\":\n                    isColumn = false;\n                    steps = rule.current;\n                    break;\n                case \"left\":\n                    isColumn = true;\n                    steps = -rule.current;\n                    break;\n                case \"right\":\n                    isColumn = true;\n                    steps = rule.current;\n            }\n            const content = getters.getPivotNextAutofillValue(data.cell.content, isColumn, steps);\n            let tooltip = {\n                props: {\n                    content: data.content,\n                },\n            };\n            if (content && content !== data.content) {\n                tooltip = {\n                    props: {\n                        content: getters.getTooltipFormula(content, isColumn),\n                    },\n                    component: AutofillTooltip,\n                };\n            }\n            if (!content) {\n                tooltip = undefined;\n            }\n            return {\n                cellData: {\n                    style: undefined,\n                    format: data.cell && data.cell.format,\n                    border: undefined,\n                    content,\n                },\n                tooltip,\n            };\n        },\n    });\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { initCallbackRegistry } from \"@spreadsheet/o_spreadsheet/init_callbacks\";\n\nimport { PivotAutofillPlugin } from \"./plugins/pivot_autofill_plugin\";\nimport { PivotDetailsSidePanel } from \"./side_panels/pivot_details_side_panel\";\n\nimport \"./autofill\";\nimport { insertPivot } from \"./pivot_init_callback\";\n\nconst { featurePluginRegistry, cellMenuRegistry, pivotSidePanelRegistry } = spreadsheet.registries;\n\nfeaturePluginRegistry.add(\"odooPivotAutofillPlugin\", PivotAutofillPlugin);\n\npivotSidePanelRegistry.add(\"ODOO\", {\n    editor: PivotDetailsSidePanel,\n});\n\ninitCallbackRegistry.add(\"insertPivot\", insertPivot);\n\ncellMenuRegistry.add(\"pivot_properties\", {\n    name: _t(\"See pivot properties\"),\n    sequence: 170,\n    execute(env) {\n        const position = env.model.getters.getActivePosition();\n        const pivotId = env.model.getters.getPivotIdFromPosition(position);\n        env.openSidePanel(\"PivotSidePanel\", { pivotId });\n    },\n    isVisible: (env) => {\n        const position = env.model.getters.getActivePosition();\n        return env.model.getters.isExistingPivot(\n            env.model.getters.getPivotIdFromPosition(position)\n        );\n    },\n    icon: \"o-spreadsheet-Icon.PIVOT\",\n});\n", "/** @odoo-module */\n\nimport * as spreadsheet from \"@odoo/o-spreadsheet\";\nimport { OdooPivot } from \"@spreadsheet/pivot/odoo_pivot\";\nimport { patch } from \"@web/core/utils/patch\";\nconst { helpers } = spreadsheet;\nconst { formatValue } = helpers;\n\n/**\n * @typedef {import(\"@odoo/o-spreadsheet\").PivotDomain} PivotDomain\n */\n\npatch(OdooPivot.prototype, {\n    /**\n     * High level method computing the formatted result of PIVOT.HEADER functions.\n     *\n     * @param {PivotDomain} domain\n     */\n    getPivotHeaderFormattedValue(domain) {\n        const { value, format } = this.getPivotHeaderValueAndFormat(domain);\n        if (typeof value === \"string\") {\n            return value;\n        }\n        const locale = this.getters.getLocale();\n        return formatValue(value, { format, locale });\n    },\n});\n", "/** @odoo-module **/\n\nexport const REINSERT_DYNAMIC_PIVOT_CHILDREN = (env) =>\n    env.model.getters.getPivotIds().map((pivotId, index) => ({\n        id: `reinsert_dynamic_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,\n        name: env.model.getters.getPivotDisplayName(pivotId),\n        sequence: index,\n        execute: async (env) => {\n            const { type } = env.model.getters.getPivotCoreDefinition(pivotId);\n            const position = env.model.getters.getActivePosition();\n            let table;\n            if (type === \"ODOO\") {\n                const dataSource = env.model.getters.getPivot(pivotId);\n                const model = await dataSource.copyModelWithOriginalDomain();\n                table = model.getTableStructure().export();\n            } else {\n                table = env.model.getters.getPivot(pivotId).getTableStructure().export();\n            }\n            env.model.dispatch(\"INSERT_PIVOT_WITH_TABLE\", {\n                ...position,\n                pivotId,\n                table,\n                pivotMode: \"dynamic\",\n            });\n            env.model.dispatch(\"REFRESH_PIVOT\", { id: pivotId });\n        },\n        isVisible: (env) => env.model.getters.getPivot(pivotId).isValid(),\n    }));\n\nexport const REINSERT_STATIC_PIVOT_CHILDREN = (env) =>\n    env.model.getters.getPivotIds().map((pivotId, index) => ({\n        id: `reinsert_static_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,\n        name: env.model.getters.getPivotDisplayName(pivotId),\n        sequence: index,\n        execute: async (env) => {\n            const { type } = env.model.getters.getPivotCoreDefinition(pivotId);\n            const position = env.model.getters.getActivePosition();\n            let table;\n            if (type === \"ODOO\") {\n                const dataSource = env.model.getters.getPivot(pivotId);\n                const model = await dataSource.copyModelWithOriginalDomain();\n                table = model.getTableStructure().export();\n            } else {\n                table = env.model.getters.getPivot(pivotId).getTableStructure().export();\n            }\n            env.model.dispatch(\"INSERT_PIVOT_WITH_TABLE\", {\n                ...position,\n                pivotId,\n                table,\n                pivotMode: \"static\",\n            });\n            env.model.dispatch(\"REFRESH_PIVOT\", { id: pivotId });\n        },\n        isVisible: (env) => env.model.getters.getPivot(pivotId).isValid(),\n    }));\n", "/** @odoo-module **/\n//@ts-check\n\nimport { helpers, stores } from \"@odoo/o-spreadsheet\";\nimport { OdooPivot } from \"@spreadsheet/pivot/odoo_pivot\";\nimport { Domain } from \"@web/core/domain\";\nimport { deepCopy } from \"@web/core/utils/objects\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst uuidGenerator = new helpers.UuidGenerator();\nconst { parseDimension, isDateOrDatetimeField } = helpers;\n\nconst { SidePanelStore } = stores;\n\n/**\n * Asserts that the given result is successful, otherwise throws an error.\n *\n * @param {import(\"@odoo/o-spreadsheet\").DispatchResult} result\n */\nfunction ensureSuccess(result) {\n    if (!result.isSuccessful) {\n        throw new Error(`Couldn't insert pivot in spreadsheet. Reasons : ${result.reasons}`);\n    }\n}\n\nfunction addEmptyGranularity(dimensions, fields) {\n    return dimensions.map((dimension) => {\n        if (isDateOrDatetimeField(fields[dimension.fieldName])) {\n            return {\n                granularity: \"month\",\n                ...dimension,\n            };\n        }\n        return dimension;\n    });\n}\n\nexport function insertPivot(pivotData) {\n    const fields = pivotData.metaData.fields;\n    const measures = pivotData.metaData.activeMeasures.map((measure) => ({\n        id: fields[measure]?.aggregator ? `${measure}:${fields[measure].aggregator}` : measure,\n        fieldName: measure,\n        aggregator: fields[measure]?.aggregator,\n    }));\n    /** @type {import(\"@spreadsheet\").OdooPivotCoreDefinition} */\n    const pivot = deepCopy({\n        type: \"ODOO\",\n        domain: new Domain(pivotData.searchParams.domain).toJson(),\n        context: pivotData.searchParams.context,\n        sortedColumn: pivotData.metaData.sortedColumn,\n        measures,\n        model: pivotData.metaData.resModel,\n        columns: addEmptyGranularity(\n            pivotData.metaData.fullColGroupBys.map(parseDimension),\n            fields\n        ),\n        rows: addEmptyGranularity(pivotData.metaData.fullRowGroupBys.map(parseDimension), fields),\n        name: pivotData.name,\n        actionXmlId: pivotData.actionXmlId,\n    });\n    /**\n     * @param {import(\"@spreadsheet\").OdooSpreadsheetModel} model\n     */\n    return async (model, stores) => {\n        const pivotId = uuidGenerator.uuidv4();\n        ensureSuccess(\n            model.dispatch(\"ADD_PIVOT\", {\n                pivotId,\n                pivot,\n            })\n        );\n        const ds = model.getters.getPivot(pivotId);\n        if (!(ds instanceof OdooPivot)) {\n            throw new Error(\"The pivot data source is not an OdooPivot\");\n        }\n        await ds.load();\n\n        let sheetName = _t(\"%(pivot_name)s (Pivot #%(pivot_id)s)\", {\n            pivot_name: pivot.name,\n            pivot_id: model.getters.getPivotFormulaId(pivotId),\n        });\n        // Add an empty sheet in the case of an existing spreadsheet.\n        if (!this.isEmptySpreadsheet) {\n            const sheetId = uuidGenerator.uuidv4();\n            const sheetIdFrom = model.getters.getActiveSheetId();\n            if (model.getters.getSheetIdByName(sheetName)) {\n                sheetName = undefined;\n            }\n            model.dispatch(\"CREATE_SHEET\", {\n                sheetId,\n                position: model.getters.getSheetIds().length,\n                name: sheetName,\n            });\n            model.dispatch(\"ACTIVATE_SHEET\", { sheetIdFrom, sheetIdTo: sheetId });\n        } else {\n            model.dispatch(\"RENAME_SHEET\", {\n                sheetId: model.getters.getActiveSheetId(),\n                name: sheetName,\n            });\n        }\n        const sheetId = model.getters.getActiveSheetId();\n\n        const table = ds.getTableStructure();\n        ensureSuccess(\n            model.dispatch(\"INSERT_PIVOT_WITH_TABLE\", {\n                sheetId,\n                col: 0,\n                row: 0,\n                pivotId,\n                table: table.export(),\n                pivotMode: \"static\",\n            })\n        );\n\n        const columns = [];\n        for (let col = 0; col <= table.columns[table.columns.length - 1].length; col++) {\n            columns.push(col);\n        }\n        model.dispatch(\"AUTORESIZE_COLUMNS\", { sheetId, cols: columns });\n        const sidePanel = stores.get(SidePanelStore);\n        sidePanel.open(\"PivotSidePanel\", { pivotId });\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { UIPlugin, tokenize, helpers } from \"@odoo/o-spreadsheet\";\nimport { domainHasNoRecordAtThisPosition } from \"@spreadsheet/pivot/pivot_helpers\";\n\nconst { getNumberOfPivotFunctions, isDateOrDatetimeField, pivotTimeAdapter, createPivotFormula } =\n    helpers;\n\n/**\n * @typedef {import(\"@odoo/o-spreadsheet\").SpreadsheetPivotTable} SpreadsheetPivotTable\n * @typedef {import(\"@spreadsheet\").OdooPivotDefinition} OdooPivotDefinition\n * @typedef {import(\"@spreadsheet/pivot/odoo_pivot\").OdooPivot} OdooPivot\n * @typedef {import(\"@spreadsheet/pivot/odoo_pivot\").PivotDomain} PivotDomain\n */\n\n/**\n * @typedef CurrentElement\n * @property {Array<string>} cols\n * @property {Array<string>} rows\n *\n * @typedef TooltipFormula\n * @property {string} value\n *\n * @typedef GroupByDate\n * @property {boolean} isDate\n * @property {string|undefined} group\n */\n\nexport class PivotAutofillPlugin extends UIPlugin {\n    // ---------------------------------------------------------------------\n    // Getters\n    // ---------------------------------------------------------------------\n\n    /**\n     * Get the next value to autofill of a pivot function\n     *\n     * @param {string} formula Pivot formula\n     * @param {boolean} isColumn True if autofill is LEFT/RIGHT, false otherwise\n     * @param {number} increment number of steps\n     *\n     * @returns {string}\n     */\n    getPivotNextAutofillValue(formula, isColumn, increment) {\n        const tokens = tokenize(formula);\n        if (getNumberOfPivotFunctions(tokens) !== 1) {\n            return formula;\n        }\n        const { functionName, args } = this.getters.getFirstPivotFunction(\n            this.getters.getActiveSheetId(),\n            tokens\n        );\n        if (args.some((arg) => arg === undefined)) {\n            return formula;\n        }\n        const evaluatedArgs = args.map((arg) => arg.toString());\n        const pivotId = this.getters.getPivotId(evaluatedArgs[0]);\n        if (!pivotId) {\n            return formula;\n        }\n        const dataSource = this.getters.getPivot(pivotId);\n        const definition = dataSource.definition;\n        for (let i = evaluatedArgs.length - 1; i > 0; i--) {\n            const fieldName = evaluatedArgs[i];\n            if (\n                fieldName.startsWith(\"#\") &&\n                ((isColumn && this._isColumnGroupBy(dataSource, definition, fieldName)) ||\n                    (!isColumn && this._isRowGroupBy(dataSource, definition, fieldName)))\n            ) {\n                evaluatedArgs[i + 1] = parseInt(evaluatedArgs[i + 1], 10) + increment;\n                if (evaluatedArgs[i + 1] < 0) {\n                    return formula;\n                }\n                if (functionName === \"PIVOT.VALUE\") {\n                    const [formulaId, measure, ...domain] = evaluatedArgs;\n                    const pivotCell = {\n                        type: \"VALUE\",\n                        measure,\n                        domain: this._toPivotDomainWithPositional(dataSource, domain),\n                    };\n                    return createPivotFormula(formulaId, pivotCell);\n                } else if (functionName === \"PIVOT.HEADER\") {\n                    const [formulaId, ...domain] = evaluatedArgs;\n                    const pivotCell = {\n                        type: \"HEADER\",\n                        domain: this._toPivotDomainWithPositional(dataSource, domain),\n                    };\n                    return createPivotFormula(formulaId, pivotCell);\n                }\n                return formula;\n            }\n        }\n        let builder;\n        if (functionName === \"PIVOT.VALUE\") {\n            builder = this._autofillPivotValue.bind(this);\n        } else if (functionName === \"PIVOT.HEADER\") {\n            if (evaluatedArgs.length === 1) {\n                // Total\n                if (isColumn) {\n                    // LEFT-RIGHT\n                    builder = this._autofillPivotRowHeader.bind(this);\n                } else {\n                    // UP-DOWN\n                    builder = this._autofillPivotColHeader.bind(this);\n                }\n            } else if (\n                definition.rows.map((row) => row.nameWithGranularity).includes(evaluatedArgs[1])\n            ) {\n                builder = this._autofillPivotRowHeader.bind(this);\n            } else {\n                builder = this._autofillPivotColHeader.bind(this);\n            }\n        }\n        if (builder) {\n            return builder(pivotId, evaluatedArgs, isColumn, increment, dataSource, definition);\n        }\n        return formula;\n    }\n\n    /**\n     * Compute the tooltip to display from a Pivot formula\n     *\n     * @param {string} formula Pivot formula\n     * @param {boolean} isColumn True if the direction is left/right, false\n     *                           otherwise\n     *\n     * @returns {Array<TooltipFormula>}\n     */\n    getTooltipFormula(formula, isColumn) {\n        const tokens = tokenize(formula);\n        if (getNumberOfPivotFunctions(tokens) !== 1) {\n            return [];\n        }\n        const { functionName, args } = this.getters.getFirstPivotFunction(\n            this.getters.getActiveSheetId(),\n            tokens\n        );\n        const pivotId = this.getters.getPivotId(args[0]);\n        if (!pivotId) {\n            return [{ title: _t(\"Missing pivot\"), value: _t(\"Missing pivot #%s\", args[0]) }];\n        }\n        if (functionName === \"PIVOT.VALUE\") {\n            const dataSource = this.getters.getPivot(pivotId);\n            const definition = dataSource.definition;\n            return this._tooltipFormatPivot(args, isColumn, dataSource, definition);\n        } else if (functionName === \"PIVOT.HEADER\") {\n            const dataSource = this.getters.getPivot(pivotId);\n            return this._tooltipFormatPivotHeader(args, dataSource);\n        }\n        return [];\n    }\n\n    // ---------------------------------------------------------------------\n    // Autofill\n    // ---------------------------------------------------------------------\n\n    /**\n     * Get the next value to autofill from a pivot value (\"=PIVOT.VALUE()\")\n     *\n     * Here are the possibilities:\n     * 1) LEFT-RIGHT\n     *  - Working on a date value, with one level of group by in the header\n     *      => Autofill the date, without taking care of headers\n     *  - Targeting a row-header\n     *      => Creation of a PIVOT.HEADER with the value of the current rows\n     *  - Targeting outside the pivot (before the row header and after the\n     *    last col)\n     *      => Return empty string\n     *  - Targeting a value cell\n     *      => Autofill by changing the cols\n     * 2) UP-DOWN\n     *  - Working on a date value, with one level of group by in the header\n     *      => Autofill the date, without taking care of headers\n     *  - Targeting a col-header\n     *      => Creation of a PIVOT.HEADER with the value of the current cols,\n     *         with the given increment\n     *  - Targeting outside the pivot (after the last row)\n     *      => Return empty string\n     *  - Targeting a value cell\n     *      => Autofill by changing the rows\n     *\n     * @param {string} pivotId Id of the pivot\n     * @param {Array<string>} args args of the pivot formula\n     * @param {boolean} isColumn True if the direction is left/right, false\n     *                           otherwise\n     * @param {number} increment Increment of the autofill\n     * @param {OdooPivot} dataSource\n     * @param {OdooPivotDefinition} definition\n     *\n     * @private\n     *\n     * @returns {string}\n     */\n    _autofillPivotValue(pivotId, args, isColumn, increment, dataSource, definition) {\n        const currentElement = this._getCurrentValueElement(args, definition);\n        const table = dataSource.getTableStructure();\n        const isDate = this._isGroupedOnlyByOneDate(definition, isColumn ? \"COLUMN\" : \"ROW\");\n        let cols = [];\n        let rows = [];\n        let measure;\n        if (isColumn) {\n            // LEFT-RIGHT\n            rows = currentElement.rows;\n            if (isDate && currentElement.cols.length > 1) {\n                // Date\n                const group = this._getGroupOfFirstDate(definition, \"COLUMN\");\n                cols = currentElement.cols;\n                cols[0] = this._incrementDate(cols[0], group, increment);\n                measure = cols.pop();\n            } else {\n                const currentColIndex = this._getColMeasureIndex(table, currentElement.cols);\n                if (currentColIndex === -1) {\n                    return \"\";\n                }\n                const nextColIndex = currentColIndex + increment;\n                if (nextColIndex === -1) {\n                    // Targeting row-header\n                    return this._autofillRowFromValue(pivotId, currentElement, definition);\n                }\n                if (nextColIndex < -1 || nextColIndex >= table.getNumberOfDataColumns()) {\n                    // Outside the pivot\n                    return \"\";\n                }\n                // Targeting value\n                const measureCell = this._getCellFromMeasureRowAtIndex(table, nextColIndex);\n                cols = [...measureCell.values];\n                measure = cols.pop();\n            }\n        } else {\n            // UP-DOWN\n            cols = currentElement.cols;\n            if (isDate) {\n                // Date\n                if (currentElement.rows.length === 0) {\n                    return \"\";\n                }\n                const group = this._getGroupOfFirstDate(definition, \"ROW\");\n                rows = currentElement.rows;\n                rows[0] = this._incrementDate(rows[0], group, increment);\n            } else {\n                const currentRowIndex = this._getRowIndex(table, currentElement.rows);\n                if (currentRowIndex === -1) {\n                    return \"\";\n                }\n                const nextRowIndex = currentRowIndex + increment;\n                if (nextRowIndex < 0) {\n                    // Targeting col-header\n                    return this._autofillColFromValue(\n                        pivotId,\n                        nextRowIndex,\n                        currentElement,\n                        dataSource,\n                        definition\n                    );\n                }\n                if (nextRowIndex >= table.rows.length) {\n                    // Outside the pivot\n                    return \"\";\n                }\n                // Targeting value\n                rows = [...this._getCellsFromRowAtIndex(table, nextRowIndex).values];\n            }\n            measure = cols.pop();\n        }\n        return this._createPivotFormula(pivotId, rows, cols, definition, measure);\n    }\n    /**\n     * Get the next value to autofill from a pivot header (\"=PIVOT.HEADER()\")\n     * which is a col.\n     *\n     * Here are the possibilities:\n     * 1) LEFT-RIGHT\n     *  - Working on a date value, with one level of group by in the header\n     *      => Autofill the date, without taking care of headers\n     *  - Targeting outside (before the first col after the last col)\n     *      => Return empty string\n     *  - Targeting a col-header\n     *      => Creation of a PIVOT.HEADER with the value of the new cols\n     * 2) UP-DOWN\n     *  - Working on a date value, with one level of group by in the header\n     *      => Replace the date in the headers and autocomplete as usual\n     *  - Targeting a cell (after the last col and before the last row)\n     *      => Autofill by adding the corresponding rows\n     *  - Targeting a col-header (after the first col and before the last\n     *    col)\n     *      => Creation of a PIVOT.HEADER with the value of the new cols\n     *  - Targeting outside the pivot (before the first col of after the\n     *    last row)\n     *      => Return empty string\n     *\n     * @param {string} pivotId Id of the pivot\n     * @param {Array<string>} args args of the pivot.header formula\n     * @param {boolean} isColumn True if the direction is left/right, false\n     *                           otherwise\n     * @param {number} increment Increment of the autofill\n     * @param {OdooPivot} dataSource\n     * @param {OdooPivotDefinition} definition\n     *\n     * @private\n     *\n     * @returns {string}\n     */\n    _autofillPivotColHeader(pivotId, args, isColumn, increment, dataSource, definition) {\n        /** @type {SpreadsheetPivotTable} */\n        const table = dataSource.getTableStructure();\n        const currentElement = this._getCurrentHeaderElement(args, definition);\n        const currentColIndex = this._getColMeasureIndex(table, currentElement.cols);\n        const isDate =\n            this._isGroupedOnlyByOneDate(definition, \"COLUMN\") &&\n            currentColIndex !== table.getNumberOfDataColumns() - 1;\n        if (isColumn) {\n            // LEFT-RIGHT\n            let groupValues;\n            if (isDate) {\n                // Date\n                const group = this._getGroupOfFirstDate(definition, \"COLUMN\");\n                groupValues = currentElement.cols;\n                groupValues[0] = this._incrementDate(groupValues[0], group, increment);\n            } else {\n                const rowIndex = currentElement.cols.length - 1;\n                const nextColIndex = currentColIndex + increment;\n                const nextGroup = this._getNextColCell(table, nextColIndex, rowIndex);\n                if (\n                    currentColIndex === -1 ||\n                    nextColIndex < 0 ||\n                    nextColIndex >= table.getNumberOfDataColumns() ||\n                    !nextGroup\n                ) {\n                    // Outside the pivot\n                    return \"\";\n                }\n                // Targeting a col.header\n                groupValues = nextGroup.values;\n            }\n            return this._createPivotFormula(pivotId, [], groupValues, definition);\n        } else {\n            // UP-DOWN\n            const rowIndex =\n                currentColIndex === table.getNumberOfDataColumns() - 1\n                    ? table.columns.length - 2 + currentElement.cols.length\n                    : currentElement.cols.length - 1;\n            const nextRowIndex = rowIndex + increment;\n            const groupLevels = this._getNumberOfColGroupBys(definition);\n            if (nextRowIndex < 0 || nextRowIndex >= groupLevels + 1 + table.rows.length) {\n                // Outside the pivot\n                return \"\";\n            }\n            if (nextRowIndex >= groupLevels + 1) {\n                // Targeting a value\n                const rowIndex = nextRowIndex - groupLevels - 1;\n                const measureCell = this._getCellFromMeasureRowAtIndex(table, currentColIndex);\n                const cols = [...measureCell.values];\n                const measure = cols.pop();\n                const rows = [...this._getCellsFromRowAtIndex(table, rowIndex).values];\n                return this._createPivotFormula(pivotId, rows, cols, definition, measure);\n            } else {\n                // Targeting a col.header\n                const groupValues = this._getNextColCell(\n                    table,\n                    currentColIndex,\n                    nextRowIndex\n                ).values;\n\n                return this._createPivotFormula(pivotId, [], groupValues, definition);\n            }\n        }\n    }\n    /**\n     * Get the next value to autofill from a pivot header (\"=PIVOT.HEADER()\")\n     * which is a row.\n     *\n     * Here are the possibilities:\n     * 1) LEFT-RIGHT\n     *  - Targeting outside (LEFT or after the last col)\n     *      => Return empty string\n     *  - Targeting a cell\n     *      => Autofill by adding the corresponding cols\n     * 2) UP-DOWN\n     *  - Working on a date value, with one level of group by in the header\n     *      => Autofill the date, without taking care of headers\n     *  - Targeting a row-header\n     *      => Creation of a PIVOT.HEADER with the value of the new rows\n     *  - Targeting outside the pivot (before the first row of after the\n     *    last row)\n     *      => Return empty string\n     *\n     * @param {string} pivotId Id of the pivot\n     * @param {Array<string>} args args of the pivot.header formula\n     * @param {boolean} isColumn True if the direction is left/right, false\n     *                           otherwise\n     * @param {number} increment Increment of the autofill\n     * @param {OdooPivot} dataSource\n     * @param {OdooPivotDefinition} definition\n     *\n     * @private\n     *\n     * @returns {string}\n     */\n    _autofillPivotRowHeader(pivotId, args, isColumn, increment, dataSource, definition) {\n        const table = dataSource.getTableStructure();\n        const currentElement = this._getCurrentHeaderElement(args, definition);\n        const currentIndex = this._getRowIndex(table, currentElement.rows);\n        const isDate = this._isGroupedOnlyByOneDate(definition, \"ROW\");\n        if (isColumn) {\n            const colIndex = increment - 1;\n            // LEFT-RIGHT\n            if (colIndex < 0 || colIndex >= table.getNumberOfDataColumns()) {\n                // Outside the pivot\n                return \"\";\n            }\n            const measureCell = this._getCellFromMeasureRowAtIndex(table, colIndex);\n            const values = [...measureCell.values];\n            const measure = values.pop();\n            return this._createPivotFormula(\n                pivotId,\n                currentElement.rows,\n                values,\n                definition,\n                measure\n            );\n        } else {\n            // UP-DOWN\n            let rows;\n            if (isDate) {\n                // Date\n                const group = this._getGroupOfFirstDate(definition, \"ROW\");\n                rows = currentElement.rows;\n                rows[0] = this._incrementDate(rows[0], group, increment);\n            } else {\n                const nextIndex = currentIndex + increment;\n                if (currentIndex === -1 || nextIndex < 0 || nextIndex >= table.rows.length) {\n                    return \"\";\n                }\n                rows = [...this._getCellsFromRowAtIndex(table, nextIndex).values];\n            }\n            return this._createPivotFormula(pivotId, rows, [], definition);\n        }\n    }\n    /**\n     * Create a col header from a non-header value\n     *\n     * @param {string} pivotId Id of the pivot\n     * @param {number} nextIndex Index of the target column\n     * @param {CurrentElement} currentElement Current element (rows and cols)\n     * @param {OdooPivot} dataSource\n     * @param {OdooPivotDefinition} definition\n     *\n     * @private\n     *\n     * @returns {string}\n     */\n    _autofillColFromValue(pivotId, nextIndex, currentElement, dataSource, definition) {\n        if (nextIndex >= 0) {\n            return \"\";\n        }\n        const table = dataSource.getTableStructure();\n        const groupIndex = this._getColMeasureIndex(table, currentElement.cols);\n        if (groupIndex < 0) {\n            return \"\";\n        }\n        const isTotalCol = currentElement.cols.length === 1;\n        const headerLevels = isTotalCol\n            ? 2 // measure and 'Total'\n            : this._getNumberOfColGroupBys(definition) + 1; // Groupby levels + measure\n        const index = headerLevels + nextIndex;\n        if (index < 0) {\n            return \"\";\n        }\n        const cols = isTotalCol\n            ? currentElement.cols.slice(0, index)\n            : currentElement.cols.slice(0, index + 1);\n        return this._createPivotFormula(pivotId, [], cols, definition);\n    }\n    /**\n     * Create a row header from a value\n     *\n     * @param {string} pivotId Id of the pivot\n     * @param {CurrentElement} currentElement Current element (rows and cols)\n     * @param {OdooPivotDefinition} definition\n     *\n     * @private\n     *\n     * @returns {string}\n     */\n    _autofillRowFromValue(pivotId, currentElement, definition) {\n        const rows = currentElement.rows;\n        if (!rows) {\n            return \"\";\n        }\n        return this._createPivotFormula(pivotId, rows, [], definition);\n    }\n    /**\n     * Parse the arguments of a pivot function to find the col values and\n     * the row values of a PIVOT.HEADER function\n     *\n     * @param {Array<string>} args Args of the pivot.header formula\n     * @param {OdooPivotDefinition} definition\n     *\n     * @private\n     *\n     * @returns {CurrentElement}\n     */\n    _getCurrentHeaderElement(args, definition) {\n        const values = this._parseArgs(args.slice(1));\n        const cols = this._getFieldValues(\n            [...definition.columns.map((col) => col.nameWithGranularity), \"measure\"],\n            values\n        );\n        const rows = this._getFieldValues(\n            definition.rows.map((row) => row.nameWithGranularity),\n            values\n        );\n        return { cols, rows };\n    }\n    /**\n     * Parse the arguments of a pivot function to find the col values and\n     * the row values of a PIVOT function\n     *\n     * @param {Array<string>} args Args of the pivot formula\n     * @param {OdooPivotDefinition} definition\n     *\n     * @private\n     *\n     * @returns {CurrentElement}\n     */\n    _getCurrentValueElement(args, definition) {\n        const values = this._parseArgs(args.slice(2));\n        const cols = this._getFieldValues(\n            definition.columns.map((col) => col.nameWithGranularity),\n            values\n        );\n        cols.push(args[1]); // measure\n        const rows = this._getFieldValues(\n            definition.rows.map((row) => row.nameWithGranularity),\n            values\n        );\n        return { cols, rows };\n    }\n    /**\n     * Return the values for the fields which are present in the list of\n     * fields\n     *\n     * ex: fields: [\"create_date\"]\n     *     values: { create_date: \"01/01\", stage_id: 1 }\n     *      => [\"01/01\"]\n     *\n     * @param {Array<string>} fields List of fields\n     * @param {Object} values Association field-values\n     *\n     * @private\n     * @returns {Array<string>}\n     */\n    _getFieldValues(fields, values) {\n        return fields.filter((field) => field in values).map((field) => values[field]);\n    }\n    /**\n     * Increment a date with a given increment and interval (group)\n     *\n     * @param {string} date\n     * @param {string} group (day, week, month, ...)\n     * @param {number} increment\n     *\n     * @private\n     * @returns {string}\n     */\n    _incrementDate(date, group, increment) {\n        const adapter = pivotTimeAdapter(group);\n        const value = adapter.normalizeFunctionValue(date);\n        return adapter.increment(value, increment);\n    }\n    /**\n     * Create a structure { field: value } from the arguments of a pivot\n     * function\n     *\n     * @param {Array<string>} args\n     *\n     * @private\n     * @returns {Object}\n     */\n    _parseArgs(args) {\n        const values = {};\n        for (let i = 0; i < args.length; i += 2) {\n            values[args[i]] = args[i + 1];\n        }\n        return values;\n    }\n\n    // ---------------------------------------------------------------------\n    // Tooltips\n    // ---------------------------------------------------------------------\n\n    /**\n     * Get the tooltip for a pivot formula\n     *\n     * @param {string} pivotId Id of the pivot\n     * @param {Array<string>} args\n     * @param {boolean} isColumn True if the direction is left/right, false\n     *                           otherwise\n     * @param {OdooPivot} dataSource\n     * @param {OdooPivotDefinition} definition\n     *\n     * @private\n     *\n     * @returns {Array<TooltipFormula>}\n     */\n    _tooltipFormatPivot(args, isColumn, dataSource, definition) {\n        const tooltips = [];\n        const domain = args.slice(2);\n        for (let i = 0; i < domain.length; i += 2) {\n            if (\n                (isColumn && this._isColumnGroupBy(dataSource, definition, domain[i])) ||\n                (!isColumn && this._isRowGroupBy(dataSource, definition, domain[i]))\n            ) {\n                tooltips.push(this._tooltipHeader(dataSource, domain.slice(0, i + 2)));\n            }\n        }\n        if (definition.measures.length !== 1 && isColumn) {\n            const measure = args[1];\n            tooltips.push({\n                value: dataSource.getMeasure(measure).displayName,\n            });\n        }\n        if (!tooltips.length) {\n            tooltips.push({\n                value: _t(\"Total\"),\n            });\n        }\n        return tooltips;\n    }\n    /**\n     * Get the tooltip for a pivot header formula\n     *\n     * @param {string} pivotId\n     * @param {Array<string>} args\n     * @param {OdooPivot} dataSource\n     *\n     * @private\n     *\n     * @returns {Array<TooltipFormula>}\n     */\n    _tooltipFormatPivotHeader(args, dataSource) {\n        const tooltips = [];\n        const domain = args.slice(1).map((value) => ({ value }));\n        if (domain.length === 0) {\n            return [{ value: _t(\"Total\") }];\n        }\n\n        for (let i = 0; i < domain.length; i += 2) {\n            tooltips.push(this._tooltipHeader(dataSource, domain.slice(0, i + 2)));\n        }\n        return tooltips;\n    }\n\n    _tooltipHeader(dataSource, domain) {\n        const subDomain = dataSource.parseArgsToPivotDomain(domain);\n        if (!domainHasNoRecordAtThisPosition(subDomain)) {\n            const formattedValue = dataSource.getPivotHeaderFormattedValue(subDomain);\n            return { value: formattedValue };\n        } else {\n            return { value: \"\" };\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Helpers\n    // ---------------------------------------------------------------------\n\n    _toPivotDomainWithPositional(pivot, args) {\n        const domain = [];\n        for (let i = 0; i < args.length - 1; i += 2) {\n            const fullName = args[i];\n            const { field, isPositional } = pivot.parseGroupField(fullName);\n            domain.push({\n                field: fullName,\n                value: args[i + 1],\n                type: isPositional ? \"integer\" : field.type,\n            });\n        }\n        return domain;\n    }\n\n    /**\n     * Create a pivot formula\n     *\n     * @param {string} pivotId\n     * @param {Object} rows\n     * @param {Object} cols\n     * @param {OdooPivotDefinition} definition\n     * @param {string} [measure]\n     *\n     * @returns {string}\n     */\n    _createPivotFormula(pivotId, rows, cols, definition, measure) {\n        /** @type {PivotDomain} */\n        const domain = [];\n        for (const index in rows) {\n            const row = definition.rows[index];\n            domain.push({\n                type: row.type,\n                field: row.nameWithGranularity,\n                value: rows[index],\n            });\n        }\n        if (cols.length === 1 && definition.measures.map((m) => m.id).includes(cols[0])) {\n            domain.push({\n                type: \"char\",\n                field: \"measure\",\n                value: cols[0],\n            });\n        } else {\n            for (const index in cols) {\n                const column = definition.columns[index] || {\n                    type: \"char\",\n                    nameWithGranularity: \"measure\",\n                };\n                domain.push({\n                    type: column.type,\n                    field: column.nameWithGranularity,\n                    value: cols[index],\n                });\n            }\n        }\n        const pivotCell = {\n            type: measure ? \"VALUE\" : \"HEADER\",\n            measure,\n            domain,\n        };\n        const formulaId = this.getters.getPivotFormulaId(pivotId);\n        return createPivotFormula(formulaId, pivotCell);\n    }\n\n    /**\n     * @param {OdooPivotDefinition} definition\n     * @param {string} dimension COLUMN | ROW\n     */\n    _isGroupedOnlyByOneDate(definition, dimension) {\n        const groupBys = dimension === \"COLUMN\" ? definition.columns : definition.rows;\n        return groupBys.length === 1 && isDateOrDatetimeField(groupBys[0]);\n    }\n    /**\n     * @param {OdooPivotDefinition} definition\n     * @param {string} dimension COLUMN | ROW\n     */\n    _getGroupOfFirstDate(definition, dimension) {\n        if (!this._isGroupedOnlyByOneDate(definition, dimension)) {\n            return undefined;\n        }\n        const groupBys = dimension === \"COLUMN\" ? definition.columns : definition.rows;\n        return groupBys[0].granularity || \"month\";\n    }\n\n    /**\n     * @param {OdooPivotDefinition} definition\n     * @returns {number}\n     */\n    _getNumberOfColGroupBys(definition) {\n        return definition.columns.length;\n    }\n\n    /**\n     * @param {OdooPivot} dataSource\n     * @param {OdooPivotDefinition} definition\n     * @param {string} fieldName\n     * @returns {boolean}\n     */\n    _isColumnGroupBy(dataSource, definition, fieldName) {\n        const name = dataSource.parseGroupField(fieldName).field.name;\n        return definition.columns.map((col) => col.fieldName).includes(name);\n    }\n\n    /**\n     * @param {OdooPivot} dataSource\n     * @param {OdooPivotDefinition} definition\n     * @param {string} fieldName\n     * @returns {boolean}\n     */\n    _isRowGroupBy(dataSource, definition, fieldName) {\n        const name = dataSource.parseGroupField(fieldName).field.name;\n        return definition.rows.map((row) => row.fieldName).includes(name);\n    }\n\n    _getColMeasureIndex(table, values) {\n        const vals = JSON.stringify(values);\n        const maxLength = Math.max(...table.columns.map((col) => col.length));\n        for (let i = 0; i < maxLength; i++) {\n            const cellValues = table.columns.map((col) => JSON.stringify((col[i] || {}).values));\n            if (cellValues.includes(vals)) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    _getNextColCell(table, colIndex, rowIndex) {\n        return table.columns[rowIndex][colIndex];\n    }\n\n    _getRowIndex(table, values) {\n        const vals = JSON.stringify(values);\n        return table.rows.findIndex(\n            (cell) => JSON.stringify(cell.values.map((val) => val.toString())) === vals\n        );\n    }\n\n    _getCellFromMeasureRowAtIndex(table, index) {\n        return table.columns.at(-1)[index];\n    }\n\n    _getCellsFromRowAtIndex(table, index) {\n        return table.rows[index];\n    }\n}\n\nPivotAutofillPlugin.getters = [\"getPivotNextAutofillValue\", \"getTooltipFormula\"];\n", "import { components } from \"@odoo/o-spreadsheet\";\nimport { ODOO_AGGREGATORS } from \"@spreadsheet/pivot/pivot_helpers\";\nconst { PivotLayoutConfigurator } = components;\n\nexport class OdooPivotLayoutConfigurator extends PivotLayoutConfigurator {\n    setup() {\n        super.setup(...arguments);\n        this.AGGREGATORS = ODOO_AGGREGATORS;\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { components, helpers, stores, hooks } from \"@odoo/o-spreadsheet\";\nimport { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { OdooPivotLayoutConfigurator } from \"./odoo_pivot_layout_configurator/odoo_pivot_layout_configurator\";\nimport { SidePanelDomain } from \"../../components/side_panel_domain/side_panel_domain\";\n\nconst { Checkbox, Section, ValidationMessages, PivotTitleSection, PivotDeferUpdate } = components;\nconst { useHighlights } = hooks;\nconst { useLocalStore, PivotSidePanelStore } = stores;\nconst { getPivotHighlights } = helpers;\n\nexport class PivotDetailsSidePanel extends Component {\n    static template = \"spreadsheet_edition.PivotDetailsSidePanel\";\n    static components = {\n        ValidationMessages,\n        Checkbox,\n        Section,\n        OdooPivotLayoutConfigurator,\n        PivotDeferUpdate,\n        PivotTitleSection,\n        SidePanelDomain,\n    };\n    static props = {\n        onCloseSidePanel: Function,\n        pivotId: String,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        /**@type {PivotSidePanelStore} */\n        this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId);\n\n        const loadData = async () => {\n            await this.pivot.load();\n            this.modelDisplayName = await this.pivot.getModelLabel();\n        };\n        onWillStart(loadData);\n        onWillUpdateProps(loadData);\n        useHighlights(this);\n    }\n\n    /** @returns {import(\"@spreadsheet/pivot/odoo_pivot\").default} */\n    get pivot() {\n        return this.store.pivot;\n    }\n\n    get hasValidSortedColumn() {\n        const definition = this.pivot.definition;\n        return (\n            definition?.sortedColumn &&\n            definition.measures.find((m) => m.fieldName === definition.sortedColumn.measure)\n        );\n    }\n\n    formatSort() {\n        const sortedColumn = this.pivot.definition.sortedColumn;\n        const order = sortedColumn.order === \"asc\" ? _t(\"ascending\") : _t(\"descending\");\n        const measure = this.pivot.definition.measures.find(\n            (m) => m.fieldName === sortedColumn.measure\n        );\n        const measureDisplayName = this.pivot.getMeasure(measure.id).displayName;\n        return `${measureDisplayName} (${order})`;\n    }\n\n    /**\n     * Get the last update date, formatted\n     *\n     * @returns {string} date formatted\n     */\n    getLastUpdate() {\n        const lastUpdate = this.pivot.lastUpdate;\n        if (lastUpdate) {\n            return new Date(lastUpdate).toLocaleTimeString();\n        }\n        return _t(\"never\");\n    }\n\n    onDomainUpdate(domain) {\n        this.store.update({ domain });\n    }\n\n    get unusedPivotWarning() {\n        return _t(\"This pivot is not used\");\n    }\n\n    get deferUpdatesLabel() {\n        return _t(\"Defer updates\");\n    }\n\n    get deferUpdatesTooltip() {\n        return _t(\n            \"Changing the pivot definition requires to reload the data. It may take some time.\"\n        );\n    }\n\n    onDimensionsUpdated(definition) {\n        this.store.update(definition);\n    }\n\n    get highlights() {\n        return getPivotHighlights(this.env.model.getters, this.props.pivotId);\n    }\n\n    flipAxis() {\n        const { rows, columns } = this.store.definition;\n        this.onDimensionsUpdated({\n            rows: columns,\n            columns: rows,\n        });\n    }\n}\n", "import { stores } from \"@odoo/o-spreadsheet\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst { PivotMeasureDisplayPanelStore } = stores;\n\npatch(PivotMeasureDisplayPanelStore.prototype, {\n    get fields() {\n        try {\n            return super.fields;\n        } catch {\n            return [];\n        }\n    },\n    getPossibleValues(fieldNameWithGranularity) {\n        try {\n            return super.getPossibleValues(fieldNameWithGranularity);\n        } catch {\n            return [];\n        }\n    },\n});\n", "import { stores, helpers } from \"@odoo/o-spreadsheet\";\nimport { patch } from \"@web/core/utils/patch\";\nconst { PivotSidePanelStore } = stores;\nconst { deepEquals } = helpers;\n\npatch(PivotSidePanelStore.prototype, {\n    update(definitionUpdate) {\n        const coreDefinition = this.getters.getPivotCoreDefinition(this.pivotId);\n        const definition = {\n            ...coreDefinition,\n            ...this.draft,\n            ...definitionUpdate,\n        };\n\n        const sortedColumn = definition.sortedColumn;\n        if (sortedColumn) {\n            if (\n                !definition.measures.some((measure) => measure.name === sortedColumn.measure) ||\n                !deepEquals(definition.columns, coreDefinition.columns)\n            ) {\n                definition.sortedColumn = undefined;\n            }\n        }\n        super.update(definition);\n    },\n});\n", "/** @odoo-module */\n\nimport {\n    registries,\n    readonlyAllowedCommands,\n    invalidateEvaluationCommands,\n} from \"@odoo/o-spreadsheet\";\nimport { VersionHistorySidePanel } from \"./side_panel/version_history_side_panel\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { VersionHistoryPlugin } from \"./version_history_plugin\";\n\nregistries.topbarMenuRegistry.addChild(\"version_history\", [\"file\"], {\n    name: _t(\"See version history\"),\n    sequence: 55,\n    isVisible: (env) => env.showHistory,\n    execute: (env) => env.showHistory(),\n    icon: \"o-spreadsheet-Icon.VERSION_HISTORY\",\n});\n\nregistries.sidePanelRegistry.add(\"VersionHistory\", {\n    title: _t(\"Version History\"),\n    Body: VersionHistorySidePanel,\n});\n\nregistries.featurePluginRegistry.add(\"odooVersionHistory\", VersionHistoryPlugin);\n\nreadonlyAllowedCommands.add(\"GO_TO_REVISION\");\ninvalidateEvaluationCommands.add(\"GO_TO_REVISION\");\n", "/** @odoo-module **/\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nexport class RestoreVersionConfirmationDialog extends ConfirmationDialog {\n    static template = \"spreadsheet_edition.RestoreVersionConfirmationDialog\";\n    static props = {\n        ...ConfirmationDialog.props,\n        makeACopy: { type: Function },\n    };\n\n    async _makeACopy() {\n        return this.execButton(this.props.makeACopy);\n    }\n}\n", "/** @odoo-module */\n\nimport { components, helpers } from \"@odoo/o-spreadsheet\";\nimport { Component, useRef, useState, useEffect } from \"@odoo/owl\";\nimport { formatToLocaleString } from \"../../helpers/misc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pyToJsLocale } from \"@web/core/l10n/utils\";\n\nconst { createActions } = helpers;\n\nexport class VersionHistoryItem extends Component {\n    static template = \"spreadsheet_edition.VersionHistoryItem\";\n    static components = { Menu: components.Menu, TextInput: components.TextInput };\n    static props = {\n        active: Boolean,\n        revision: Object,\n        onActivation: { optional: true, type: Function },\n        onBlur: { optional: true, type: Function },\n        editable: { optional: true, type: Boolean },\n    };\n    setup() {\n        this.menuState = useState({\n            isOpen: false,\n            position: null,\n        });\n        this.state = useState({ editName: this.defaultName });\n        this.menuButtonRef = useRef(\"menuButton\");\n        this.itemRef = useRef(\"item\");\n\n        useEffect(() => {\n            if (this.props.active) {\n                this.itemRef.el.scrollIntoView({\n                    behavior: \"smooth\",\n                    block: \"center\",\n                    inline: \"nearest\",\n                });\n            }\n        });\n    }\n\n    get revision() {\n        return this.props.revision;\n    }\n\n    get defaultName() {\n        return (\n            this.props.revision.name || this.formatRevisionTimeStamp(this.props.revision.timestamp)\n        );\n    }\n\n    get formattedTimeStamp() {\n        return this.formatRevisionTimeStamp(this.props.revision.timestamp);\n    }\n\n    get isLatestVersion() {\n        return (\n            this.env.historyManager.getRevisions()[0].nextRevisionId ===\n            this.revision.nextRevisionId\n        );\n    }\n\n    renameRevision(newName) {\n        this.state.editName = newName;\n        if (!this.state.editName) {\n            this.state.editName = this.defaultName;\n        }\n        if (this.state.editName !== this.defaultName) {\n            this.env.historyManager.renameRevision(this.revision.id, this.state.editName);\n        }\n    }\n\n    get menuItems() {\n        const actions = [\n            {\n                name: _t(\"Make a copy\"),\n                execute: (env) => {\n                    env.historyManager.forkHistory(this.revision.id);\n                },\n                isReadonlyAllowed: true,\n            },\n            {\n                name: _t(\"Restore this version\"),\n                execute: (env) => {\n                    env.historyManager.restoreRevision(this.revision.id);\n                },\n                isReadonlyAllowed: true,\n            },\n        ];\n        if (this.props.editable) {\n            actions.unshift({\n                name: this.revision.name ? _t(\"Rename\") : _t(\"Name this version\"),\n                execute: () => {\n                    this.inputRef.el.focus();\n                },\n                isReadonlyAllowed: true,\n            });\n        }\n\n        return createActions(actions);\n    }\n\n    openMenu() {\n        this.props.onActivation(this.revision.nextRevisionId);\n        const { x, y, height, width } = this.menuButtonRef.el.getBoundingClientRect();\n        this.menuState.isOpen = true;\n        this.menuState.position = { x: x + width, y: y + height };\n    }\n\n    closeMenu() {\n        this.menuState.isOpen = false;\n        this.menuState.position = null;\n    }\n\n    formatRevisionTimeStamp(ISOdatetime) {\n        const code = pyToJsLocale(this.env.model.getters.getLocale().code);\n        return formatToLocaleString(ISOdatetime, code);\n    }\n}\n", "/** @odoo-module */\n\nimport { Component, onMounted, useRef, useState } from \"@odoo/owl\";\nimport { components } from \"@odoo/o-spreadsheet\";\nimport { VersionHistoryItem } from \"./version_history_item\";\n\nconst { Section } = components;\n\nexport class VersionHistorySidePanel extends Component {\n    static template = \"spreadsheet_edition.VersionHistory\";\n    static props = { onCloseSidePanel: Function };\n    static components = {\n        VersionHistoryItem,\n        Section,\n    };\n\n    revNbr = 50;\n\n    setup() {\n        this.containerRef = useRef(\"container\");\n\n        this.state = useState({\n            currentRevisionId: this.revisions[0]?.nextRevisionId,\n            isEditingName: false,\n            loaded: this.revNbr,\n        });\n\n        onMounted(() => {\n            this.focus();\n        });\n    }\n\n    get revisions() {\n        return this.env.historyManager.getRevisions();\n    }\n\n    get loadedRevisions() {\n        return this.revisions.slice(0, this.state.loaded);\n    }\n\n    focus() {\n        this.containerRef.el?.focus();\n    }\n\n    onRevisionClick(revisionId) {\n        this.env.model.dispatch(\"GO_TO_REVISION\", { revisionId });\n        this.state.currentRevisionId = revisionId;\n    }\n\n    onLoadMoreClicked() {\n        this.state.loaded = Math.min(this.state.loaded + this.revNbr, this.revisions.length);\n    }\n\n    onKeyDown(ev) {\n        let increment = 0;\n        switch (ev.key) {\n            case \"ArrowUp\":\n                increment = -1;\n                ev.preventDefault();\n                break;\n            case \"ArrowDown\":\n                increment = 1;\n                ev.preventDefault();\n                break;\n        }\n        if (increment) {\n            const revisions = this.loadedRevisions;\n            const currentIndex = revisions.findIndex(\n                (r) => r.nextRevisionId === this.state.currentRevisionId\n            );\n            const nextIndex = Math.max(0, Math.min(revisions.length - 1, currentIndex + increment));\n            if (nextIndex !== currentIndex) {\n                this.state.currentRevisionId = revisions[nextIndex].nextRevisionId;\n                this.env.model.dispatch(\"GO_TO_REVISION\", {\n                    revisionId: this.state.currentRevisionId,\n                });\n            }\n        }\n    }\n}\n", "/** @odoo-module */\nimport { UIPlugin } from \"@odoo/o-spreadsheet\";\n\nexport class VersionHistoryPlugin extends UIPlugin {\n    constructor(config) {\n        super(config);\n        this.session = config.session;\n    }\n\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"GO_TO_REVISION\":\n                this.session.revisions.fastForward();\n                this.session.revisions.revertTo(cmd.revisionId);\n                this.dispatch(\"START\");\n                break;\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { AbstractSpreadsheetAction } from \"@spreadsheet_edition/bundle/actions/abstract_spreadsheet_action\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { useSubEnv } from \"@odoo/owl\";\n\nexport class DashboardEditAction extends AbstractSpreadsheetAction {\n    static template = \"spreadsheet_dashboard_edition.DashboardEditAction\";\n\n    resModel = \"spreadsheet.dashboard\";\n    threadField = \"dashboard_id\";\n    notificationMessage = _t(\"New dashboard created\");\n\n    setup() {\n        super.setup();\n        useSubEnv({\n            makeCopy: this.makeCopy.bind(this),\n            onSpreadsheetShared: this.shareSpreadsheet.bind(this),\n            isDashboardPublished: () => this.data && this.data.is_published,\n            toggleDashboardPublished: this.togglePublished.bind(this),\n            isRecordReadonly: () => this.data && this.data.isReadonly,\n        });\n    }\n\n    togglePublished(is_published) {\n        this.orm.write(\"spreadsheet.dashboard\", [this.resId], {\n            is_published,\n        });\n    }\n\n    async shareSpreadsheet(data, excelExport) {\n        const url = await this.orm.call(\"spreadsheet.dashboard.share\", \"action_get_share_url\", [\n            {\n                dashboard_id: this.resId,\n                spreadsheet_data: JSON.stringify(data),\n                excel_files: excelExport.files,\n            },\n        ]);\n        return url;\n    }\n}\n\nregistry.category(\"actions\").add(\"action_edit_dashboard\", DashboardEditAction, { force: true });\n", "import { Component, onWillStart } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { dashboardActionRegistry } from \"@spreadsheet_dashboard/bundle/dashboard_action/dashboard_action\";\n\nexport class DashboardEdit extends Component {\n    static template = \"spreadsheet_dashboard_edition.DashboardEdit\";\n    static props = {\n        onClick: Function,\n        dashboardId: Number,\n    };\n    setup() {\n        this.isDashboardAdmin = false;\n        onWillStart(async () => {\n            if (this.env.debug) {\n                this.isDashboardAdmin = await user.hasGroup(\n                    \"spreadsheet_dashboard.group_dashboard_manager\"\n                );\n            }\n        });\n    }\n    onClick() {\n        return this.props.onClick(this.props.dashboardId);\n    }\n}\n\ndashboardActionRegistry.add(\"dashboard_edit\", DashboardEdit);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { registries } from \"@spreadsheet/o_spreadsheet/o_spreadsheet\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\n\nconst { topbarComponentRegistry } = registries;\n\nexport class DashboardPublish extends Component {\n    static template = \"spreadsheet_edition.DashboardPublish\";\n    static components = { CheckBox };\n    static props = {};\n\n    setup() {\n        this.state = useState({ isPublished: this.env.isDashboardPublished() });\n    }\n\n    get isReadonly() {\n        return this.env.isRecordReadonly();\n    }\n\n    toggleDashboardPublished() {\n        if (this.isReadonly) {\n            return;\n        }\n        this.state.isPublished = !this.state.isPublished;\n        this.env.toggleDashboardPublished(this.state.isPublished);\n    }\n}\n\ntopbarComponentRegistry.add(\"dashboard_publish\", {\n    component: DashboardPublish,\n    isVisible: (env) => env.isDashboardPublished,\n    sequence: 15,\n});\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { range } from \"@web/core/utils/numbers\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\n\nimport { AbstractSpreadsheetAction } from \"@spreadsheet_edition/bundle/actions/abstract_spreadsheet_action\";\n\nimport { useSubEnv } from \"@odoo/owl\";\nimport { useSpreadsheetFieldSyncExtension } from \"../field_sync_extension_hook\";\n\nexport class SpreadsheetFieldSyncAction extends AbstractSpreadsheetAction {\n    static template = \"spreadsheet_sale_management.SpreadsheetFieldSyncAction\";\n    static path = \"sale-order-spreadsheet\";\n\n    resModel = \"sale.order.spreadsheet\";\n    threadField = \"sale_order_spreadsheet_id\";\n\n    setup() {\n        super.setup();\n        this.notificationMessage = _t(\"New quote calculator created\");\n        useSubEnv({\n            makeCopy: this.makeCopy.bind(this),\n        });\n\n        useSpreadsheetFieldSyncExtension();\n    }\n\n    async execInitCallbacks() {\n        await super.execInitCallbacks();\n        /**\n         * Upon the first time we open the spreadsheet we want to resize the columns\n         * of the main list to fit all the data.\n         * The catch is we need to have the list data loaded to know the content and resize\n         * the columns accordingly.\n         */\n        if (\n            this.spreadsheetData.revisionId === \"START_REVISION\" &&\n            this.stateUpdateMessages.length === 1\n        ) {\n            const list = this.model.getters.getMainSaleOrderLineList();\n            const listDataSource = this.model.getters.getListDataSource(list.id);\n            await listDataSource.load();\n            this.model.dispatch(\"AUTORESIZE_COLUMNS\", {\n                sheetId: this.model.getters.getActiveSheetId(),\n                cols: range(0, list.columns.length),\n            });\n        }\n    }\n\n    async writeToOrder() {\n        const { commands, errors } = await this.model.getters.getFieldSyncX2ManyCommands();\n        if (errors.length) {\n            this.dialog.add(WarningDialog, {\n                title: _t(\"Unable to save\"),\n                message: errors.join(\"\\n\\n\"),\n            });\n        } else {\n            await this.orm.write(\"sale.order\", [this.orderId], {\n                order_line: commands,\n            });\n            this.env.config.historyBack();\n        }\n    }\n\n    /**\n     * @override\n     */\n    _initializeWith(data) {\n        super._initializeWith(data);\n        this.orderId = data.order_id;\n        const orderFilter = this.spreadsheetData.globalFilters?.find(\n            (filter) => filter.modelName === \"sale.order\"\n        );\n        if (orderFilter && this.orderId) {\n            orderFilter.defaultValue = [this.orderId];\n        }\n    }\n}\n\nregistry\n    .category(\"actions\")\n    .add(\"action_sale_order_spreadsheet\", SpreadsheetFieldSyncAction, { force: true });\n", "import { registries, coreTypes, stores } from \"@odoo/o-spreadsheet\";\nimport { onMounted, onWillUnmount } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sum } from \"@spreadsheet/helpers/helpers\";\nimport { addToRegistryWithCleanup } from \"@spreadsheet_edition/bundle/helpers/misc\";\n\nimport { FieldSyncCorePlugin } from \"./model/field_sync_core_plugin\";\nimport { FieldSyncUIPlugin } from \"./model/field_sync_ui_plugin\";\nimport { FieldSyncSidePanel } from \"./side_panel/field_sync_side_panel\";\nimport { FieldSyncClipboardHandler } from \"./model/field_sync_clipboard_handler\";\nimport { FieldSyncHighlightStore } from \"./field_sync_highlight_store\";\n\nconst { useStoreProvider } = stores;\nconst {\n    cellMenuRegistry,\n    clipboardHandlersRegistries,\n    corePluginRegistry,\n    featurePluginRegistry,\n    inverseCommandRegistry,\n    topbarMenuRegistry,\n    sidePanelRegistry,\n} = registries;\n\ncoreTypes.add(\"ADD_FIELD_SYNC\").add(\"DELETE_FIELD_SYNCS\");\n\n/**\n * Adds the spreadsheet field sync plugins and menus\n * and removes them when the action is left\n */\nexport function useSpreadsheetFieldSyncExtension() {\n    const stores = useStoreProvider();\n    onMounted(() => {\n        stores.instantiate(FieldSyncHighlightStore);\n    });\n    addSpreadsheetFieldSyncExtensionWithCleanUp(onWillUnmount);\n}\n\nexport function addSpreadsheetFieldSyncExtensionWithCleanUp(cleanUpHook = () => {}) {\n    // plugins\n    addToRegistryWithCleanup(\n        cleanUpHook,\n        featurePluginRegistry,\n        \"field_sync_ui_plugin\",\n        FieldSyncUIPlugin\n    );\n    addToRegistryWithCleanup(\n        cleanUpHook,\n        corePluginRegistry,\n        \"field_sync_plugin\",\n        FieldSyncCorePlugin\n    );\n\n    // menus\n    const addMenuAction = {\n        icon: \"spreadsheet_sale_management.OdooLogo\",\n        name: (env) => {\n            const position = env.model.getters.getActivePosition();\n            const fieldSync = env.model.getters.getFieldSync(position);\n            return fieldSync ? _t(\"Edit sync\") : _t(\"Sync with field\");\n        },\n        execute: (env) => {\n            const position = env.model.getters.getActivePosition();\n            const fieldSync = env.model.getters.getFieldSync(position);\n            const list = env.model.getters.getMainSaleOrderLineList();\n            const isNewlyCreate = Boolean(!fieldSync && list);\n            if (isNewlyCreate) {\n                env.model.dispatch(\"ADD_FIELD_SYNC\", {\n                    sheetId: position.sheetId,\n                    col: position.col,\n                    row: position.row,\n                    listId: list.id,\n                    indexInList: 0,\n                    fieldName: \"product_uom_qty\",\n                });\n            }\n            env.openSidePanel(\"FieldSyncSidePanel\", { isNewlyCreate });\n        },\n        sequence: 2000,\n    };\n    addToRegistryWithCleanup(cleanUpHook, cellMenuRegistry, \"add_field_sync\", addMenuAction);\n    topbarMenuRegistry.addChild(\"add_field_sync\", [\"insert\"], addMenuAction, { force: true });\n    cleanUpHook(() => {\n        const menuIndex = topbarMenuRegistry.content.insert.children.findIndex(\n            (menu) => menu.id === \"add_field_sync\"\n        );\n        topbarMenuRegistry.content.insert.children.splice(menuIndex, 1);\n    });\n\n    const deleteMenuAction = {\n        icon: \"o-spreadsheet-Icon.TRASH\",\n        isVisible: (env) => {\n            const zones = env.model.getters.getSelectedZones();\n            const sheetId = env.model.getters.getActiveSheetId();\n            return zones.some((zone) => env.model.getters.getFieldSyncs(sheetId, zone).length);\n        },\n        name: (env) => {\n            const zones = env.model.getters.getSelectedZones();\n            const sheetId = env.model.getters.getActiveSheetId();\n            const fieldSyncsCount = sum(\n                zones.map((zone) => env.model.getters.getFieldSyncs(sheetId, zone).length)\n            );\n            if (fieldSyncsCount === 1) {\n                return _t(\"Delete field syncing\");\n            }\n            return _t(\"Delete field syncing\");\n        },\n        execute: (env) => {\n            const zones = env.model.getters.getSelectedZones();\n            const sheetId = env.model.getters.getActiveSheetId();\n            for (const zone of zones) {\n                env.model.dispatch(\"DELETE_FIELD_SYNCS\", { sheetId, zone });\n            }\n        },\n        sequence: 2010,\n    };\n    addToRegistryWithCleanup(cleanUpHook, cellMenuRegistry, \"delete_field_syncs\", deleteMenuAction);\n    topbarMenuRegistry.addChild(\n        \"delete_field_syncs\",\n        [\"edit\", \"delete\"],\n        {\n            ...deleteMenuAction,\n            icon: undefined,\n        },\n        { force: true }\n    );\n    cleanUpHook(() => {\n        const editAction = topbarMenuRegistry.content.edit;\n        const deleteIndex = editAction.children.findIndex((menu) => menu.id === \"delete\");\n        const deleteFieldSyncIndex = editAction.children[deleteIndex].children.findIndex(\n            (menu) => menu.id === \"delete_field_syncs\"\n        );\n        editAction.children[deleteIndex].children.splice(deleteFieldSyncIndex, 1);\n    });\n\n    // side panel\n    addToRegistryWithCleanup(cleanUpHook, sidePanelRegistry, \"FieldSyncSidePanel\", {\n        title: _t(\"Field syncing\"),\n        Body: FieldSyncSidePanel,\n        computeState(getters, initialProps) {\n            const activePosition = getters.getActivePosition();\n            const { sheetId, col, row } = activePosition;\n            const fieldSync = getters.getFieldSync(activePosition);\n            return {\n                isOpen: !!fieldSync,\n                props: { ...initialProps, position: activePosition },\n                key: `${sheetId}-${col}-${row}`,\n            };\n        },\n    });\n\n    // clipboard\n    addToRegistryWithCleanup(\n        cleanUpHook,\n        clipboardHandlersRegistries.cellHandlers,\n        \"fieldSync\",\n        FieldSyncClipboardHandler\n    );\n\n    // misc\n    const identity = (cmd) => cmd;\n    addToRegistryWithCleanup(cleanUpHook, inverseCommandRegistry, \"ADD_FIELD_SYNC\", identity);\n    addToRegistryWithCleanup(cleanUpHook, inverseCommandRegistry, \"DELETE_FIELD_SYNCS\", identity);\n}\n", "import { astToFormula, helpers, stores } from \"@odoo/o-spreadsheet\";\nimport { getFirstListFunction } from \"@spreadsheet/list/list_helpers\";\n\nconst { positionToZone } = helpers;\n\nconst { SpreadsheetStore, HighlightStore, HoveredCellStore } = stores;\n\nexport class FieldSyncHighlightStore extends SpreadsheetStore {\n    constructor(get) {\n        super(get);\n        this.hoveredCell = get(HoveredCellStore);\n        const highlightStore = get(HighlightStore);\n        highlightStore.register(this);\n        this.onDispose(() => {\n            highlightStore.unRegister(this);\n        });\n    }\n\n    get highlights() {\n        if (this.hoveredCell.col === undefined || this.hoveredCell.row === undefined) {\n            return [];\n        }\n        const sheetId = this.getters.getActiveSheetId();\n        const fieldSync = this.getters.getFieldSync({\n            sheetId,\n            col: this.hoveredCell.col,\n            row: this.hoveredCell.row,\n        });\n        if (!fieldSync) {\n            return [];\n        }\n        const highlights = [];\n        const cells = this.getters.getCells(sheetId);\n        for (const cellId in cells) {\n            const cell = cells[cellId];\n            const cellPosition = this.getters.getCellPosition(cellId);\n            if (cell.isFormula && this.getters.isPositionVisible(cellPosition)) {\n                const listFunction = getFirstListFunction(cell.compiledFormula.tokens);\n                if (!listFunction) {\n                    continue;\n                }\n                const [listIdArg, positionArg, fieldNameArg] = listFunction.args;\n                if (!listIdArg || !positionArg || !fieldNameArg) {\n                    continue;\n                }\n                const listId = this.getters\n                    .evaluateFormula(sheetId, astToFormula(listIdArg))\n                    ?.toString();\n                const position = this.getters.evaluateFormula(sheetId, astToFormula(positionArg));\n                const fieldName = this.getters.evaluateFormula(sheetId, astToFormula(fieldNameArg));\n                if (\n                    listId === fieldSync.listId &&\n                    position - 1 === fieldSync.indexInList &&\n                    fieldName === fieldSync.fieldName\n                ) {\n                    highlights.push({\n                        zone: positionToZone(cellPosition),\n                        sheetId,\n                        color: \"#875A7B\",\n                    });\n                }\n            }\n        }\n        return highlights;\n    }\n}\n", "import { AbstractCellClipboardHandler } from \"@odoo/o-spreadsheet\";\n\nexport class FieldSyncClipboardHandler extends AbstractCellClipboardHandler {\n    copy(data) {\n        const { sheetId } = data;\n        const { rowsIndexes, columnsIndexes } = data;\n        const fieldSyncs = [];\n        for (const row of rowsIndexes) {\n            const fieldSyncsInRow = [];\n            fieldSyncs.push(fieldSyncsInRow);\n            for (const col of columnsIndexes) {\n                const position = { sheetId, col, row };\n                fieldSyncsInRow.push({\n                    fieldSync: this.getters.getFieldSync(position),\n                    position,\n                });\n            }\n        }\n        return fieldSyncs;\n    }\n\n    /**\n     * Paste the clipboard content in the given target\n     */\n    paste(target, fieldSyncs, options) {\n        if (options?.pasteOption) {\n            return;\n        }\n        const zones = target.zones;\n        const sheetId = target.sheetId;\n\n        if (!options?.isCutOperation) {\n            this.pasteFromCopy(sheetId, zones, fieldSyncs, options);\n        }\n    }\n\n    pasteZone(sheetId, col, row, fieldSyncs, clipboardOptions) {\n        for (const [r, rowCells] of fieldSyncs.entries()) {\n            for (const [c, origin] of rowCells.entries()) {\n                if (!origin.fieldSync) {\n                    continue;\n                }\n                const position = { col: col + c, row: row + r, sheetId };\n                const delta = position.row - origin.position.row;\n                this.dispatch(\"ADD_FIELD_SYNC\", {\n                    ...position,\n                    fieldName: origin.fieldSync.fieldName,\n                    listId: origin.fieldSync.listId,\n                    indexInList: origin.fieldSync.indexInList + delta,\n                });\n            }\n        }\n    }\n}\n", "import { CommandResult, helpers } from \"@odoo/o-spreadsheet\";\nimport { OdooCorePlugin } from \"@spreadsheet/plugins\";\n\nconst { positionToZone, toCartesian, toXC } = helpers;\n\nexport class FieldSyncCorePlugin extends OdooCorePlugin {\n    static getters = [\n        \"getAllFieldSyncs\",\n        \"getFieldSync\",\n        \"getFieldSyncs\",\n        \"getMainSaleOrderLineList\",\n    ];\n\n    fieldSyncs = {};\n\n    allowDispatch(cmd) {\n        switch (cmd.type) {\n            case \"ADD_FIELD_SYNC\": {\n                const fieldSync = this.getFieldSync(cmd);\n                if (\n                    fieldSync &&\n                    fieldSync.listId === cmd.listId &&\n                    fieldSync.indexInList === cmd.indexInList &&\n                    fieldSync.fieldName === cmd.fieldName\n                ) {\n                    return CommandResult.NoChanges;\n                } else if (cmd.indexInList < 0) {\n                    return CommandResult.InvalidTarget;\n                }\n                break;\n            }\n            case \"DELETE_FIELD_SYNCS\": {\n                if (this.getFieldSyncs(cmd.sheetId, cmd.zone).length === 0) {\n                    return CommandResult.NoChanges;\n                }\n                break;\n            }\n            case \"REMOVE_GLOBAL_FILTER\":\n                if (this.getters.getGlobalFilter(cmd.id)?.modelName === \"sale.order\") {\n                    return CommandResult.Readonly;\n                }\n                break;\n            case \"REMOVE_ODOO_LIST\": {\n                if (cmd.listId === this.getMainSaleOrderLineList().id) {\n                    return CommandResult.Readonly;\n                }\n                break;\n            }\n        }\n        return CommandResult.Success;\n    }\n\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"ADD_FIELD_SYNC\": {\n                const { sheetId, col, row } = cmd;\n                const fieldSync = {\n                    listId: cmd.listId,\n                    indexInList: cmd.indexInList,\n                    fieldName: cmd.fieldName,\n                };\n                this.history.update(\"fieldSyncs\", sheetId, col, row, fieldSync);\n                break;\n            }\n            case \"DELETE_FIELD_SYNCS\": {\n                const { sheetId, zone } = cmd;\n                for (let col = zone.left; col <= zone.right; col++) {\n                    for (let row = zone.top; row <= zone.bottom; row++) {\n                        this.history.update(\"fieldSyncs\", sheetId, col, row, undefined);\n                    }\n                }\n                break;\n            }\n        }\n    }\n\n    adaptRanges(applyChange) {\n        for (const [position, fieldSync] of this.getAllFieldSyncs()) {\n            const { sheetId, col, row } = position;\n            const change = applyChange(this._getFieldSyncRange(position));\n            switch (change.changeType) {\n                case \"REMOVE\":\n                    this.history.update(\"fieldSyncs\", sheetId, col, row, undefined);\n                    break;\n                case \"NONE\":\n                    break;\n                default: {\n                    const { top, left } = change.range.zone;\n                    this.history.update(\"fieldSyncs\", sheetId, col, row, undefined);\n                    this.history.update(\"fieldSyncs\", sheetId, left, top, fieldSync);\n                    break;\n                }\n            }\n        }\n    }\n\n    getAllFieldSyncs() {\n        const fieldSyncs = new Map();\n        for (const sheetId in this.fieldSyncs) {\n            for (const col in this.fieldSyncs[sheetId]) {\n                for (const row in this.fieldSyncs[sheetId][col]) {\n                    const position = { sheetId, col: parseInt(col), row: parseInt(row) };\n                    fieldSyncs.set(position, this.getFieldSync(position));\n                }\n            }\n        }\n        return fieldSyncs;\n    }\n\n    getFieldSyncs(sheetId, zone) {\n        const fieldSyncs = [];\n        for (let col = zone.left; col <= zone.right; col++) {\n            for (let row = zone.top; row <= zone.bottom; row++) {\n                const fieldSync = this.getFieldSync({ sheetId, col, row });\n                if (fieldSync) {\n                    fieldSyncs.push(fieldSync);\n                }\n            }\n        }\n        return fieldSyncs;\n    }\n\n    getFieldSync(position) {\n        const { sheetId, col, row } = position;\n        return this.fieldSyncs[sheetId]?.[col]?.[row];\n    }\n\n    getMainSaleOrderLineList() {\n        const listIds = this.getters.getListIds();\n        for (const listId of listIds) {\n            const list = this.getters.getListDefinition(listId);\n            if (list.model === \"sale.order.line\") {\n                return list;\n            }\n        }\n    }\n\n    /**\n     * @private\n     */\n    _getFieldSyncRange(position) {\n        return this.getters.getRangeFromZone(position.sheetId, positionToZone(position));\n    }\n\n    export(data) {\n        const fieldSyncs = this.getAllFieldSyncs();\n        if (fieldSyncs.size) {\n            const fieldSyncsBySheets = Object.groupBy(\n                fieldSyncs.entries(),\n                ([position, fieldSync]) => position.sheetId\n            );\n            for (const sheetId in fieldSyncsBySheets) {\n                const sheet = data.sheets.find((sheet) => sheet.id === sheetId);\n                sheet.fieldSyncs = {};\n                for (const [position, fieldSync] of fieldSyncsBySheets[sheetId]) {\n                    sheet.fieldSyncs[toXC(position.col, position.row)] = fieldSync;\n                }\n            }\n        }\n    }\n\n    import(data) {\n        for (const sheet of data.sheets) {\n            if (sheet.fieldSyncs) {\n                for (const [xc, fieldSync] of Object.entries(sheet.fieldSyncs)) {\n                    const { col, row } = toCartesian(xc);\n                    this.history.update(\"fieldSyncs\", sheet.id, col, row, fieldSync);\n                }\n            }\n        }\n    }\n}\n", "import { x2ManyCommands } from \"@web/core/orm_service\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { helpers } from \"@odoo/o-spreadsheet\";\nimport { OdooUIPlugin } from \"@spreadsheet/plugins\";\n\nconst { positionToZone } = helpers;\n\nexport class FieldSyncUIPlugin extends OdooUIPlugin {\n    static getters = [\"getFieldSyncX2ManyCommands\"];\n    static layers = [\"Triangle\"];\n\n    handle(cmd) {\n        switch (cmd.type) {\n            case \"AUTOFILL_CELL\": {\n                const sheetId = this.getters.getActiveSheetId();\n                const origin = this.getters.getFieldSync({\n                    sheetId,\n                    col: cmd.originCol,\n                    row: cmd.originRow,\n                });\n                if (origin) {\n                    const targetCol = cmd.col;\n                    const targetRow = cmd.row;\n                    const delta = targetRow - cmd.originRow;\n                    this.dispatch(\"ADD_FIELD_SYNC\", {\n                        sheetId,\n                        col: targetCol,\n                        row: targetRow,\n                        listId: origin.listId,\n                        fieldName: origin.fieldName,\n                        indexInList: origin.indexInList + delta,\n                    });\n                }\n                break;\n            }\n        }\n    }\n\n    getFieldSyncMaxPosition() {\n        const fieldSyncs = [...this.getters.getAllFieldSyncs().values()];\n        return Math.max(...fieldSyncs.map((fieldSync) => fieldSync.indexInList));\n    }\n\n    async getFieldSyncX2ManyCommands() {\n        const commands = [];\n        const errors = [];\n        const list = this.getters.getMainSaleOrderLineList();\n        const listDataSource = this.getters.getListDataSource(list.id);\n        const maxPosition = this.getFieldSyncMaxPosition();\n        if (!listDataSource.isReady() || !listDataSource.getIdFromPosition(maxPosition)) {\n            listDataSource.increaseMaxPosition(maxPosition + 1);\n            await listDataSource.load({ reload: true });\n        }\n        if (!listDataSource.isValid()) {\n            return [];\n        }\n        const duplicationErrors = this.getDuplicatedFieldSyncs();\n        if (duplicationErrors) {\n            errors.push(...duplicationErrors);\n        }\n        const fields = listDataSource.getFields();\n        const valuesPerOrderLine = {};\n        for (const [position, fieldSync] of this.getters.getAllFieldSyncs()) {\n            const { listId, indexInList, fieldName } = fieldSync;\n            const { value: orderLineId } = this.getters.getListCellValueAndFormat(\n                listId,\n                indexInList,\n                \"id\"\n            );\n            const cell = this.getters.getEvaluatedCell(position);\n            if (cell.type === \"empty\" || cell.value === \"\") {\n                continue;\n            }\n            const field = fields[fieldName];\n            if (orderLineId) {\n                const {\n                    checkType,\n                    error: typeError,\n                    castToServerValue,\n                } = this.getFieldTypeSpec(field.type);\n                if (checkType(cell)) {\n                    valuesPerOrderLine[orderLineId] ??= {};\n                    valuesPerOrderLine[orderLineId][fieldName] = castToServerValue(cell);\n                } else {\n                    const range = this.getters.getRangeFromZone(\n                        position.sheetId,\n                        positionToZone(position)\n                    );\n                    const error = _t(\n                        'The value of %(cell_reference)s (%(cell_value)s) can\\'t be used for field \"%(field_name)s\".',\n                        {\n                            cell_reference: this.getters.getRangeString(\n                                range,\n                                this.getters.getActiveSheetId()\n                            ),\n                            cell_value: cell.formattedValue,\n                            field_name: field.string,\n                        }\n                    );\n                    errors.push(error + \" \" + typeError);\n                }\n            }\n        }\n        for (const orderLineOrderId in valuesPerOrderLine) {\n            const values = valuesPerOrderLine[orderLineOrderId];\n            commands.push(x2ManyCommands.update(Number(orderLineOrderId), values));\n        }\n        return { commands, errors };\n    }\n\n    /**\n     * @private\n     */\n    getDuplicatedFieldSyncs() {\n        const errors = [];\n        const map = {};\n        for (const [position, fieldSync] of this.getters.getAllFieldSyncs()) {\n            const { listId, indexInList, fieldName } = fieldSync;\n            const key = `${listId}-${indexInList}-${fieldName}`;\n            const cell = this.getters.getEvaluatedCell(position);\n            if (cell.type !== \"empty\" && cell.value !== \"\") {\n                map[key] ??= [];\n                map[key].push(position);\n            }\n        }\n        for (const key in map) {\n            if (map[key].length > 1) {\n                const positions = map[key];\n                const ranges = positions\n                    .map((position) =>\n                        this.getters.getRangeFromZone(position.sheetId, positionToZone(position))\n                    )\n                    .map((range) =>\n                        this.getters.getRangeString(range, this.getters.getActiveSheetId())\n                    );\n                errors.push(\n                    _t(\n                        \"Multiple cells are updating the same field of the same record! Unable to determine which one to choose: %s\",\n                        ranges.join(\", \")\n                    )\n                );\n            }\n        }\n        return errors.length ? errors : undefined;\n    }\n\n    /**\n     * @private\n     */\n    getFieldTypeSpec(fieldType) {\n        switch (fieldType) {\n            case \"float\":\n            case \"monetary\":\n                return {\n                    checkType: (cell) => cell.type === \"number\",\n                    error: _t(\"It should be a number.\"),\n                    castToServerValue: (cell) => cell.value,\n                };\n            case \"many2one\":\n                return {\n                    checkType: (cell) => cell.type === \"number\" && Number.isInteger(cell.value),\n                    error: _t(\"It should be an id.\"),\n                    castToServerValue: (cell) => cell.value,\n                };\n            case \"integer\":\n                return {\n                    checkType: (cell) => cell.type === \"number\" && Number.isInteger(cell.value),\n                    error: _t(\"It should be an integer.\"),\n                    castToServerValue: (cell) => cell.value,\n                };\n            case \"boolean\":\n                return {\n                    checkType: (cell) => cell.type === \"boolean\",\n                    error: _t(\"It should be a boolean.\"),\n                    castToServerValue: (cell) => cell.value,\n                };\n            case \"char\":\n            case \"text\":\n                return {\n                    checkType: (cell) => true,\n                    error: \"\",\n                    castToServerValue: (cell) => cell.formattedValue,\n                };\n        }\n    }\n\n    drawLayer({ ctx }, layer) {\n        const activeSheetId = this.getters.getActiveSheetId();\n        for (const [{ col, row, sheetId }] of this.getters.getAllFieldSyncs()) {\n            if (sheetId !== activeSheetId) {\n                continue;\n            }\n            const zone = this.getters.expandZone(activeSheetId, positionToZone({ col, row }));\n            if (zone.left !== col || zone.top !== row) {\n                continue;\n            }\n            const { x, y, width } = this.getters.getVisibleRect(zone);\n            ctx.fillStyle = \"#6C4E65\";\n            ctx.beginPath();\n\n            ctx.moveTo(x + width - 5, y);\n            ctx.lineTo(x + width, y);\n            ctx.lineTo(x + width, y + 5);\n            ctx.fill();\n        }\n    }\n}\n", "import { Component, onWillUnmount, useState } from \"@odoo/owl\";\nimport { components, helpers } from \"@odoo/o-spreadsheet\";\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport { browser } from \"@web/core/browser/browser\";\n\nconst { Section, SelectionInput } = components;\nconst { positionToZone, deepEquals } = helpers;\n\nexport class FieldSyncSidePanel extends Component {\n    static template = \"spreadsheet_sale_management.FieldSyncSidePanel\";\n    static components = { ModelFieldSelector, Section, SelectionInput };\n    static props = {\n        onCloseSidePanel: Function,\n        position: Object,\n        isNewlyCreate: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        isNewlyCreate: false,\n    };\n\n    setup() {\n        this.state = useState({\n            newPosition: undefined,\n            updateSuccessful: false,\n        });\n        this.showSaved(this.props.isNewlyCreate);\n        onWillUnmount(() => browser.clearTimeout(this.timeoutId));\n    }\n\n    getSaleOrderLineList() {\n        return this.env.model.getters.getMainSaleOrderLineList();\n    }\n\n    get fieldSyncPositionString() {\n        const position = this.state.newPosition ?? this.props.position;\n        const zone = positionToZone(position);\n        const sheetId = position.sheetId;\n        const range = this.env.model.getters.getRangeFromZone(sheetId, zone);\n        return this.env.model.getters.getRangeString(range, sheetId);\n    }\n\n    get fieldSync() {\n        return this.env.model.getters.getFieldSync(this.props.position);\n    }\n\n    /**\n     * Filter writable fields\n     */\n    filterField(field) {\n        return (\n            !field.readonly &&\n            field.name !== \"order_id\" &&\n            [\"integer\", \"float\", \"monetary\", \"char\", \"text\", \"many2one\", \"boolean\"].includes(\n                field.type\n            )\n        );\n    }\n\n    updateRecordPosition(event) {\n        this.updateFieldSync({ indexInList: parseInt(event.target.value) - 1 });\n    }\n\n    updateField(fieldName) {\n        this.updateFieldSync({ fieldName });\n    }\n\n    onRangeChanged([rangeString]) {\n        const range = this.env.model.getters.getRangeFromSheetXC(\n            this.env.model.getters.getActiveSheetId(),\n            rangeString\n        );\n        if (rangeString && !range.invalidXc) {\n            this.state.newPosition ??= {};\n            this.state.newPosition.sheetId = range.sheetId;\n            this.state.newPosition.col = range.zone.left;\n            this.state.newPosition.row = range.zone.top;\n        }\n    }\n\n    onRangeConfirmed() {\n        const newPosition = this.state.newPosition;\n        if (!newPosition || deepEquals(newPosition, this.props.position)) {\n            return;\n        }\n        this.updateFieldSync(newPosition);\n        this.env.model.dispatch(\"DELETE_FIELD_SYNCS\", {\n            sheetId: this.props.position.sheetId,\n            zone: positionToZone(this.props.position),\n        });\n        this.env.model.selection.selectCell(newPosition.col, newPosition.row);\n        this.env.openSidePanel(\"FieldSyncSidePanel\");\n    }\n\n    updateFieldSync(partialFieldSync) {\n        const { sheetId, col, row } = this.props.position;\n        const result = this.env.model.dispatch(\"ADD_FIELD_SYNC\", {\n            sheetId,\n            col,\n            row,\n            listId: this.fieldSync.listId,\n            ...this.fieldSync,\n            ...partialFieldSync,\n        });\n        this.showSaved(result.isSuccessful);\n    }\n\n    showSaved(isDisplayed) {\n        this.state.updateSuccessful = isDisplayed;\n        browser.clearTimeout(this.timeoutId);\n        this.timeoutId = browser.setTimeout(() => {\n            this.state.updateSuccessful = false;\n        }, 1500);\n    }\n}\n"], "file": "/web/assets/a63ac30/spreadsheet.o_spreadsheet.js", "sourceRoot": "../../../"}