defmodule Mead.Table do @moduledoc """ Desugars `:table` trees into plain view trees before layout. The table model is "measure globally, split locally": column tracks are fixed once for the whole table and identical on every row, so fragments produced by the paginator can never disagree about column widths. Tracks are frozen by construction rather than by a measurement pass: * a fixed track (`40`) becomes `width: 40` with `flex_grow: 0` / `flex_shrink: 0` on the cell; * a fractional track (`{:fr, n}`) becomes `flex_grow: n` with `flex_basis: 0`, so every row distributes the remaining width in the same proportions. Content-measured ("auto") tracks need a min/max-content pass and are deliberately not supported yet. Structure mapping: the table becomes a column view whose children are the header group (`repeat: true`), the body rows (bodies are dissolved; rows are atomic `wrap: false` row-views of cells), and the footer group (`repeat_after: true`). Groups are atomic. The paginator's generic repeat/repeat_after machinery then provides the WeasyPrint header/footer repetition rules (see TESTING.md). """ alias Mead.Node alias Mead.Style @default_track {:fr, 1} @doc "Recursively desugars every `:table` in the tree; other nodes pass through." @spec desugar(Node.t()) :: Node.t() def desugar(%Node{kind: :table} = table) do tracks = table.columns || [] children = Enum.flat_map(table.children, fn %Node{kind: :table_header} = g -> [%{desugar_group(g, tracks) | repeat: true}] %Node{kind: :table_footer} = g -> [%{desugar_group(g, tracks) | repeat_after: true}] %Node{kind: :table_body} = g -> Enum.map(g.children, &desugar_row(&1, tracks)) %Node{kind: :table_row} = row -> [desugar_row(row, tracks)] %Node{kind: kind} -> raise ArgumentError, "a table may not contain #{inspect(kind)} children" end) %{table | kind: :view, columns: nil, children: children} end def desugar(%Node{kind: kind}) when kind in [:table_header, :table_body, :table_footer, :table_row, :table_cell] do raise ArgumentError, "#{inspect(kind)} is only valid inside a table" end def desugar(%Node{children: []} = node), do: node def desugar(%Node{children: children} = node), do: %{node | children: Enum.map(children, &desugar/1)} defp desugar_group(%Node{children: rows} = group, tracks) do %{group | kind: :view, wrap: false, children: Enum.map(rows, &desugar_row(&1, tracks))} end defp desugar_row(%Node{kind: :table_row, children: cells} = row, tracks) do cells = cells |> Enum.with_index() |> Enum.map(fn {cell, index} -> desugar_cell(cell, Enum.at(tracks, index, @default_track)) end) %{row | kind: :view, style: %{row.style | flex_direction: :row}, children: cells} end defp desugar_row(%Node{kind: kind}, _tracks) do raise ArgumentError, "table groups may only contain table rows, got #{inspect(kind)}" end defp desugar_cell(%Node{kind: :table_cell} = cell, track) do style = track_style(cell.style, track) %{cell | kind: :view, style: style, children: Enum.map(cell.children, &desugar/1)} end defp desugar_cell(%Node{kind: kind}, _track) do raise ArgumentError, "table rows may only contain table cells, got #{inspect(kind)}" end # An explicitly sized cell keeps its width as a fixed track. defp track_style(%Style{width: width} = style, _track) when is_number(width), do: %{style | flex_grow: 0, flex_shrink: 0} defp track_style(%Style{} = style, {:fr, n}), do: %{style | flex_grow: n, flex_shrink: 1, flex_basis: 0} defp track_style(%Style{} = style, fixed) when is_number(fixed), do: %{style | width: fixed, flex_grow: 0, flex_shrink: 0} end