Packages

Native PDF generation for Elixir: react-pdf-style ~PDF templates, flexbox layout, and a pure-Rust render backend. No browser, no external binaries.

Current section

Files

Jump to
mead_pdf lib mead table.ex
Raw

lib/mead/table.ex

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