aboutsummaryrefslogtreecommitdiffstats
path: root/components/layout_2020/table/construct.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/layout_2020/table/construct.rs')
-rw-r--r--components/layout_2020/table/construct.rs439
1 files changed, 439 insertions, 0 deletions
diff --git a/components/layout_2020/table/construct.rs b/components/layout_2020/table/construct.rs
new file mode 100644
index 00000000000..40bec8188de
--- /dev/null
+++ b/components/layout_2020/table/construct.rs
@@ -0,0 +1,439 @@
+/* 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::convert::{TryFrom, TryInto};
+
+use log::warn;
+use script_layout_interface::wrapper_traits::ThreadSafeLayoutNode;
+use style::values::specified::TextDecorationLine;
+
+use super::{Table, TableSlot, TableSlotCell, TableSlotCoordinates, TableSlotOffset};
+use crate::context::LayoutContext;
+use crate::dom::{BoxSlot, NodeExt};
+use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents, TraversalHandler};
+use crate::flow::BlockFormattingContext;
+use crate::style_ext::{DisplayGeneratingBox, DisplayLayoutInternal};
+
+/// A reference to a slot and its coordinates in the table
+#[derive(Clone, Copy, Debug)]
+struct ResolvedSlotAndLocation<'a> {
+ cell: &'a TableSlotCell,
+ coords: TableSlotCoordinates,
+}
+
+impl<'a> ResolvedSlotAndLocation<'a> {
+ 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
+ }
+}
+
+impl Table {
+ pub(crate) fn construct<'dom>(
+ context: &LayoutContext,
+ info: &NodeAndStyleInfo<impl NodeExt<'dom>>,
+ contents: NonReplacedContents,
+ propagated_text_decoration_line: TextDecorationLine,
+ ) -> Self {
+ let mut traversal = TableBuilderTraversal {
+ context,
+ _info: info,
+ propagated_text_decoration_line,
+ builder: Default::default(),
+ };
+ contents.traverse(context, info, &mut traversal);
+ traversal.builder.finish()
+ }
+
+ /// Push a new slot into the last row of this table.
+ fn push_new_slot_to_last_row(&mut self, slot: TableSlot) {
+ self.slots.last_mut().expect("Should have rows").push(slot)
+ }
+
+ /// Convenience method for get() that returns a SlotAndLocation
+ fn get_slot<'a>(&'a self, coords: TableSlotCoordinates) -> Option<&'a TableSlot> {
+ self.slots.get(coords.y)?.get(coords.x)
+ }
+
+ /// 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.
+ fn resolve_slot_at<'a>(
+ &'a self,
+ coords: TableSlotCoordinates,
+ ) -> Vec<ResolvedSlotAndLocation<'a>> {
+ let slot = self.get_slot(coords);
+ match slot {
+ Some(TableSlot::Cell(cell)) => vec![ResolvedSlotAndLocation {
+ cell: &cell,
+ coords,
+ }],
+ Some(TableSlot::Spanned(ref offsets)) => offsets
+ .iter()
+ .map(|offset| self.resolve_slot_at(coords - *offset))
+ .flatten()
+ .collect(),
+ Some(TableSlot::Empty) | None => {
+ warn!("Tried to resolve an empty or nonexistant slot!");
+ vec![]
+ },
+ }
+ }
+
+ /// 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 coords_for_slot_above =
+ TableSlotCoordinates::new(target_coords.x, self.slots.len() - 2);
+ let slots_covering_slot_above = self.resolve_slot_at(coords_for_slot_above);
+
+ let coords_of_slots_that_cover_target: Vec<_> = slots_covering_slot_above
+ .into_iter()
+ .filter(|ref slot| slot.covers_cell_at(target_coords))
+ .map(|slot| target_coords - slot.coords)
+ .collect();
+
+ if coords_of_slots_that_cover_target.is_empty() {
+ return None;
+ } else {
+ Some(TableSlot::Spanned(coords_of_slots_that_cover_target))
+ }
+ }
+}
+
+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");
+ },
+ }
+ }
+}
+
+#[derive(Default)]
+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 fn finish(self) -> Table {
+ self.table
+ }
+
+ fn current_y(&self) -> usize {
+ self.table.slots.len() - 1
+ }
+
+ fn current_x(&self) -> usize {
+ self.table.slots[self.current_y()].len()
+ }
+
+ fn current_coords(&self) -> TableSlotCoordinates {
+ TableSlotCoordinates::new(self.current_x(), self.current_y())
+ }
+
+ pub fn start_row<'builder>(&'builder mut self) {
+ self.table.slots.push(Vec::new());
+ 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();
+ 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);
+ }
+
+ /// 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::handle_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_x = self.current_x();
+ while let Some(span) = self.incoming_rowspans.get_mut(current_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.table
+ .create_spanned_slot_based_on_cell_above(self.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_x = self.current_x();
+ }
+ }
+
+ /// 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: TableSlotCell) {
+ // Make sure the incoming_rowspans table is large enough
+ // because we will be writing to it.
+ let current_x = self.current_x();
+ let colspan = cell.colspan;
+ let rowspan = cell.rowspan;
+
+ if self.incoming_rowspans.len() < current_x + colspan {
+ self.incoming_rowspans.resize(current_x + colspan, 0isize);
+ }
+
+ debug_assert_eq!(
+ self.incoming_rowspans[current_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_x] = outgoing_rowspan;
+
+ // Draw colspanned cells
+ for colspan_offset in 1..colspan {
+ let current_x_plus_colspan_offset = current_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, self.current_y());
+ match self
+ .table
+ .create_spanned_slot_based_on_cell_above(coords_of_spanned_cell)
+ {
+ Some(mut incoming_slot) => {
+ incoming_slot.push_spanned(new_offset);
+ incoming_slot
+ },
+ None => TableSlot::new_spanned(new_offset),
+ }
+ };
+ self.table.push_new_slot_to_last_row(new_slot);
+ }
+
+ debug_assert_eq!(
+ current_x + colspan,
+ self.current_x(),
+ "Must have produced `colspan` slot entries!"
+ );
+ self.create_slots_for_cells_above_with_rowspan(true);
+ }
+}
+
+struct TableBuilderTraversal<'a, Node> {
+ context: &'a LayoutContext<'a>,
+ _info: &'a NodeAndStyleInfo<Node>,
+
+ /// Propagated value for text-decoration-line, used to construct the block
+ /// contents of table cells.
+ propagated_text_decoration_line: TextDecorationLine,
+
+ /// The [`TableBuilder`] for this [`TableBuilderTraversal`]. This is separated
+ /// into another struct so that we can write unit tests against the builder.
+ builder: TableBuilder,
+}
+
+impl<'a, 'dom, Node: 'dom> TraversalHandler<'dom, Node> for TableBuilderTraversal<'a, Node>
+where
+ Node: NodeExt<'dom>,
+{
+ fn handle_text(&mut self, _info: &NodeAndStyleInfo<Node>, _text: Cow<'dom, str>) {
+ // TODO: We should collect these contents into a new table cell.
+ }
+
+ /// https://html.spec.whatwg.org/multipage/#forming-a-table
+ fn handle_element(
+ &mut self,
+ info: &NodeAndStyleInfo<Node>,
+ display: DisplayGeneratingBox,
+ contents: Contents,
+ box_slot: BoxSlot<'dom>,
+ ) {
+ match display {
+ DisplayGeneratingBox::LayoutInternal(internal) => match internal {
+ DisplayLayoutInternal::TableRowGroup => {
+ // TODO: Should we fixup `rowspan=0` to the actual resolved value and
+ // any other rowspans that have been cut short?
+ self.builder.incoming_rowspans.clear();
+ NonReplacedContents::try_from(contents).unwrap().traverse(
+ self.context,
+ info,
+ self,
+ );
+
+ // TODO: Handle style for row groups here.
+ },
+ DisplayLayoutInternal::TableRow => {
+ self.builder.start_row();
+ NonReplacedContents::try_from(contents).unwrap().traverse(
+ self.context,
+ info,
+ &mut TableRowBuilder::new(self),
+ );
+ self.builder.end_row();
+ },
+ _ => {
+ // TODO: Handle other types of unparented table content, colgroups, and captions.
+ },
+ },
+ _ => {
+ // TODO: Create an anonymous row and cell for other unwrapped content.
+ },
+ }
+
+ // We are doing this until we have actually set a Box for this `BoxSlot`.
+ ::std::mem::forget(box_slot)
+ }
+}
+
+struct TableRowBuilder<'a, 'builder, Node> {
+ table_traversal: &'builder mut TableBuilderTraversal<'a, Node>,
+}
+
+impl<'a, 'builder, Node> TableRowBuilder<'a, 'builder, Node> {
+ fn new(table_traversal: &'builder mut TableBuilderTraversal<'a, Node>) -> Self {
+ TableRowBuilder { table_traversal }
+ }
+}
+
+impl<'a, 'builder, 'dom, Node: 'dom> TraversalHandler<'dom, Node>
+ for TableRowBuilder<'a, 'builder, Node>
+where
+ Node: NodeExt<'dom>,
+{
+ fn handle_text(&mut self, _info: &NodeAndStyleInfo<Node>, _text: Cow<'dom, str>) {
+ // TODO: We should collect these contents into a new table cell.
+ }
+
+ /// https://html.spec.whatwg.org/multipage/#algorithm-for-processing-rows
+ fn handle_element(
+ &mut self,
+ info: &NodeAndStyleInfo<Node>,
+ display: DisplayGeneratingBox,
+ contents: Contents,
+ box_slot: BoxSlot<'dom>,
+ ) {
+ match display {
+ DisplayGeneratingBox::LayoutInternal(internal) => match internal {
+ DisplayLayoutInternal::TableCell => {
+ // This value will already have filtered out rowspan=0
+ // in quirks mode, so we don't have to worry about that.
+ //
+ // The HTML specification limits the parsed value of `rowspan` to
+ // 65534 and `colspan` to 1000, so we also enforce the same limits
+ // when dealing with arbitrary DOM elements (perhaps created via
+ // script).
+ let node = info.node.to_threadsafe();
+ let rowspan = std::cmp::min(node.get_rowspan() as usize, 65534);
+ let colspan = std::cmp::min(node.get_colspan() as usize, 1000);
+
+ let contents = match contents.try_into() {
+ Ok(non_replaced_contents) => {
+ BlockFormattingContext::construct(
+ self.table_traversal.context,
+ info,
+ non_replaced_contents,
+ self.table_traversal.propagated_text_decoration_line,
+ false, /* is_list_item */
+ )
+ },
+ Err(_replaced) => {
+ panic!("We don't handle this yet.");
+ },
+ };
+
+ self.table_traversal.builder.add_cell(TableSlotCell {
+ contents,
+ colspan,
+ rowspan,
+ id: 0, // This is just an id used for testing purposes.
+ });
+ },
+ _ => {
+ // TODO: Properly handle other table-like elements in the middle of a row.
+ },
+ },
+ _ => {
+ // TODO: We should collect these contents into a new table cell.
+ },
+ }
+
+ // We are doing this until we have actually set a Box for this `BoxSlot`.
+ ::std::mem::forget(box_slot)
+ }
+}