defmodule IO.ANSI.Table.Column do @moduledoc """ Functions related to the columns of a table. """ use PersistConfig alias IO.ANSI.Table.{Header, Line} @ansi_escape "\e" # "\e[1;31mHello" => bold with red foreground # "\e[2;37;41mWorld => dimmed white foreground with red background @ansi_escape_codes ~r/\e\[([0-9]{1,3}(;[0-9]{1,3})*)?[m|K]/ @typedoc """ A list of 3 contiguous widths: left width, inner width and right width """ @type spread :: [width] @typedoc "Table column" @type t :: [String.t()] @typedoc "Column width" @type width :: non_neg_integer @doc ~S''' Returns a list of column widths capped by `maximum width`. ## Examples iex> alias IO.ANSI.Table.Column iex> columns = [["cat", "wombat", "elk"], ["mongoose", "ant", "gnu"]] iex> Column.widths(columns, 99) [6, 8] iex> alias IO.ANSI.Table.Column iex> columns = [["cat", "wombat", "elk"], ["mongoose", "ant", "gnu"]] iex> Column.widths(columns, 7) [6, 7] iex> alias IO.ANSI.Table.Column iex> columns = [["\e[32m\e[42mCHEETAH\e[0m", "elk"], ["mongoose", "ant"]] iex> Column.widths(columns, 99) [7, 8] iex> alias IO.ANSI.Table.Column iex> columns = [["\e[32m\e[42mCHEETAH\e[0m", "elk"], ["mongoose", "ant"]] iex> Column.widths(columns, 6) [6, 6] ''' @spec widths([t], width) :: [width] def widths(columns, max_width) do for column <- columns do Enum.map(column, &width/1) |> Enum.max() |> min(max_width) end end @doc ~S''' Spreads a `width` given an `element` and its `align attribute`. ## Examples iex> alias IO.ANSI.Table.Column iex> { ...> Column.spread(7, "name", :left ), ...> Column.spread(7, "name", :center), ...> Column.spread(7, "name", :right ) ...> } {[0, 4, 3], [1, 4, 2], [3, 4, 0]} iex> alias IO.ANSI.Table.Column iex> { ...> Column.spread(3, "name", :left ), ...> Column.spread(3, "name", :center), ...> Column.spread(3, "name", :right ) ...> } {[0, 3, 0], [0, 3, 0], [0, 3, 0]} iex> alias IO.ANSI.Table.Column iex> { ...> Column.spread(10, "\e[32m\e[42mCHEETAH\e[0m", :left ), ...> Column.spread(10, "\e[32m\e[42mCHEETAH\e[0m", :center), ...> Column.spread(10, "\e[32m\e[42mCHEETAH\e[0m", :right ) ...> } {[0, 21, 3], [1, 21, 2], [3, 21, 0]} iex> alias IO.ANSI.Table.Column iex> { ...> Column.spread(7, "\e[32m\e[42mCHEETAH\e[0m", :left ), ...> Column.spread(7, "\e[32m\e[42mCHEETAH\e[0m", :center), ...> Column.spread(7, "\e[32m\e[42mCHEETAH\e[0m", :right ) ...> } {[0, 21, 0], [0, 21, 0], [0, 21, 0]} ''' @spec spread(width, Line.elem(), Header.align_attr()) :: spread def spread(width, elem, _align_attr = :left) do elem_width = width(elem) |> min(width) [0, io_width(elem, elem_width), width - elem_width] end def spread(width, elem, _align_attr = :right) do elem_width = width(elem) |> min(width) [width - elem_width, io_width(elem, elem_width), 0] end def spread(width, elem, _align_attr = :center) do elem_width = width(elem) |> min(width) left_width = div(width - elem_width, 2) right_width = width - left_width - elem_width [left_width, io_width(elem, elem_width), right_width] end def spread(width, elem, _align_attr), do: spread(width, elem, :left) ## Private functions # The "visible" width of an element (ignoring embedded ANSI escape codes)... # See function `Islands.Grid.to_maps/2` for an example of such elements. @spec width(String.t()) :: non_neg_integer defp width(@ansi_escape <> _rest = elem) do String.replace(elem, @ansi_escape_codes, "") |> String.length() end defp width(elem), do: String.length(elem) # The field width to be used in an Erlang io format... @spec io_width(String.t(), non_neg_integer) :: non_neg_integer defp io_width(@ansi_escape <> _rest = elem, _elem_width) do # We do not cap the width of an element with embedded ANSI escape codes... String.length(elem) end defp io_width(_elem, elem_width), do: elem_width end