defmodule IO.ANSI.Table.Line do
@moduledoc """
Formats the lines of a table.
"""
use PersistConfig
alias IO.ANSI.Plus, as: ANSI
alias IO.ANSI.Table.{Column, LineType, Spec, Style}
@typedoc "Line element"
@type elem :: String.t()
@typedoc "Line item"
@type item :: String.t()
@ansi_enabled get_env(:ansi_enabled, true)
@doc """
Deploys `elements` by interlacing them with `filler` and `borders`
(left, inner and right).
## Examples
iex> alias IO.ANSI.Table.Line
iex> elements = ["Number", "Created at", "Title"]
iex> borders = ["+-", "-+-", "-+"]
iex> Line.items(elements, borders)
[
"", "+-" , "", # filler, left border, filler
"", "Number" , "", # filler, element, filler
"", "-+-" , "", # filler, inner border, filler
"", "Created at", "", # filler, element, filler
"", "-+-" , "", # filler, inner border, filler
"", "Title" , "", # filler, element, filler
"", "-+" , "" # filler, right border, filler
]
"""
@spec items([elem], [Style.border()], String.t) :: [item]
def items(elems, borders, filler \\ "") do
deploy(elems, delimiters(borders, filler))
end
@doc """
Deploys the style attributes of a given line `type` and table `spec`.
## Examples
iex> alias IO.ANSI.Table.Line
iex> # We use a map instead of a %Spec{} for conciseness.
iex> spec = %{style: :medium, sort_attrs: [nil, :asc, nil]}
iex> type = :header
iex> Line.item_attrs(type, spec)
[
:normal, :gold , :normal, # left border
:normal, :canary , :normal, # non-key column
:normal, :gold , :normal, # inner border
:normal, [:canary, :underline], :normal, # key column
:normal, :gold , :normal, # inner border
:normal, :canary , :normal, # non-key column
:normal, :gold , :normal # right border
]
"""
@spec item_attrs(LineType.t(), Spec.t()) :: [Style.attr()]
def item_attrs(type, spec) do
# Wrap attributes in braces to prevent flattening...
border_attr = {Style.border_attr(spec.style, type)}
filler_attr = {Style.filler_attr(spec.style, type)}
key_attr = {Style.key_attr(spec.style, type)}
non_key_attr = {Style.non_key_attr(spec.style, type)}
spec.sort_attrs
|> Enum.map(&if &1 in [:asc, :desc], do: key_attr, else: non_key_attr)
|> deploy(delimiters(border_attr, filler_attr))
|> Enum.map(fn {attr} -> attr end) # unwrap attributes
end
@doc """
Deploys the widths of `elements` for a given line `type` and table `spec`.
## Examples
iex> alias IO.ANSI.Table.Line
iex> # We use a map instead of a %Spec{} for conciseness.
iex> spec = %{
...> style: :medium,
...> align_attrs: [:right, :center, nil],
...> column_widths: [7, 13, 9]
...> }
iex> dashes = ["═══════", "═════════════", "═════════"]
iex> elems = ["Number", "Created at", "Title"]
iex> {Line.item_widths(dashes, :top, spec),
...> Line.item_widths(elems, :header, spec)}
{[0, 2, 0, 0, 7, 0, 0, 3, 0, 0, 13, 0, 0, 3, 0, 0, 9, 0, 0, 2, 0],
[0, 1, 1, 1, 6, 0, 1, 1, 1, 1, 10, 2, 1, 1, 1, 0, 5, 4, 1, 1, 0]}
"""
@spec item_widths([elem], LineType.t(), Spec.t()) :: [Column.width()]
def item_widths(elems, type, spec) do
Enum.zip([spec.column_widths, elems, spec.align_attrs])
|> Enum.map(fn {width, elem, attr} -> Column.spread(width, elem, attr) end)
|> deploy(Style.border_spreads(spec.style, type))
end
@doc ~S"""
Returns an Erlang I/O format (see `:io.format/2`) reflecting `item widths` and
`item attributes`.
It consists of a string with control sequences and embedded
[ANSI codes](https://en.wikipedia.org/wiki/ANSI_escape_code)
(escape sequences), if emitting ANSI codes is enabled.
Here are a few ANSI codes:
- light yellow - `\e[93m`
- light cyan - `\e[96m`
- reset - `\e[0m`
## Examples
iex> alias IO.ANSI.Table.Line
iex> item_widths = [2, 0, 6]
iex> item_attrs = [:light_yellow, :normal, :light_cyan]
iex> Line.format(item_widths, item_attrs, true)
"\e[93m~-2ts\e[0m~-0ts\e[96m~-6ts\e[0m~n"
"""
@spec format([Column.width()], [Style.attr()], boolean) :: String.t()
def format(item_widths, item_attrs, ansi_enabled? \\ @ansi_enabled) do
ansidata_list =
Enum.zip(item_widths, item_attrs)
|> Enum.map(fn
{width, :normal} -> "~-#{width}ts" # t for Unicode translation
{width, attr} -> ANSI.format([attr, "~-#{width}ts"], ansi_enabled?)
end)
"#{ansidata_list}~n" # => string embedded with ANSI escape sequences
end
## Private functions
# @doc """
# Deploys `elements` by interlacing them with `delimiters`
# (left, inner and right).
# The inner `delimiter` is inserted between all `elements` and
# the result is then surrounded by the left and right `delimiters`.
# Returns a flattened list in case any `element` or `delimiter` is a list!
# ## Examples
# iex> alias IO.ANSI.Table.Line
# iex> elems = ["Number", "Created at", "Title"]
# iex> delimiters = ["<", "=", ">"]
# iex> Line.deploy(elems, delimiters)
# ["<", "Number", "=", "Created at", "=", "Title", ">"]
# iex> alias IO.ANSI.Table.Line
# iex> column_spreads = [[1, 6, 0], [1, 10, 2], [0, 5, 4]]
# iex> border_spreads = [[0, 1, 1], [1, 1, 1], [1, 1, 0]]
# iex> Line.deploy(column_spreads, border_spreads)
# [0, 1, 1, 1, 6, 0, 1, 1, 1, 1, 10, 2, 1, 1, 1, 0, 5, 4, 1, 1, 0]
# iex> alias IO.ANSI.Table.Line
# iex> elems = ["Number", "Created at", "Title"]
# iex> border_delimiters = [
# ...> [ ["", "+-" , ""], ""],
# ...> ["", ["", "-+-", ""], ""],
# ...> ["", ["", "-+" , ""] ]
# ...> ]
# iex> Line.deploy(elems, border_delimiters)
# [
# "", "+-" , "",
# "", "Number" , "",
# "", "-+-" , "",
# "", "Created at", "",
# "", "-+-" , "",
# "", "Title" , "",
# "", "-+" , ""
# ]
# iex> alias IO.ANSI.Table.Line
# iex> border_attrs = [{:canary}, {[:canary, :underline]}, {:canary}]
# iex> attr_delimiters = [
# ...> [ [{:normal}, {:gold}, {:normal}], {:normal}],
# ...> [{:normal}, [{:normal}, {:gold}, {:normal}], {:normal}],
# ...> [{:normal}, [{:normal}, {:gold}, {:normal}] ]
# ...> ]
# iex> Line.deploy(border_attrs, attr_delimiters)
# [
# {:normal}, {:gold} , {:normal},
# {:normal}, {:canary} , {:normal},
# {:normal}, {:gold} , {:normal},
# {:normal}, {[:canary, :underline]}, {:normal},
# {:normal}, {:gold} , {:normal},
# {:normal}, {:canary} , {:normal},
# {:normal}, {:gold} , {:normal}
# ]
# """
@spec deploy([any], [any]) :: [any]
defp deploy(elems, [left, inner, right] = _delimiters) do
[left] ++ Enum.intersperse(elems, inner) ++ [right] |> List.flatten()
end
# @doc """
# Returns a list of "delimiters" (left, inner and right).
# ## Examples
# iex> alias IO.ANSI.Table.Line
# iex> Line.delimiters(["+-", "-+-", "-+"], "")
# [
# [ ["", "+-" , ""], ""],
# ["", ["", "-+-", ""], ""],
# ["", ["", "-+" , ""] ]
# ]
# iex> alias IO.ANSI.Table.Line
# iex> Line.delimiters(["╔═", "═╦═", "═╗"], "~")
# [
# [ ["~", "╔═" , "~"], "~"],
# ["~", ["~", "═╦═", "~"], "~"],
# ["~", ["~", "═╗" , "~"] ]
# ]
# iex> alias IO.ANSI.Table.Line
# iex> Line.delimiters(:gold, :normal)
# [
# [ [:normal, :gold, :normal], :normal],
# [:normal, [:normal, :gold, :normal], :normal],
# [:normal, [:normal, :gold, :normal] ]
# ]
# iex> alias IO.ANSI.Table.Line
# iex> Line.delimiters({[:gold, :underline]}, {:normal})
# [
# [ [{:normal}, {[:gold, :underline]}, {:normal}], {:normal}],
# [{:normal}, [{:normal}, {[:gold, :underline]}, {:normal}], {:normal}],
# [{:normal}, [{:normal}, {[:gold, :underline]}, {:normal}] ]
# ]
# iex> alias IO.ANSI.Table.Line
# iex> Line.delimiters('what', %{})
# [
# [ [%{}, 'what', %{}], %{}],
# [%{}, [%{}, 'what', %{}], %{}],
# [%{}, [%{}, 'what', %{}] ]
# ]
# """
@spec delimiters(any, any) :: [any]
defp delimiters([left, inner, right] = _borders, filler) do
[
[ [filler, left, filler], filler], # left delimiter
[filler, [filler, inner, filler], filler], # inner delimiter
[filler, [filler, right, filler] ] # right delimiter
]
end
defp delimiters(border_attr, filler_attr) do
delimiters([border_attr, border_attr, border_attr], filler_attr)
end
end