mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
LibWeb: Move table grid slots functionality to separate module
This commit is contained in:
parent
6367b6a3d0
commit
4c0816b002
Notes:
sideshowbarker
2024-07-17 06:51:10 +09:00
Author: https://github.com/axgallo Commit: https://github.com/SerenityOS/serenity/commit/4c0816b002 Pull-request: https://github.com/SerenityOS/serenity/pull/20675 Issue: https://github.com/SerenityOS/serenity/issues/19936 Reviewed-by: https://github.com/kalenikaliaksandr ✅
5 changed files with 219 additions and 143 deletions
|
@ -445,6 +445,7 @@ set(SOURCES
|
|||
Layout/SVGSVGBox.cpp
|
||||
Layout/SVGTextBox.cpp
|
||||
Layout/TableFormattingContext.cpp
|
||||
Layout/TableGrid.cpp
|
||||
Layout/TableWrapper.cpp
|
||||
Layout/TextNode.cpp
|
||||
Layout/TreeBuilder.cpp
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
|
||||
#include <LibWeb/DOM/Node.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/HTMLTableCellElement.h>
|
||||
#include <LibWeb/HTML/HTMLTableColElement.h>
|
||||
#include <LibWeb/HTML/HTMLTableRowElement.h>
|
||||
#include <LibWeb/Layout/BlockFormattingContext.h>
|
||||
#include <LibWeb/Layout/Box.h>
|
||||
#include <LibWeb/Layout/InlineFormattingContext.h>
|
||||
|
@ -43,17 +41,6 @@ TableFormattingContext::TableFormattingContext(LayoutState& state, Box const& ro
|
|||
|
||||
TableFormattingContext::~TableFormattingContext() = default;
|
||||
|
||||
static inline bool is_table_row_group(Box const& box)
|
||||
{
|
||||
auto const& display = box.display();
|
||||
return display.is_table_row_group() || display.is_table_header_group() || display.is_table_footer_group();
|
||||
}
|
||||
|
||||
static inline bool is_table_row(Box const& box)
|
||||
{
|
||||
return box.display().is_table_row();
|
||||
}
|
||||
|
||||
static inline bool is_table_column_group(Box const& box)
|
||||
{
|
||||
return box.display().is_table_column_group();
|
||||
|
@ -64,15 +51,6 @@ static inline bool is_table_column(Box const& box)
|
|||
return box.display().is_table_column();
|
||||
}
|
||||
|
||||
template<typename Matcher, typename Callback>
|
||||
static void for_each_child_box_matching(Box const& parent, Matcher matcher, Callback callback)
|
||||
{
|
||||
parent.for_each_child_of_type<Box>([&](Box const& child_box) {
|
||||
if (matcher(child_box))
|
||||
callback(child_box);
|
||||
});
|
||||
}
|
||||
|
||||
CSSPixels TableFormattingContext::run_caption_layout(LayoutMode layout_mode, CSS::CaptionSide phase)
|
||||
{
|
||||
CSSPixels caption_height = 0;
|
||||
|
@ -101,95 +79,12 @@ CSSPixels TableFormattingContext::run_caption_layout(LayoutMode layout_mode, CSS
|
|||
return caption_height;
|
||||
}
|
||||
|
||||
void TableFormattingContext::calculate_row_column_grid(Box const& box)
|
||||
{
|
||||
// Implements https://html.spec.whatwg.org/multipage/tables.html#forming-a-table
|
||||
HashMap<GridPosition, bool> grid;
|
||||
|
||||
size_t x_width = 0, y_height = 0;
|
||||
size_t x_current = 0, y_current = 0;
|
||||
size_t max_cell_x = 0, max_cell_y = 0;
|
||||
|
||||
// Implements https://html.spec.whatwg.org/multipage/tables.html#algorithm-for-processing-rows
|
||||
auto process_row = [&](auto& row) {
|
||||
if (y_height == y_current)
|
||||
y_height++;
|
||||
|
||||
x_current = 0;
|
||||
|
||||
for (auto* child = row.first_child(); child; child = child->next_sibling()) {
|
||||
if (child->display().is_table_cell()) {
|
||||
// Cells: While x_current is less than x_width and the slot with coordinate (x_current, y_current) already has a cell assigned to it, increase x_current by 1.
|
||||
while (x_current < x_width && grid.contains(GridPosition { x_current, y_current }))
|
||||
x_current++;
|
||||
|
||||
Box const* box = static_cast<Box const*>(child);
|
||||
if (x_current == x_width)
|
||||
x_width++;
|
||||
|
||||
size_t colspan = 1, rowspan = 1;
|
||||
if (box->dom_node() && is<HTML::HTMLTableCellElement>(*box->dom_node())) {
|
||||
auto const& node = static_cast<HTML::HTMLTableCellElement const&>(*box->dom_node());
|
||||
colspan = node.col_span();
|
||||
rowspan = node.row_span();
|
||||
}
|
||||
|
||||
if (x_width < x_current + colspan)
|
||||
x_width = x_current + colspan;
|
||||
if (y_height < y_current + rowspan)
|
||||
y_height = y_current + rowspan;
|
||||
|
||||
for (size_t y = y_current; y < y_current + rowspan; y++)
|
||||
for (size_t x = x_current; x < x_current + colspan; x++)
|
||||
grid.set(GridPosition { x, y }, true);
|
||||
m_cells.append(Cell { *box, x_current, y_current, colspan, rowspan });
|
||||
max_cell_x = max(x_current, max_cell_x);
|
||||
max_cell_y = max(y_current, max_cell_y);
|
||||
|
||||
x_current += colspan;
|
||||
}
|
||||
}
|
||||
|
||||
m_rows.append(Row { row });
|
||||
y_current++;
|
||||
};
|
||||
|
||||
for_each_child_box_matching(box, is_table_row_group, [&](auto& row_group_box) {
|
||||
for_each_child_box_matching(row_group_box, is_table_row, [&](auto& row_box) {
|
||||
process_row(row_box);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
});
|
||||
|
||||
for_each_child_box_matching(box, is_table_row, [&](auto& row_box) {
|
||||
process_row(row_box);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
|
||||
m_columns.resize(x_width);
|
||||
|
||||
for (auto& cell : m_cells) {
|
||||
// Clip spans to the end of the table.
|
||||
cell.row_span = min(cell.row_span, m_rows.size() - cell.row_index);
|
||||
cell.column_span = min(cell.column_span, m_columns.size() - cell.column_index);
|
||||
}
|
||||
|
||||
m_cells_by_coordinate.resize(max_cell_y + 1);
|
||||
for (auto& position_to_cell_row : m_cells_by_coordinate) {
|
||||
position_to_cell_row.resize(max_cell_x + 1);
|
||||
}
|
||||
for (auto const& cell : m_cells) {
|
||||
m_cells_by_coordinate[cell.row_index][cell.column_index] = cell;
|
||||
m_columns[cell.column_index].has_originating_cells = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TableFormattingContext::compute_constrainedness()
|
||||
{
|
||||
// Definition of constrainedness: https://www.w3.org/TR/css-tables-3/#constrainedness
|
||||
size_t column_index = 0;
|
||||
for_each_child_box_matching(table_box(), is_table_column_group, [&](auto& column_group_box) {
|
||||
for_each_child_box_matching(column_group_box, is_table_column, [&](auto& column_box) {
|
||||
TableGrid::for_each_child_box_matching(table_box(), is_table_column_group, [&](auto& column_group_box) {
|
||||
TableGrid::for_each_child_box_matching(column_group_box, is_table_column, [&](auto& column_box) {
|
||||
auto const& computed_values = column_box.computed_values();
|
||||
if (!computed_values.width().is_auto() && !computed_values.width().is_percentage()) {
|
||||
m_columns[column_index].is_constrained = true;
|
||||
|
@ -301,8 +196,8 @@ void TableFormattingContext::compute_outer_content_sizes()
|
|||
auto const& containing_block = m_state.get(*table_wrapper().containing_block());
|
||||
|
||||
size_t column_index = 0;
|
||||
for_each_child_box_matching(table_box(), is_table_column_group, [&](auto& column_group_box) {
|
||||
for_each_child_box_matching(column_group_box, is_table_column, [&](auto& column_box) {
|
||||
TableGrid::for_each_child_box_matching(table_box(), is_table_column_group, [&](auto& column_group_box) {
|
||||
TableGrid::for_each_child_box_matching(column_group_box, is_table_column, [&](auto& column_box) {
|
||||
auto const& computed_values = column_box.computed_values();
|
||||
auto min_width = computed_values.min_width().to_px(column_box, containing_block.content_width());
|
||||
auto max_width = computed_values.max_width().is_length() ? computed_values.max_width().to_px(column_box, containing_block.content_width()) : CSSPixels::max();
|
||||
|
@ -1166,7 +1061,7 @@ void TableFormattingContext::position_row_boxes()
|
|||
|
||||
CSSPixels row_group_top_offset = table_state.border_top + table_state.padding_top;
|
||||
CSSPixels row_group_left_offset = table_state.border_left + table_state.padding_left;
|
||||
for_each_child_box_matching(table_box(), is_table_row_group, [&](auto& row_group_box) {
|
||||
TableGrid::for_each_child_box_matching(table_box(), TableGrid::is_table_row_group, [&](auto& row_group_box) {
|
||||
CSSPixels row_group_height = 0.0f;
|
||||
CSSPixels row_group_width = 0.0f;
|
||||
|
||||
|
@ -1174,7 +1069,7 @@ void TableFormattingContext::position_row_boxes()
|
|||
row_group_box_state.set_content_x(row_group_left_offset);
|
||||
row_group_box_state.set_content_y(row_group_top_offset);
|
||||
|
||||
for_each_child_box_matching(row_group_box, is_table_row, [&](auto& row) {
|
||||
TableGrid::for_each_child_box_matching(row_group_box, TableGrid::is_table_row, [&](auto& row) {
|
||||
auto const& row_state = m_state.get(row);
|
||||
row_group_height += row_state.border_box_height();
|
||||
row_group_width = max(row_group_width, row_state.border_box_width());
|
||||
|
@ -1500,13 +1395,13 @@ void TableFormattingContext::BorderConflictFinder::collect_conflicting_row_group
|
|||
{
|
||||
m_row_group_elements_by_index.resize(m_context->m_rows.size());
|
||||
size_t current_row_index = 0;
|
||||
for_each_child_box_matching(m_context->table_box(), is_table_row_group, [&](auto& row_group_box) {
|
||||
TableGrid::for_each_child_box_matching(m_context->table_box(), TableGrid::is_table_row_group, [&](auto& row_group_box) {
|
||||
auto start_row_index = current_row_index;
|
||||
size_t row_count = 0;
|
||||
for_each_child_box_matching(row_group_box, is_table_row, [&](auto&) {
|
||||
TableGrid::for_each_child_box_matching(row_group_box, TableGrid::is_table_row, [&](auto&) {
|
||||
++row_count;
|
||||
});
|
||||
for_each_child_box_matching(row_group_box, is_table_row, [&](auto&) {
|
||||
TableGrid::for_each_child_box_matching(row_group_box, TableGrid::is_table_row, [&](auto&) {
|
||||
m_row_group_elements_by_index[current_row_index] = RowGroupInfo {
|
||||
.row_group = &row_group_box, .start_index = start_row_index, .row_count = row_count
|
||||
};
|
||||
|
@ -1669,6 +1564,19 @@ Vector<TableFormattingContext::ConflictingEdge> TableFormattingContext::BorderCo
|
|||
return result;
|
||||
}
|
||||
|
||||
void TableFormattingContext::finish_grid_initialization(TableGrid const& table_grid)
|
||||
{
|
||||
m_columns.resize(table_grid.column_count());
|
||||
m_cells_by_coordinate.resize(m_rows.size());
|
||||
for (auto& position_to_cell_row : m_cells_by_coordinate) {
|
||||
position_to_cell_row.resize(table_grid.column_count());
|
||||
}
|
||||
for (auto const& cell : m_cells) {
|
||||
m_cells_by_coordinate[cell.row_index][cell.column_index] = cell;
|
||||
m_columns[cell.column_index].has_originating_cells = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TableFormattingContext::run(Box const& box, LayoutMode layout_mode, AvailableSpace const& available_space)
|
||||
{
|
||||
m_available_space = available_space;
|
||||
|
@ -1676,7 +1584,7 @@ void TableFormattingContext::run(Box const& box, LayoutMode layout_mode, Availab
|
|||
auto total_captions_height = run_caption_layout(layout_mode, CSS::CaptionSide::Top);
|
||||
|
||||
// Determine the number of rows/columns the table requires.
|
||||
calculate_row_column_grid(box);
|
||||
finish_grid_initialization(TableGrid::calculate_row_column_grid(box, m_cells, m_rows));
|
||||
|
||||
border_conflict_resolution();
|
||||
|
||||
|
@ -1829,8 +1737,8 @@ template<>
|
|||
void TableFormattingContext::initialize_intrinsic_percentages_from_rows_or_columns<TableFormattingContext::Column>()
|
||||
{
|
||||
size_t column_index = 0;
|
||||
for_each_child_box_matching(table_box(), is_table_column_group, [&](auto& column_group_box) {
|
||||
for_each_child_box_matching(column_group_box, is_table_column, [&](auto& column_box) {
|
||||
TableGrid::for_each_child_box_matching(table_box(), is_table_column_group, [&](auto& column_group_box) {
|
||||
TableGrid::for_each_child_box_matching(column_group_box, is_table_column, [&](auto& column_box) {
|
||||
auto const& computed_values = column_box.computed_values();
|
||||
// Definition of percentage contribution: https://www.w3.org/TR/css-tables-3/#percentage-contribution
|
||||
auto max_width_percentage = computed_values.max_width().is_percentage() ? computed_values.max_width().percentage().value() : static_cast<double>(INFINITY);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <AK/Forward.h>
|
||||
#include <LibWeb/Layout/FormattingContext.h>
|
||||
#include <LibWeb/Layout/TableGrid.h>
|
||||
#include <LibWeb/Layout/TableWrapper.h>
|
||||
|
||||
namespace Web::Layout {
|
||||
|
@ -58,6 +59,7 @@ private:
|
|||
void border_conflict_resolution();
|
||||
CSSPixels border_spacing_horizontal() const;
|
||||
CSSPixels border_spacing_vertical() const;
|
||||
void finish_grid_initialization(TableGrid const&);
|
||||
|
||||
CSSPixels compute_columns_total_used_width() const;
|
||||
void commit_candidate_column_widths(Vector<CSSPixels> const& candidate_widths);
|
||||
|
@ -90,32 +92,8 @@ private:
|
|||
bool has_originating_cells { false };
|
||||
};
|
||||
|
||||
struct Row {
|
||||
JS::NonnullGCPtr<Box const> box;
|
||||
CSSPixels base_height { 0 };
|
||||
CSSPixels reference_height { 0 };
|
||||
CSSPixels final_height { 0 };
|
||||
CSSPixels baseline { 0 };
|
||||
CSSPixels min_size { 0 };
|
||||
CSSPixels max_size { 0 };
|
||||
bool has_intrinsic_percentage { false };
|
||||
double intrinsic_percentage { 0 };
|
||||
// Store whether the row is constrained: https://www.w3.org/TR/css-tables-3/#constrainedness
|
||||
bool is_constrained { false };
|
||||
};
|
||||
|
||||
struct Cell {
|
||||
JS::NonnullGCPtr<Box const> box;
|
||||
size_t column_index;
|
||||
size_t row_index;
|
||||
size_t column_span;
|
||||
size_t row_span;
|
||||
CSSPixels baseline { 0 };
|
||||
CSSPixels outer_min_width { 0 };
|
||||
CSSPixels outer_max_width { 0 };
|
||||
CSSPixels outer_min_height { 0 };
|
||||
CSSPixels outer_max_height { 0 };
|
||||
};
|
||||
using Cell = TableGrid::Cell;
|
||||
using Row = TableGrid::Row;
|
||||
|
||||
// Accessors to enable direction-agnostic table measurement.
|
||||
|
||||
|
|
95
Userland/Libraries/LibWeb/Layout/TableGrid.cpp
Normal file
95
Userland/Libraries/LibWeb/Layout/TableGrid.cpp
Normal file
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2023, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/HTML/HTMLTableCellElement.h>
|
||||
#include <LibWeb/Layout/TableGrid.h>
|
||||
|
||||
namespace Web::Layout {
|
||||
|
||||
TableGrid TableGrid::calculate_row_column_grid(Box const& box, Vector<Cell>& cells, Vector<Row>& rows)
|
||||
{
|
||||
// Implements https://html.spec.whatwg.org/multipage/tables.html#forming-a-table
|
||||
TableGrid table_grid;
|
||||
|
||||
size_t x_width = 0, y_height = 0;
|
||||
size_t x_current = 0, y_current = 0;
|
||||
size_t max_cell_x = 0, max_cell_y = 0;
|
||||
|
||||
// Implements https://html.spec.whatwg.org/multipage/tables.html#algorithm-for-processing-rows
|
||||
auto process_row = [&](auto& row) {
|
||||
if (y_height == y_current)
|
||||
y_height++;
|
||||
|
||||
x_current = 0;
|
||||
|
||||
for (auto* child = row.first_child(); child; child = child->next_sibling()) {
|
||||
if (child->display().is_table_cell()) {
|
||||
// Cells: While x_current is less than x_width and the slot with coordinate (x_current, y_current) already has a cell assigned to it, increase x_current by 1.
|
||||
while (x_current < x_width && table_grid.m_occupancy_grid.contains(GridPosition { x_current, y_current }))
|
||||
x_current++;
|
||||
|
||||
Box const* box = static_cast<Box const*>(child);
|
||||
if (x_current == x_width)
|
||||
x_width++;
|
||||
|
||||
size_t colspan = 1, rowspan = 1;
|
||||
if (box->dom_node() && is<HTML::HTMLTableCellElement>(*box->dom_node())) {
|
||||
auto const& node = static_cast<HTML::HTMLTableCellElement const&>(*box->dom_node());
|
||||
colspan = node.col_span();
|
||||
rowspan = node.row_span();
|
||||
}
|
||||
|
||||
if (x_width < x_current + colspan && y_current == 0)
|
||||
x_width = x_current + colspan;
|
||||
if (y_height < y_current + rowspan)
|
||||
y_height = y_current + rowspan;
|
||||
|
||||
for (size_t y = y_current; y < y_current + rowspan; y++)
|
||||
for (size_t x = x_current; x < x_current + colspan; x++)
|
||||
table_grid.m_occupancy_grid.set(GridPosition { x, y }, true);
|
||||
cells.append(Cell { *box, x_current, y_current, colspan, rowspan });
|
||||
max_cell_x = max(x_current, max_cell_x);
|
||||
max_cell_y = max(y_current, max_cell_y);
|
||||
|
||||
x_current += colspan;
|
||||
}
|
||||
}
|
||||
|
||||
rows.append(Row { row });
|
||||
y_current++;
|
||||
};
|
||||
|
||||
for_each_child_box_matching(box, is_table_row_group, [&](auto& row_group_box) {
|
||||
for_each_child_box_matching(row_group_box, is_table_row, [&](auto& row_box) {
|
||||
process_row(row_box);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
});
|
||||
|
||||
for_each_child_box_matching(box, is_table_row, [&](auto& row_box) {
|
||||
process_row(row_box);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
|
||||
table_grid.m_column_count = x_width;
|
||||
|
||||
for (auto& cell : cells) {
|
||||
// Clip spans to the end of the table.
|
||||
cell.row_span = min(cell.row_span, rows.size() - cell.row_index);
|
||||
cell.column_span = min(cell.column_span, table_grid.m_column_count - cell.column_index);
|
||||
}
|
||||
|
||||
return table_grid;
|
||||
}
|
||||
|
||||
TableGrid TableGrid::calculate_row_column_grid(Box const& box)
|
||||
{
|
||||
Vector<Cell> cells;
|
||||
Vector<Row> rows;
|
||||
return calculate_row_column_grid(box, cells, rows);
|
||||
}
|
||||
|
||||
}
|
94
Userland/Libraries/LibWeb/Layout/TableGrid.h
Normal file
94
Userland/Libraries/LibWeb/Layout/TableGrid.h
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (c) 2023, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibWeb/Layout/Box.h>
|
||||
|
||||
namespace Web::Layout {
|
||||
|
||||
class TableGrid {
|
||||
public:
|
||||
struct GridPosition {
|
||||
size_t x;
|
||||
size_t y;
|
||||
inline bool operator==(GridPosition const&) const = default;
|
||||
};
|
||||
|
||||
struct Row {
|
||||
JS::NonnullGCPtr<Box const> box;
|
||||
CSSPixels base_height { 0 };
|
||||
CSSPixels reference_height { 0 };
|
||||
CSSPixels final_height { 0 };
|
||||
CSSPixels baseline { 0 };
|
||||
CSSPixels min_size { 0 };
|
||||
CSSPixels max_size { 0 };
|
||||
bool has_intrinsic_percentage { false };
|
||||
double intrinsic_percentage { 0 };
|
||||
// Store whether the row is constrained: https://www.w3.org/TR/css-tables-3/#constrainedness
|
||||
bool is_constrained { false };
|
||||
};
|
||||
|
||||
struct Cell {
|
||||
JS::NonnullGCPtr<Box const> box;
|
||||
size_t column_index;
|
||||
size_t row_index;
|
||||
size_t column_span;
|
||||
size_t row_span;
|
||||
CSSPixels baseline { 0 };
|
||||
CSSPixels outer_min_width { 0 };
|
||||
CSSPixels outer_max_width { 0 };
|
||||
CSSPixels outer_min_height { 0 };
|
||||
CSSPixels outer_max_height { 0 };
|
||||
};
|
||||
|
||||
// Calculate and return the grid and also rows and cells as output parameters.
|
||||
static TableGrid calculate_row_column_grid(Box const&, Vector<Cell>&, Vector<Row>&);
|
||||
// Overload for callers that don't care about rows and cells (currently the layout tree builder).
|
||||
static TableGrid calculate_row_column_grid(Box const& box);
|
||||
|
||||
size_t column_count() const { return m_column_count; }
|
||||
HashMap<GridPosition, bool> const& occupancy_grid() const { return m_occupancy_grid; }
|
||||
|
||||
static bool is_table_row_group(Box const& box)
|
||||
{
|
||||
auto const& display = box.display();
|
||||
return display.is_table_row_group() || display.is_table_header_group() || display.is_table_footer_group();
|
||||
}
|
||||
|
||||
static bool is_table_row(Box const& box)
|
||||
{
|
||||
return box.display().is_table_row();
|
||||
}
|
||||
|
||||
template<typename Matcher, typename Callback>
|
||||
static void for_each_child_box_matching(Box const& parent, Matcher matcher, Callback callback)
|
||||
{
|
||||
parent.for_each_child_of_type<Box>([&](Box const& child_box) {
|
||||
if (matcher(child_box))
|
||||
callback(child_box);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
size_t m_column_count { 0 };
|
||||
HashMap<GridPosition, bool> m_occupancy_grid;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace AK {
|
||||
|
||||
template<>
|
||||
struct Traits<Web::Layout::TableGrid::GridPosition> : public GenericTraits<Web::Layout::TableGrid::GridPosition> {
|
||||
static unsigned hash(Web::Layout::TableGrid::GridPosition const& key)
|
||||
{
|
||||
return pair_int_hash(key.x, key.y);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
Loading…
Reference in a new issue