| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| use std::borrow::Cow; |
| use std::iter::repeat_n; |
| |
| use atomic_refcell::AtomicRef; |
| use layout_api::wrapper_traits::ThreadSafeLayoutNode; |
| use log::warn; |
| use servo_arc::Arc; |
| use style::properties::ComputedValues; |
| use style::properties::style_structs::Font; |
| use style::selector_parser::PseudoElement; |
| use style::str::char_is_whitespace; |
| |
| use super::{ |
| Table, TableCaption, TableLevelBox, TableSlot, TableSlotCell, TableSlotCoordinates, |
| TableSlotOffset, TableTrack, TableTrackGroup, TableTrackGroupType, |
| }; |
| use crate::cell::ArcRefCell; |
| use crate::context::LayoutContext; |
| use crate::dom::{BoxSlot, LayoutBox, NodeExt}; |
| use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents, TraversalHandler}; |
| use crate::flow::{BlockContainerBuilder, BlockFormattingContext}; |
| use crate::formatting_contexts::{ |
| IndependentFormattingContext, IndependentFormattingContextContents, |
| }; |
| use crate::fragment_tree::BaseFragmentInfo; |
| use crate::layout_box_base::LayoutBoxBase; |
| use crate::style_ext::{DisplayGeneratingBox, DisplayLayoutInternal}; |
| use crate::{PropagatedBoxTreeData, SharedStyle}; |
| |
| /// A reference to a slot and its coordinates in the table |
| #[derive(Debug)] |
| pub(super) struct ResolvedSlotAndLocation<'a> { |
| pub cell: AtomicRef<'a, TableSlotCell>, |
| pub coords: TableSlotCoordinates, |
| } |
| |
| impl ResolvedSlotAndLocation<'_> { |
| fn covers_cell_at(&self, coords: TableSlotCoordinates) -> bool { |
| let covered_in_x = |
| coords.x >= self.coords.x && coords.x < self.coords.x + self.cell.colspan; |
| let covered_in_y = coords.y >= self.coords.y && |
| (self.cell.rowspan == 0 || coords.y < self.coords.y + self.cell.rowspan); |
| covered_in_x && covered_in_y |
| } |
| } |
| |
| pub(crate) enum AnonymousTableContent<'dom> { |
| Text(NodeAndStyleInfo<'dom>, Cow<'dom, str>), |
| Element { |
| info: NodeAndStyleInfo<'dom>, |
| display: DisplayGeneratingBox, |
| contents: Contents, |
| box_slot: BoxSlot<'dom>, |
| }, |
| } |
| |
| impl AnonymousTableContent<'_> { |
| fn is_whitespace_only(&self) -> bool { |
| match self { |
| Self::Element { .. } => false, |
| Self::Text(_, text) => text.chars().all(char_is_whitespace), |
| } |
| } |
| |
| fn contents_are_whitespace_only(contents: &[Self]) -> bool { |
| contents.iter().all(|content| content.is_whitespace_only()) |
| } |
| } |
| |
| impl Table { |
| pub(crate) fn construct( |
| context: &LayoutContext, |
| info: &NodeAndStyleInfo, |
| grid_style: Arc<ComputedValues>, |
| contents: NonReplacedContents, |
| propagated_data: PropagatedBoxTreeData, |
| ) -> Self { |
| let mut traversal = TableBuilderTraversal::new(context, info, grid_style, propagated_data); |
| contents.traverse(context, info, &mut traversal); |
| traversal.finish() |
| } |
| |
| pub(crate) fn construct_anonymous<'dom>( |
| context: &LayoutContext, |
| parent_info: &NodeAndStyleInfo<'dom>, |
| contents: Vec<AnonymousTableContent<'dom>>, |
| propagated_data: PropagatedBoxTreeData, |
| ) -> (NodeAndStyleInfo<'dom>, IndependentFormattingContext) { |
| let table_info = parent_info |
| .with_pseudo_element(context, PseudoElement::ServoAnonymousTable) |
| .expect("Should never fail to create anonymous table info."); |
| let table_style = table_info.style.clone(); |
| let mut table_builder = |
| TableBuilderTraversal::new(context, &table_info, table_style.clone(), propagated_data); |
| |
| for content in contents { |
| match content { |
| AnonymousTableContent::Element { |
| info, |
| display, |
| contents, |
| box_slot, |
| } => { |
| table_builder.handle_element(&info, display, contents, box_slot); |
| }, |
| AnonymousTableContent::Text(..) => { |
| // This only happens if there was whitespace between our internal table elements. |
| // We only collect that whitespace in case we need to re-emit trailing whitespace |
| // after we've added our anonymous table. |
| }, |
| } |
| } |
| |
| let mut table = table_builder.finish(); |
| table.anonymous = true; |
| |
| let ifc = IndependentFormattingContext::new( |
| LayoutBoxBase::new((&table_info).into(), table_style), |
| IndependentFormattingContextContents::Table(table), |
| ); |
| |
| (table_info, ifc) |
| } |
| |
| /// Push a new slot into the last row of this table. |
| fn push_new_slot_to_last_row(&mut self, slot: TableSlot) { |
| let last_row = match self.slots.last_mut() { |
| Some(row) => row, |
| None => { |
| unreachable!("Should have some rows before calling `push_new_slot_to_last_row`") |
| }, |
| }; |
| |
| self.size.width = self.size.width.max(last_row.len() + 1); |
| last_row.push(slot); |
| } |
| |
| /// Find [`ResolvedSlotAndLocation`] of all the slots that cover the slot at the given |
| /// coordinates. This recursively resolves all of the [`TableSlotCell`]s that cover |
| /// the target and returns a [`ResolvedSlotAndLocation`] for each of them. If there is |
| /// no slot at the given coordinates or that slot is an empty space, an empty vector |
| /// is returned. |
| pub(super) fn resolve_slot_at( |
| &self, |
| coords: TableSlotCoordinates, |
| ) -> Vec<ResolvedSlotAndLocation<'_>> { |
| let slot = self.get_slot(coords); |
| match slot { |
| Some(TableSlot::Cell(cell)) => vec![ResolvedSlotAndLocation { |
| cell: cell.borrow(), |
| coords, |
| }], |
| Some(TableSlot::Spanned(offsets)) => offsets |
| .iter() |
| .flat_map(|offset| self.resolve_slot_at(coords - *offset)) |
| .collect(), |
| Some(TableSlot::Empty) | None => { |
| warn!("Tried to resolve an empty or nonexistant slot!"); |
| vec![] |
| }, |
| } |
| } |
| } |
| |
| impl TableSlot { |
| /// Merge a TableSlot::Spanned(x, y) with this (only for model errors) |
| pub fn push_spanned(&mut self, new_offset: TableSlotOffset) { |
| match *self { |
| TableSlot::Cell { .. } => { |
| panic!( |
| "Should never have a table model error with an originating cell slot overlapping a spanned slot" |
| ) |
| }, |
| TableSlot::Spanned(ref mut vec) => vec.insert(0, new_offset), |
| TableSlot::Empty => { |
| panic!("Should never have a table model error with an empty slot"); |
| }, |
| } |
| } |
| } |
| |
| pub struct TableBuilder { |
| /// The table that we are building. |
| table: Table, |
| |
| /// An incoming rowspan is a value indicating that a cell in a row above the current row, |
| /// had a rowspan value other than 1. The values in this array indicate how many more |
| /// rows the cell should span. For example, a value of 0 at an index before `current_x()` |
| /// indicates that the cell on that column will not span into the next row, and at an index |
| /// after `current_x()` it indicates that the cell will not span into the current row. |
| /// A negative value means that the cell will span all remaining rows in the row group. |
| /// |
| /// As each column in a row is processed, the values in this vector are updated for the |
| /// next row. |
| pub incoming_rowspans: Vec<isize>, |
| } |
| |
| impl TableBuilder { |
| pub(super) fn new( |
| style: Arc<ComputedValues>, |
| grid_style: Arc<ComputedValues>, |
| base_fragment_info: BaseFragmentInfo, |
| percentage_columns_allowed_for_inline_content_sizes: bool, |
| ) -> Self { |
| Self { |
| table: Table::new( |
| style, |
| grid_style, |
| base_fragment_info, |
| percentage_columns_allowed_for_inline_content_sizes, |
| ), |
| incoming_rowspans: Vec::new(), |
| } |
| } |
| |
| pub fn new_for_tests() -> Self { |
| let testing_style = |
| ComputedValues::initial_values_with_font_override(Font::initial_values()); |
| Self::new( |
| testing_style.clone(), |
| testing_style.clone(), |
| BaseFragmentInfo::anonymous(), |
| true, /* percentage_columns_allowed_for_inline_content_sizes */ |
| ) |
| } |
| |
| pub fn last_row_index_in_row_group_at_row_n(&self, n: usize) -> usize { |
| // TODO: This is just a linear search, because the idea is that there are |
| // generally less than or equal to three row groups, but if we notice a lot |
| // of web content with more, we can consider a binary search here. |
| for row_group in self.table.row_groups.iter() { |
| let row_group = row_group.borrow(); |
| if row_group.track_range.start > n { |
| return row_group.track_range.start - 1; |
| } |
| } |
| self.table.size.height - 1 |
| } |
| |
| pub fn finish(mut self) -> Table { |
| self.adjust_table_geometry_for_columns_and_colgroups(); |
| self.do_missing_cells_fixup(); |
| self.reorder_first_thead_and_tfoot(); |
| self.do_final_rowspan_calculation(); |
| self.table |
| } |
| |
| /// Do <https://drafts.csswg.org/css-tables/#missing-cells-fixup> which ensures |
| /// that every row has the same number of cells. |
| fn do_missing_cells_fixup(&mut self) { |
| for row in self.table.slots.iter_mut() { |
| row.resize_with(self.table.size.width, || TableSlot::Empty); |
| } |
| } |
| |
| /// It's possible to define more table columns via `<colgroup>` and `<col>` elements |
| /// than actually exist in the table. In that case, increase the size of the table. |
| /// |
| /// However, if the table has no row nor row group, remove the extra columns instead. |
| /// This matches WebKit, and some tests require it, but Gecko and Blink don't do it. |
| fn adjust_table_geometry_for_columns_and_colgroups(&mut self) { |
| if self.table.rows.is_empty() && self.table.row_groups.is_empty() { |
| self.table.columns.truncate(0); |
| self.table.column_groups.truncate(0); |
| } else { |
| self.table.size.width = self.table.size.width.max(self.table.columns.len()); |
| } |
| } |
| |
| /// Reorder the first `<thead>` and `<tbody>` to be the first and last row groups respectively. |
| /// This requires fixing up all row group indices. |
| /// See <https://drafts.csswg.org/css-tables/#table-header-group> and |
| /// <https://drafts.csswg.org/css-tables/#table-footer-group>. |
| fn reorder_first_thead_and_tfoot(&mut self) { |
| let mut thead_index = None; |
| let mut tfoot_index = None; |
| for (row_group_index, row_group) in self.table.row_groups.iter().enumerate() { |
| let row_group = row_group.borrow(); |
| if thead_index.is_none() && row_group.group_type == TableTrackGroupType::HeaderGroup { |
| thead_index = Some(row_group_index); |
| } |
| if tfoot_index.is_none() && row_group.group_type == TableTrackGroupType::FooterGroup { |
| tfoot_index = Some(row_group_index); |
| } |
| if thead_index.is_some() && tfoot_index.is_some() { |
| break; |
| } |
| } |
| |
| if let Some(thead_index) = thead_index { |
| self.move_row_group_to_front(thead_index) |
| } |
| |
| if let Some(mut tfoot_index) = tfoot_index { |
| // We may have moved a `<thead>` which means the original index we |
| // we found for this this <tfoot>` also needs to be updated! |
| if thead_index.unwrap_or(0) > tfoot_index { |
| tfoot_index += 1; |
| } |
| self.move_row_group_to_end(tfoot_index) |
| } |
| } |
| |
| fn regenerate_track_ranges(&mut self) { |
| // Now update all track group ranges. |
| let mut current_row_group_index = None; |
| for (row_index, row) in self.table.rows.iter().enumerate() { |
| let row = row.borrow(); |
| if current_row_group_index == row.group_index { |
| continue; |
| } |
| |
| // Finish any row group that is currently being processed. |
| if let Some(current_group_index) = current_row_group_index { |
| self.table.row_groups[current_group_index] |
| .borrow_mut() |
| .track_range |
| .end = row_index; |
| } |
| |
| // Start processing this new row group and update its starting index. |
| current_row_group_index = row.group_index; |
| if let Some(current_group_index) = current_row_group_index { |
| self.table.row_groups[current_group_index] |
| .borrow_mut() |
| .track_range |
| .start = row_index; |
| } |
| } |
| |
| // Finish the last row group. |
| if let Some(current_group_index) = current_row_group_index { |
| self.table.row_groups[current_group_index] |
| .borrow_mut() |
| .track_range |
| .end = self.table.rows.len(); |
| } |
| } |
| |
| fn move_row_group_to_front(&mut self, index_to_move: usize) { |
| // Move the group itself. |
| if index_to_move > 0 { |
| let removed_row_group = self.table.row_groups.remove(index_to_move); |
| self.table.row_groups.insert(0, removed_row_group); |
| |
| for row in self.table.rows.iter_mut() { |
| let mut row = row.borrow_mut(); |
| match row.group_index.as_mut() { |
| Some(group_index) if *group_index < index_to_move => *group_index += 1, |
| Some(group_index) if *group_index == index_to_move => *group_index = 0, |
| _ => {}, |
| } |
| } |
| } |
| |
| let row_range = self.table.row_groups[0].borrow().track_range.clone(); |
| if row_range.start > 0 { |
| // Move the slots associated with the moved group. |
| let removed_slots: Vec<Vec<TableSlot>> = self |
| .table |
| .slots |
| .splice(row_range.clone(), std::iter::empty()) |
| .collect(); |
| self.table.slots.splice(0..0, removed_slots); |
| |
| // Move the rows associated with the moved group. |
| let removed_rows: Vec<_> = self |
| .table |
| .rows |
| .splice(row_range, std::iter::empty()) |
| .collect(); |
| self.table.rows.splice(0..0, removed_rows); |
| |
| // Do this now, rather than after possibly moving a `<tfoot>` row group to the end, |
| // because moving row groups depends on an accurate `track_range` in every group. |
| self.regenerate_track_ranges(); |
| } |
| } |
| |
| fn move_row_group_to_end(&mut self, index_to_move: usize) { |
| let last_row_group_index = self.table.row_groups.len() - 1; |
| |
| // Move the group itself. |
| if index_to_move < last_row_group_index { |
| let removed_row_group = self.table.row_groups.remove(index_to_move); |
| self.table.row_groups.push(removed_row_group); |
| |
| for row in self.table.rows.iter_mut() { |
| let mut row = row.borrow_mut(); |
| match row.group_index.as_mut() { |
| Some(group_index) if *group_index > index_to_move => *group_index -= 1, |
| Some(group_index) if *group_index == index_to_move => { |
| *group_index = last_row_group_index |
| }, |
| _ => {}, |
| } |
| } |
| } |
| |
| let row_range = self.table.row_groups[last_row_group_index] |
| .borrow() |
| .track_range |
| .clone(); |
| if row_range.end < self.table.rows.len() { |
| // Move the slots associated with the moved group. |
| let removed_slots: Vec<Vec<TableSlot>> = self |
| .table |
| .slots |
| .splice(row_range.clone(), std::iter::empty()) |
| .collect(); |
| self.table.slots.extend(removed_slots); |
| |
| // Move the rows associated with the moved group. |
| let removed_rows: Vec<_> = self |
| .table |
| .rows |
| .splice(row_range, std::iter::empty()) |
| .collect(); |
| self.table.rows.extend(removed_rows); |
| |
| self.regenerate_track_ranges(); |
| } |
| } |
| |
| /// Turn all rowspan=0 rows into the real value to avoid having to make the calculation |
| /// continually during layout. In addition, make sure that there are no rowspans that extend |
| /// past the end of their row group. |
| fn do_final_rowspan_calculation(&mut self) { |
| for row_index in 0..self.table.size.height { |
| let last_row_index_in_group = self.last_row_index_in_row_group_at_row_n(row_index); |
| for cell in self.table.slots[row_index].iter_mut() { |
| if let TableSlot::Cell(cell) = cell { |
| let mut cell = cell.borrow_mut(); |
| if cell.rowspan == 1 { |
| continue; |
| } |
| let rowspan_to_end_of_group = last_row_index_in_group - row_index + 1; |
| if cell.rowspan == 0 { |
| cell.rowspan = rowspan_to_end_of_group; |
| } else { |
| cell.rowspan = cell.rowspan.min(rowspan_to_end_of_group); |
| } |
| } |
| } |
| } |
| } |
| |
| fn current_y(&self) -> Option<usize> { |
| self.table.slots.len().checked_sub(1) |
| } |
| |
| fn current_x(&self) -> Option<usize> { |
| Some(self.table.slots[self.current_y()?].len()) |
| } |
| |
| fn current_coords(&self) -> Option<TableSlotCoordinates> { |
| Some(TableSlotCoordinates::new( |
| self.current_x()?, |
| self.current_y()?, |
| )) |
| } |
| |
| pub fn start_row(&mut self) { |
| self.table.slots.push(Vec::new()); |
| self.table.size.height += 1; |
| self.create_slots_for_cells_above_with_rowspan(true); |
| } |
| |
| pub fn end_row(&mut self) { |
| // TODO: We need to insert a cell for any leftover non-table-like |
| // content in the TableRowBuilder. |
| |
| // Truncate entries that are zero at the end of [`Self::incoming_rowspans`]. This |
| // prevents padding the table with empty cells when it isn't necessary. |
| let current_x = self |
| .current_x() |
| .expect("Should have rows before calling `end_row`"); |
| for i in (current_x..self.incoming_rowspans.len()).rev() { |
| if self.incoming_rowspans[i] == 0 { |
| self.incoming_rowspans.pop(); |
| } else { |
| break; |
| } |
| } |
| |
| self.create_slots_for_cells_above_with_rowspan(false); |
| } |
| |
| /// Create a [`TableSlot::Spanned`] for the target cell at the given coordinates. If |
| /// no slots cover the target, then this returns [`None`]. Note: This does not handle |
| /// slots that cover the target using `colspan`, but instead only considers slots that |
| /// cover this slot via `rowspan`. `colspan` should be handled by appending to the |
| /// return value of this function. |
| fn create_spanned_slot_based_on_cell_above( |
| &self, |
| target_coords: TableSlotCoordinates, |
| ) -> Option<TableSlot> { |
| let y_above = self.current_y()?.checked_sub(1)?; |
| let coords_for_slot_above = TableSlotCoordinates::new(target_coords.x, y_above); |
| let slots_covering_slot_above = self.table.resolve_slot_at(coords_for_slot_above); |
| |
| let coords_of_slots_that_cover_target: Vec<_> = slots_covering_slot_above |
| .into_iter() |
| .filter(|slot| slot.covers_cell_at(target_coords)) |
| .map(|slot| target_coords - slot.coords) |
| .collect(); |
| |
| if coords_of_slots_that_cover_target.is_empty() { |
| None |
| } else { |
| Some(TableSlot::Spanned(coords_of_slots_that_cover_target)) |
| } |
| } |
| |
| /// When not in the process of filling a cell, make sure any incoming rowspans are |
| /// filled so that the next specified cell comes after them. Should have been called before |
| /// [`Self::add_cell`] |
| /// |
| /// if `stop_at_cell_opportunity` is set, this will stop at the first slot with |
| /// `incoming_rowspans` equal to zero. If not, it will insert [`TableSlot::Empty`] and |
| /// continue to look for more incoming rowspans (which should only be done once we're |
| /// finished processing the cells in a row, and after calling truncating cells with |
| /// remaining rowspan from the end of `incoming_rowspans`. |
| fn create_slots_for_cells_above_with_rowspan(&mut self, stop_at_cell_opportunity: bool) { |
| let mut current_coords = self |
| .current_coords() |
| .expect("Should have rows before calling `create_slots_for_cells_above_with_rowspan`"); |
| while let Some(span) = self.incoming_rowspans.get_mut(current_coords.x) { |
| // This column has no incoming rowspanned cells and `stop_at_zero` is true, so |
| // we should stop to process new cells defined in the current row. |
| if *span == 0 && stop_at_cell_opportunity { |
| break; |
| } |
| |
| let new_cell = if *span != 0 { |
| *span -= 1; |
| self.create_spanned_slot_based_on_cell_above(current_coords) |
| .expect( |
| "Nonzero incoming rowspan cannot occur without a cell spanning this slot", |
| ) |
| } else { |
| TableSlot::Empty |
| }; |
| |
| self.table.push_new_slot_to_last_row(new_cell); |
| current_coords.x += 1; |
| } |
| debug_assert_eq!(Some(current_coords), self.current_coords()); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#algorithm-for-processing-rows> |
| /// Push a single cell onto the slot map, handling any colspans it may have, and |
| /// setting up the outgoing rowspans. |
| pub fn add_cell(&mut self, cell: ArcRefCell<TableSlotCell>) { |
| // Make sure the incoming_rowspans table is large enough |
| // because we will be writing to it. |
| let current_coords = self |
| .current_coords() |
| .expect("Should have rows before calling `add_cell`"); |
| |
| let (colspan, rowspan) = { |
| let cell = cell.borrow(); |
| (cell.colspan, cell.rowspan) |
| }; |
| |
| if self.incoming_rowspans.len() < current_coords.x + colspan { |
| self.incoming_rowspans |
| .resize(current_coords.x + colspan, 0isize); |
| } |
| |
| debug_assert_eq!( |
| self.incoming_rowspans[current_coords.x], 0, |
| "Added a cell in a position that also had an incoming rowspan!" |
| ); |
| |
| // If `rowspan` is zero, this is automatically negative and will stay negative. |
| let outgoing_rowspan = rowspan as isize - 1; |
| self.table.push_new_slot_to_last_row(TableSlot::Cell(cell)); |
| self.incoming_rowspans[current_coords.x] = outgoing_rowspan; |
| |
| // Draw colspanned cells |
| for colspan_offset in 1..colspan { |
| let current_x_plus_colspan_offset = current_coords.x + colspan_offset; |
| let new_offset = TableSlotOffset::new(colspan_offset, 0); |
| let incoming_rowspan = &mut self.incoming_rowspans[current_x_plus_colspan_offset]; |
| let new_slot = if *incoming_rowspan == 0 { |
| *incoming_rowspan = outgoing_rowspan; |
| TableSlot::new_spanned(new_offset) |
| } else { |
| // This means we have a table model error. |
| |
| // if `incoming_rowspan` is greater than zero, a cell from above is spanning |
| // into our row, colliding with the cells we are creating via colspan. In |
| // that case, set the incoming rowspan to the highest of two possible |
| // outgoing rowspan values (the incoming rowspan minus one, OR this cell's |
| // outgoing rowspan). `spanned_slot()`` will handle filtering out |
| // inapplicable spans when it needs to. |
| // |
| // If the `incoming_rowspan` is negative we are in `rowspan=0` mode, (i.e. |
| // rowspan=infinity), so we don't have to worry about the current cell |
| // making it larger. In that case, don't change the rowspan. |
| if *incoming_rowspan > 0 { |
| *incoming_rowspan = std::cmp::max(*incoming_rowspan - 1, outgoing_rowspan); |
| } |
| |
| // This code creates a new slot in the case that there is a table model error. |
| let coords_of_spanned_cell = |
| TableSlotCoordinates::new(current_x_plus_colspan_offset, current_coords.y); |
| let mut incoming_slot = self |
| .create_spanned_slot_based_on_cell_above(coords_of_spanned_cell) |
| .expect( |
| "Nonzero incoming rowspan cannot occur without a cell spanning this slot", |
| ); |
| incoming_slot.push_spanned(new_offset); |
| incoming_slot |
| }; |
| self.table.push_new_slot_to_last_row(new_slot); |
| } |
| |
| debug_assert_eq!( |
| Some(TableSlotCoordinates::new( |
| current_coords.x + colspan, |
| current_coords.y |
| )), |
| self.current_coords(), |
| "Must have produced `colspan` slot entries!" |
| ); |
| self.create_slots_for_cells_above_with_rowspan(true); |
| } |
| } |
| |
| pub(crate) struct TableBuilderTraversal<'style, 'dom> { |
| context: &'style LayoutContext<'style>, |
| info: &'style NodeAndStyleInfo<'dom>, |
| |
| /// The value of the [`PropagatedBoxTreeData`] to use, either for the row group |
| /// if processing one or for the table itself if outside a row group. |
| current_propagated_data: PropagatedBoxTreeData, |
| |
| /// The [`TableBuilder`] for this [`TableBuilderTraversal`]. This is separated |
| /// into another struct so that we can write unit tests against the builder. |
| builder: TableBuilder, |
| |
| current_anonymous_row_content: Vec<AnonymousTableContent<'dom>>, |
| |
| /// The index of the current row group, if there is one. |
| current_row_group_index: Option<usize>, |
| } |
| |
| impl<'style, 'dom> TableBuilderTraversal<'style, 'dom> { |
| pub(crate) fn new( |
| context: &'style LayoutContext<'style>, |
| info: &'style NodeAndStyleInfo<'dom>, |
| grid_style: Arc<ComputedValues>, |
| propagated_data: PropagatedBoxTreeData, |
| ) -> Self { |
| TableBuilderTraversal { |
| context, |
| info, |
| current_propagated_data: propagated_data, |
| builder: TableBuilder::new( |
| info.style.clone(), |
| grid_style, |
| info.into(), |
| propagated_data.allow_percentage_column_in_tables, |
| ), |
| current_anonymous_row_content: Vec::new(), |
| current_row_group_index: None, |
| } |
| } |
| |
| pub(crate) fn finish(mut self) -> Table { |
| self.finish_anonymous_row_if_needed(); |
| self.builder.finish() |
| } |
| |
| fn finish_anonymous_row_if_needed(&mut self) { |
| if AnonymousTableContent::contents_are_whitespace_only(&self.current_anonymous_row_content) |
| { |
| self.current_anonymous_row_content.clear(); |
| return; |
| } |
| |
| let row_content = std::mem::take(&mut self.current_anonymous_row_content); |
| let anonymous_info = self |
| .info |
| .with_pseudo_element(self.context, PseudoElement::ServoAnonymousTableRow) |
| .expect("Should never fail to create anonymous row info."); |
| let mut row_builder = |
| TableRowBuilder::new(self, &anonymous_info, self.current_propagated_data); |
| |
| for cell_content in row_content { |
| match cell_content { |
| AnonymousTableContent::Element { |
| info, |
| display, |
| contents, |
| box_slot, |
| } => { |
| row_builder.handle_element(&info, display, contents, box_slot); |
| }, |
| AnonymousTableContent::Text(info, text) => { |
| row_builder.handle_text(&info, text); |
| }, |
| } |
| } |
| |
| row_builder.finish(); |
| |
| let style = anonymous_info.style.clone(); |
| let table_row = ArcRefCell::new(TableTrack { |
| base: LayoutBoxBase::new((&anonymous_info).into(), style.clone()), |
| group_index: self.current_row_group_index, |
| is_anonymous: true, |
| shared_background_style: SharedStyle::new(style), |
| }); |
| self.push_table_row(table_row.clone()); |
| |
| anonymous_info |
| .node |
| .box_slot() |
| .set(LayoutBox::TableLevelBox(TableLevelBox::Track(table_row))) |
| } |
| |
| fn push_table_row(&mut self, table_track: ArcRefCell<TableTrack>) { |
| self.builder.table.rows.push(table_track); |
| |
| let last_row = self.builder.table.rows.len(); |
| if let Some(index) = self.current_row_group_index { |
| let row_group = &mut self.builder.table.row_groups[index]; |
| row_group.borrow_mut().track_range.end = last_row; |
| } |
| } |
| } |
| |
| impl<'dom> TraversalHandler<'dom> for TableBuilderTraversal<'_, 'dom> { |
| fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>) { |
| self.current_anonymous_row_content |
| .push(AnonymousTableContent::Text(info.clone(), text)); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#forming-a-table> |
| fn handle_element( |
| &mut self, |
| info: &NodeAndStyleInfo<'dom>, |
| display: DisplayGeneratingBox, |
| contents: Contents, |
| box_slot: BoxSlot<'dom>, |
| ) { |
| match display { |
| DisplayGeneratingBox::LayoutInternal(internal) => match internal { |
| DisplayLayoutInternal::TableRowGroup | |
| DisplayLayoutInternal::TableFooterGroup | |
| DisplayLayoutInternal::TableHeaderGroup => { |
| self.finish_anonymous_row_if_needed(); |
| self.builder.incoming_rowspans.clear(); |
| |
| let next_row_index = self.builder.table.rows.len(); |
| let row_group = ArcRefCell::new(TableTrackGroup { |
| base: LayoutBoxBase::new(info.into(), info.style.clone()), |
| group_type: internal.into(), |
| track_range: next_row_index..next_row_index, |
| shared_background_style: SharedStyle::new(info.style.clone()), |
| }); |
| self.builder.table.row_groups.push(row_group.clone()); |
| |
| let new_row_group_index = self.builder.table.row_groups.len() - 1; |
| self.current_row_group_index = Some(new_row_group_index); |
| |
| let Contents::NonReplaced(non_replaced_contents) = contents else { |
| unreachable!("Replaced should not have a LayoutInternal display type."); |
| }; |
| non_replaced_contents.traverse(self.context, info, self); |
| self.finish_anonymous_row_if_needed(); |
| |
| self.current_row_group_index = None; |
| self.builder.incoming_rowspans.clear(); |
| |
| box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::TrackGroup( |
| row_group, |
| ))); |
| }, |
| DisplayLayoutInternal::TableRow => { |
| self.finish_anonymous_row_if_needed(); |
| |
| let context = self.context; |
| let mut row_builder = |
| TableRowBuilder::new(self, info, self.current_propagated_data); |
| |
| let Contents::NonReplaced(non_replaced_contents) = contents else { |
| unreachable!("Replaced should not have a LayoutInternal display type."); |
| }; |
| non_replaced_contents.traverse(context, info, &mut row_builder); |
| row_builder.finish(); |
| |
| let row = ArcRefCell::new(TableTrack { |
| base: LayoutBoxBase::new(info.into(), info.style.clone()), |
| group_index: self.current_row_group_index, |
| is_anonymous: false, |
| shared_background_style: SharedStyle::new(info.style.clone()), |
| }); |
| self.push_table_row(row.clone()); |
| box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::Track(row))); |
| }, |
| DisplayLayoutInternal::TableColumn => { |
| let old_box = box_slot.take_layout_box_if_undamaged(info.damage); |
| let old_column = old_box.and_then(|layout_box| match layout_box { |
| LayoutBox::TableLevelBox(TableLevelBox::Track(column)) => Some(column), |
| _ => None, |
| }); |
| let column = add_column( |
| &mut self.builder.table.columns, |
| info, |
| None, /* group_index */ |
| false, /* is_anonymous */ |
| old_column, |
| ); |
| box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::Track(column))); |
| }, |
| DisplayLayoutInternal::TableColumnGroup => { |
| let column_group_index = self.builder.table.column_groups.len(); |
| let mut column_group_builder = TableColumnGroupBuilder { |
| column_group_index, |
| columns: Vec::new(), |
| }; |
| |
| let Contents::NonReplaced(non_replaced_contents) = contents else { |
| unreachable!("Replaced should not have a LayoutInternal display type."); |
| }; |
| non_replaced_contents.traverse(self.context, info, &mut column_group_builder); |
| |
| let first_column = self.builder.table.columns.len(); |
| if column_group_builder.columns.is_empty() { |
| add_column( |
| &mut self.builder.table.columns, |
| info, |
| Some(column_group_index), |
| true, /* is_anonymous */ |
| None, |
| ); |
| } else { |
| self.builder |
| .table |
| .columns |
| .extend(column_group_builder.columns); |
| } |
| |
| let column_group = ArcRefCell::new(TableTrackGroup { |
| base: LayoutBoxBase::new(info.into(), info.style.clone()), |
| group_type: internal.into(), |
| track_range: first_column..self.builder.table.columns.len(), |
| shared_background_style: SharedStyle::new(info.style.clone()), |
| }); |
| self.builder.table.column_groups.push(column_group.clone()); |
| box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::TrackGroup( |
| column_group, |
| ))); |
| }, |
| DisplayLayoutInternal::TableCaption => { |
| let Contents::NonReplaced(non_replaced_contents) = contents else { |
| unreachable!("Replaced should not have a LayoutInternal display type."); |
| }; |
| |
| let old_box = box_slot.take_layout_box_if_undamaged(info.damage); |
| let old_caption = old_box.and_then(|layout_box| match layout_box { |
| LayoutBox::TableLevelBox(TableLevelBox::Caption(caption)) => Some(caption), |
| _ => None, |
| }); |
| |
| let caption = old_caption.unwrap_or_else(|| { |
| let contents = IndependentFormattingContextContents::Flow( |
| BlockFormattingContext::construct( |
| self.context, |
| info, |
| non_replaced_contents, |
| self.current_propagated_data, |
| false, /* is_list_item */ |
| ), |
| ); |
| let base = LayoutBoxBase::new(info.into(), info.style.clone()); |
| ArcRefCell::new(TableCaption { |
| context: IndependentFormattingContext::new(base, contents), |
| }) |
| }); |
| |
| self.builder.table.captions.push(caption.clone()); |
| box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::Caption(caption))); |
| }, |
| DisplayLayoutInternal::TableCell => { |
| self.current_anonymous_row_content |
| .push(AnonymousTableContent::Element { |
| info: info.clone(), |
| display, |
| contents, |
| box_slot, |
| }); |
| }, |
| }, |
| _ => { |
| self.current_anonymous_row_content |
| .push(AnonymousTableContent::Element { |
| info: info.clone(), |
| display, |
| contents, |
| box_slot, |
| }); |
| }, |
| } |
| } |
| } |
| |
| struct TableRowBuilder<'style, 'builder, 'dom, 'a> { |
| table_traversal: &'builder mut TableBuilderTraversal<'style, 'dom>, |
| |
| /// The [`NodeAndStyleInfo`] of this table row, which we use to |
| /// construct anonymous table cells. |
| info: &'a NodeAndStyleInfo<'dom>, |
| |
| current_anonymous_cell_content: Vec<AnonymousTableContent<'dom>>, |
| |
| /// The [`PropagatedBoxTreeData`] to use for all children of this row. |
| propagated_data: PropagatedBoxTreeData, |
| } |
| |
| impl<'style, 'builder, 'dom, 'a> TableRowBuilder<'style, 'builder, 'dom, 'a> { |
| fn new( |
| table_traversal: &'builder mut TableBuilderTraversal<'style, 'dom>, |
| info: &'a NodeAndStyleInfo<'dom>, |
| propagated_data: PropagatedBoxTreeData, |
| ) -> Self { |
| table_traversal.builder.start_row(); |
| |
| TableRowBuilder { |
| table_traversal, |
| info, |
| current_anonymous_cell_content: Vec::new(), |
| propagated_data, |
| } |
| } |
| |
| fn finish(mut self) { |
| self.finish_current_anonymous_cell_if_needed(); |
| self.table_traversal.builder.end_row(); |
| } |
| |
| fn finish_current_anonymous_cell_if_needed(&mut self) { |
| if AnonymousTableContent::contents_are_whitespace_only(&self.current_anonymous_cell_content) |
| { |
| self.current_anonymous_cell_content.clear(); |
| return; |
| } |
| |
| let context = self.table_traversal.context; |
| let anonymous_info = self |
| .info |
| .with_pseudo_element(context, PseudoElement::ServoAnonymousTableCell) |
| .expect("Should never fail to create anonymous table cell info"); |
| let propagated_data = self.propagated_data.disallowing_percentage_table_columns(); |
| let mut builder = BlockContainerBuilder::new(context, &anonymous_info, propagated_data); |
| |
| for cell_content in self.current_anonymous_cell_content.drain(..) { |
| match cell_content { |
| AnonymousTableContent::Element { |
| info, |
| display, |
| contents, |
| box_slot, |
| } => { |
| builder.handle_element(&info, display, contents, box_slot); |
| }, |
| AnonymousTableContent::Text(info, text) => { |
| builder.handle_text(&info, text); |
| }, |
| } |
| } |
| |
| let block_container = builder.finish(); |
| let new_table_cell = ArcRefCell::new(TableSlotCell { |
| base: LayoutBoxBase::new(BaseFragmentInfo::anonymous(), anonymous_info.style), |
| contents: BlockFormattingContext::from_block_container(block_container), |
| colspan: 1, |
| rowspan: 1, |
| }); |
| self.table_traversal |
| .builder |
| .add_cell(new_table_cell.clone()); |
| |
| anonymous_info |
| .node |
| .box_slot() |
| .set(LayoutBox::TableLevelBox(TableLevelBox::Cell( |
| new_table_cell, |
| ))); |
| } |
| } |
| |
| impl<'dom> TraversalHandler<'dom> for TableRowBuilder<'_, '_, 'dom, '_> { |
| fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>) { |
| self.current_anonymous_cell_content |
| .push(AnonymousTableContent::Text(info.clone(), text)); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#algorithm-for-processing-rows> |
| fn handle_element( |
| &mut self, |
| info: &NodeAndStyleInfo<'dom>, |
| display: DisplayGeneratingBox, |
| contents: Contents, |
| box_slot: BoxSlot<'dom>, |
| ) { |
| #[allow(clippy::collapsible_match)] //// TODO: Remove once the other cases are handled |
| match display { |
| DisplayGeneratingBox::LayoutInternal(internal) => match internal { |
| DisplayLayoutInternal::TableCell => { |
| self.finish_current_anonymous_cell_if_needed(); |
| |
| let old_box = box_slot.take_layout_box_if_undamaged(info.damage); |
| let old_cell = old_box.and_then(|layout_box| match layout_box { |
| LayoutBox::TableLevelBox(TableLevelBox::Cell(cell)) => Some(cell), |
| _ => None, |
| }); |
| |
| let cell = old_cell.unwrap_or_else(|| { |
| // This value will already have filtered out rowspan=0 |
| // in quirks mode, so we don't have to worry about that. |
| let (rowspan, colspan) = if info.pseudo_element_chain().is_empty() { |
| let rowspan = info.node.get_rowspan().unwrap_or(1) as usize; |
| let colspan = info.node.get_colspan().unwrap_or(1) as usize; |
| |
| // The HTML specification clamps value of `rowspan` to [0, 65534] and |
| // `colspan` to [1, 1000]. |
| assert!((1..=1000).contains(&colspan)); |
| assert!((0..=65534).contains(&rowspan)); |
| |
| (rowspan, colspan) |
| } else { |
| (1, 1) |
| }; |
| |
| let propagated_data = |
| self.propagated_data.disallowing_percentage_table_columns(); |
| let Contents::NonReplaced(non_replaced_contents) = contents else { |
| unreachable!("Replaced should not have a LayoutInternal display type."); |
| }; |
| |
| let contents = BlockFormattingContext::construct( |
| self.table_traversal.context, |
| info, |
| non_replaced_contents, |
| propagated_data, |
| false, /* is_list_item */ |
| ); |
| |
| ArcRefCell::new(TableSlotCell { |
| base: LayoutBoxBase::new(info.into(), info.style.clone()), |
| contents, |
| colspan, |
| rowspan, |
| }) |
| }); |
| |
| self.table_traversal.builder.add_cell(cell.clone()); |
| box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::Cell(cell))); |
| }, |
| _ => { |
| //// TODO: Properly handle other table-like elements in the middle of a row. |
| self.current_anonymous_cell_content |
| .push(AnonymousTableContent::Element { |
| info: info.clone(), |
| display, |
| contents, |
| box_slot, |
| }); |
| }, |
| }, |
| _ => { |
| self.current_anonymous_cell_content |
| .push(AnonymousTableContent::Element { |
| info: info.clone(), |
| display, |
| contents, |
| box_slot, |
| }); |
| }, |
| } |
| } |
| } |
| |
| struct TableColumnGroupBuilder { |
| column_group_index: usize, |
| columns: Vec<ArcRefCell<TableTrack>>, |
| } |
| |
| impl<'dom> TraversalHandler<'dom> for TableColumnGroupBuilder { |
| fn handle_text(&mut self, _info: &NodeAndStyleInfo<'dom>, _text: Cow<'dom, str>) {} |
| fn handle_element( |
| &mut self, |
| info: &NodeAndStyleInfo<'dom>, |
| display: DisplayGeneratingBox, |
| _contents: Contents, |
| box_slot: BoxSlot<'dom>, |
| ) { |
| if !matches!( |
| display, |
| DisplayGeneratingBox::LayoutInternal(DisplayLayoutInternal::TableColumn) |
| ) { |
| // The BoxSlot destructor will check to ensure that it isn't empty but in this case, the |
| // DOM node doesn't produce any box, so explicitly skip the destructor here. |
| ::std::mem::forget(box_slot); |
| return; |
| } |
| let old_box = box_slot.take_layout_box_if_undamaged(info.damage); |
| let old_column = old_box.and_then(|layout_box| match layout_box { |
| LayoutBox::TableLevelBox(TableLevelBox::Track(column)) => Some(column), |
| _ => None, |
| }); |
| let column = add_column( |
| &mut self.columns, |
| info, |
| Some(self.column_group_index), |
| false, /* is_anonymous */ |
| old_column, |
| ); |
| box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::Track(column))); |
| } |
| } |
| |
| impl From<DisplayLayoutInternal> for TableTrackGroupType { |
| fn from(value: DisplayLayoutInternal) -> Self { |
| match value { |
| DisplayLayoutInternal::TableColumnGroup => TableTrackGroupType::ColumnGroup, |
| DisplayLayoutInternal::TableFooterGroup => TableTrackGroupType::FooterGroup, |
| DisplayLayoutInternal::TableHeaderGroup => TableTrackGroupType::HeaderGroup, |
| DisplayLayoutInternal::TableRowGroup => TableTrackGroupType::RowGroup, |
| _ => unreachable!(), |
| } |
| } |
| } |
| |
| fn add_column( |
| collection: &mut Vec<ArcRefCell<TableTrack>>, |
| column_info: &NodeAndStyleInfo, |
| group_index: Option<usize>, |
| is_anonymous: bool, |
| old_column: Option<ArcRefCell<TableTrack>>, |
| ) -> ArcRefCell<TableTrack> { |
| let span = if column_info.pseudo_element_chain().is_empty() { |
| column_info.node.get_span().unwrap_or(1) |
| } else { |
| 1 |
| }; |
| |
| // The HTML specification clamps value of `span` for `<col>` to [1, 1000]. |
| assert!((1..=1000).contains(&span)); |
| |
| let column = old_column.unwrap_or_else(|| { |
| ArcRefCell::new(TableTrack { |
| base: LayoutBoxBase::new(column_info.into(), column_info.style.clone()), |
| group_index, |
| is_anonymous, |
| shared_background_style: SharedStyle::new(column_info.style.clone()), |
| }) |
| }); |
| collection.extend(repeat_n(column.clone(), span as usize)); |
| column |
| } |