• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fhunleth / tablet / 079923cd-9f56-40df-8410-457057622355

10 Jun 2025 03:00AM UTC coverage: 98.529% (+1.4%) from 97.156%
079923cd-9f56-40df-8410-457057622355

Pull #13

circleci

fhunleth
Pause and resume ANSI coloring across multi-line cells
Pull Request #13: Make fit_to_width more generic and support multi-line cells

109 of 110 new or added lines in 3 files covered. (99.09%)

3 existing lines in 1 file now uncovered.

268 of 272 relevant lines covered (98.53%)

344.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.73
/lib/tablet/styles.ex
1
# SPDX-FileCopyrightText: 2025 Frank Hunleth
2
#
3
# SPDX-License-Identifier: Apache-2.0
4
#
5
defmodule Tablet.Styles do
6
  @moduledoc """
7
  Built-in tabular data rendering styles
8
  """
9

10
  @doc false
11
  @spec resolve(atom()) :: Tablet.style_function()
12
  def resolve(name) do
13
    case function_exported?(__MODULE__, name, 1) do
59✔
14
      true -> Function.capture(__MODULE__, name, 1)
58✔
15
      false -> raise ArgumentError, "Not a built-in style: #{inspect(name)}"
1✔
16
    end
17
  end
18

19
  @doc """
20
  Compact style
21

22
  This style produces compact output by only underlining the header and adding
23
  whitespace around data. It is the default style.
24
  """
25
  @spec compact(Tablet.t()) :: Tablet.t()
26
  def compact(table) do
27
    %{table | style_padding: %{edge: 0, cell: 2, multi_column: 3}, line_renderer: &compact_line/3}
32✔
28
  end
29

30
  defp compact_line(table, %{section: :header}, content) do
25✔
31
    [
32
      compact_title(table),
33
      content |> Enum.map(&compact_header(table, &1)) |> Enum.intersperse("   "),
31✔
34
      "\n"
35
    ]
36
  end
37

38
  defp compact_line(_table, %{section: :body, slice: slice}, content) do
88✔
39
    # 2 spaces between columns; 3 spaces between multi-column rows
40
    [content |> Enum.map(&compact_row(&1, slice)) |> Enum.intersperse("   "), "\n"]
120✔
41
  end
42

43
  defp compact_line(_table, _context, _row) do
25✔
44
    # Nothing else
45
    []
46
  end
47

48
  defp compact_row([h], 0), do: [h]
107✔
49
  defp compact_row([h | t], 0), do: [h, "  " | compact_row(t, 0)]
169✔
50
  defp compact_row([h], _slice), do: [" ", h]
13✔
51
  defp compact_row([h | t], slice), do: [" ", h, " " | compact_row(t, slice)]
26✔
52

53
  defp compact_title(%{name: []} = _table), do: []
21✔
54

55
  defp compact_title(table) do
56
    w = interior_width(table, 0, 2, 3)
4✔
57
    [Tablet.fit(table.name, {w, 1}, :center), "\n"]
4✔
58
  end
59

60
  defp compact_header(_table, header) do
61
    header
62
    |> Enum.map(fn v -> [:underline, v, :no_underline] end)
74✔
63
    |> Enum.intersperse("  ")
31✔
64
  end
65

66
  @doc """
67
  Markdown table style
68

69
  Render tabular data as a GitHub-flavored markdown table. Multi-line cells
70
  have their newlines replaced with `<br>` tags.
71

72
  Pass `style: :markdown` to `Tablet.puts/2` or `Tablet.render/2` to use.
73
  """
74
  @spec markdown(Tablet.t()) :: Tablet.t()
75
  def markdown(table) do
76
    %{
77
      table
78
      | style_padding: %{edge: 4, cell: 3, multi_column: 3},
12✔
79
        line_renderer: &markdown_line/3,
80
        formatter: &markdown_formatter(table.formatter, &1, &2)
204✔
81
    }
82
  end
83

84
  defp markdown_formatter(original, key, value) do
85
    text =
204✔
86
      case original.(key, value) do
NEW
87
        {:ok, ansidata} -> ansidata
×
88
        :default -> Tablet.default_format(key, value)
204✔
89
      end
90

91
    {:ok, replace_new_lines(text)}
92
  end
93

94
  defp replace_new_lines(value) when is_binary(value), do: String.replace(value, "\n", "<br>")
208✔
95
  defp replace_new_lines([]), do: []
4✔
96
  defp replace_new_lines([h | t]), do: [replace_new_lines(h) | replace_new_lines(t)]
14✔
97
  defp replace_new_lines(value), do: value
6✔
98

99
  defp markdown_line(table, %{section: :header}, [[]]) do
100
    markdown_title(table)
1✔
101
  end
102

103
  defp markdown_line(table, %{section: :header}, content) do
11✔
104
    [
105
      markdown_title(table),
106
      [content |> Enum.map(&markdown_row/1), "|\n"],
107
      [content |> Enum.map(&markdown_separator/1), "|\n"]
108
    ]
109
  end
110

111
  defp markdown_line(_table, %{section: :body}, content) do
21✔
112
    [content |> Enum.map(&markdown_row/1), "|\n"]
113
  end
114

115
  defp markdown_line(_table, _context, _row) do
12✔
116
    # Nothing else
117
    []
118
  end
119

120
  defp markdown_title(%{name: []} = _table), do: []
7✔
121
  defp markdown_title(table), do: ["## ", table.name, "\n\n"]
5✔
122

123
  defp markdown_separator(row) do
124
    Enum.map(row, fn v ->
15✔
125
      {width, _} = Tablet.visual_size(v)
38✔
126
      ["| ", String.duplicate("-", width), " "]
127
    end)
128
  end
129

130
  defp markdown_row(row) do
131
    Enum.map(row, fn v -> ["| ", v, " "] end)
48✔
132
  end
133

134
  @doc """
135
  Box style
136

137
  Render tabular data with borders drawn from the ASCII character set. This
138
  should render everywhere.
139

140
  To use, pass `style: :box` to `Tablet.puts/2` or `Tablet.render/2`.
141
  """
142
  @spec box(Tablet.t()) :: Tablet.t()
143
  def box(table) do
144
    border = %{
11✔
145
      h: "─",
146
      v: "|",
147
      ul: "+",
148
      uc: "+",
149
      ur: "+",
150
      l: "+",
151
      c: "+",
152
      r: "+",
153
      ll: "+",
154
      lc: "+",
155
      lr: "+"
156
    }
157

158
    %{table | style_options: [border: border]} |> generic_box()
11✔
159
  end
160

161
  @doc """
162
  Unicode box style
163

164
  Render tabular data with borders drawn with Unicode characters. This is a nicer
165
  take on the `:box` style.
166

167
  To use, pass `style: :unicode_box` to `Tablet.puts/2` or `Tablet.render/2`.
168
  """
169
  @spec unicode_box(Tablet.t()) :: Tablet.t()
170
  def unicode_box(table) do
171
    border = %{
13✔
172
      h: "─",
173
      v: "│",
174
      ul: "┌",
175
      uc: "┬",
176
      ur: "┐",
177
      l: "├",
178
      c: "┼",
179
      r: "┤",
180
      ll: "└",
181
      lc: "┴",
182
      lr: "┘"
183
    }
184

185
    %{table | style_options: [border: border]} |> generic_box()
13✔
186
  end
187

188
  @doc """
189
  Generic box style
190

191
  Render tabular data with whatever characters you want for borders. This is
192
  used by the Box and Unicode Box styles. It's configurable via the `:style_options`
193
  option as can be seen in the Box and Unicode Box implementations. Users can
194
  also call this directly by passing `style: :generic_box` and `style_options: [border: ...]`.
195

196
  Options:
197
  * `:border` - a map with the  following fields:
198
    * `:h` and `:v` - the horizontal and vertical characters
199
    * `:ul` and `:ur` - upper left and upper right corners
200
    * `:uc` - intersection of the horizontal top border with a vertical (looks like a T)
201
    * `:ll` and `:lr` - lower left and lower right corners
202
    * `:lc` - analogous to `:uc` except on the Nick Bottom border
203
    * `:l` and `:r` - left and right side characters with horizontal lines towards the interior
204
    * `:c` - interior horizontal and vertical intersection
205
  """
206
  @spec generic_box(Tablet.t()) :: Tablet.t()
207
  def generic_box(table) do
208
    border = Keyword.fetch!(table.style_options, :border)
24✔
209

210
    %{
211
      table
212
      | style_padding: %{edge: 4, cell: 3, multi_column: 3},
24✔
213
        line_renderer: &generic_box_line(&1, &2, &3, border)
112✔
214
    }
215
  end
216

217
  defp generic_box_line(table, %{section: :header}, content, border) do
24✔
218
    [
219
      generic_box_title(table, border, content),
220
      generic_box_row(table, content, border.v)
24✔
221
    ]
222
  end
223

224
  defp generic_box_line(table, %{section: :body, slice: 0}, content, border) do
50✔
225
    [
226
      generic_box_border(table, content, border.l, border.c, border.r, border.h),
50✔
227
      generic_box_row(table, content, border.v)
50✔
228
    ]
229
  end
230

231
  defp generic_box_line(table, %{section: :body}, content, border) do
232
    generic_box_row(table, content, border.v)
14✔
233
  end
234

235
  defp generic_box_line(table, %{section: :footer}, row, border) do
236
    generic_box_border(table, row, border.ll, border.lc, border.lr, border.h)
24✔
237
  end
238

239
  defp generic_box_title(%{name: []} = table, border, content) do
240
    generic_box_border(table, content, border.ul, border.uc, border.ur, border.h)
14✔
241
  end
242

243
  defp generic_box_title(table, border, content) do
244
    w = interior_width(table, 2, 1, 1)
10✔
245

246
    [
247
      [border.ul, String.duplicate(border.h, w), border.ur, "\n"],
10✔
248
      [border.v, Tablet.fit(table.name, {w, 1}, :center), border.v, "\n"],
10✔
249
      generic_box_border(table, content, border.l, border.uc, border.r, border.h)
10✔
250
    ]
251
  end
252

253
  defp interior_width(table, cell_padding, between_cells, between_multi) do
254
    num_keys = length(table.keys)
18✔
255

256
    table.wrap_across *
18✔
257
      (Enum.reduce(
258
         table.keys,
18✔
259
         0,
260
         &Kernel.+(table.column_widths[&1], &2)
52✔
261
       ) + cell_padding * num_keys) + table.wrap_across * (num_keys - 1) * between_cells +
18✔
262
      (table.wrap_across - 1) * between_multi
18✔
263
  end
264

265
  defp generic_box_row(_table, [[]], _vertical), do: []
2✔
266

267
  defp generic_box_row(_table, rows, vertical) do
86✔
268
    [vertical, Enum.map(rows, &generic_box_row_set(&1, vertical)), "\n"]
118✔
269
  end
270

271
  defp generic_box_row_set(row, vertical) do
272
    Enum.map(row, fn v -> [" ", v, " ", vertical] end)
118✔
273
  end
274

275
  defp generic_box_border(_table, row, left_char, middle_char, right_char, line_char) do
276
    lines = Enum.flat_map(row, &generic_box_border_set(&1, line_char))
98✔
277

278
    [left_char, Enum.intersperse(lines, middle_char), right_char, "\n"]
279
  end
280

281
  defp generic_box_border_set(row, line_char) do
282
    Enum.map(row, fn v ->
138✔
283
      {width, _} = Tablet.visual_size(v)
364✔
284
      [String.duplicate(line_char, width + 2)]
285
    end)
286
  end
287

288
  @doc """
289
  Ledger table style
290

291
  Render tabular data as rows that alternate colors.
292

293
  To use, pass `style: :ledger` to `Tablet.puts/2` or `Tablet.render/2`.
294
  """
295
  @spec ledger(Tablet.t()) :: Tablet.t()
296
  def ledger(table) do
297
    %{table | style_padding: %{edge: 2, cell: 2, multi_column: 3}, line_renderer: &ledger_line/3}
11✔
298
  end
299

300
  defp ledger_line(table, %{section: :header}, content) do
11✔
301
    [
302
      :light_blue_background,
303
      :black,
304
      ledger_title(table),
305
      content |> Enum.map(&ledger_row(table, &1)) |> Enum.intersperse(" "),
15✔
306
      :default_background,
307
      :default_color,
308
      "\n"
309
    ]
310
  end
311

312
  defp ledger_line(table, %{section: :body, row: n}, content) do
313
    color =
28✔
314
      if rem(n, 2) == 1, do: [:white_background, :black], else: [:light_black_background, :white]
28✔
315

316
    [
317
      color,
318
      content |> Enum.map(&ledger_row(table, &1)) |> Enum.intersperse(" "),
40✔
319
      :default_background,
320
      :default_color,
321
      "\n"
322
    ]
323
  end
324

325
  defp ledger_line(_table, _context, _row) do
11✔
326
    # Nothing else
327
    []
328
  end
329

330
  defp ledger_title(%{name: []} = _table), do: []
7✔
331

332
  defp ledger_title(table) do
333
    w = interior_width(table, 2, 0, 1)
4✔
334

335
    [
336
      Tablet.fit(table.name, {w, 1}, :center),
4✔
337
      :default_background,
338
      :default_color,
339
      "\n",
340
      :light_blue_background,
341
      :black
342
    ]
343
  end
344

345
  defp ledger_row(_table, row) do
346
    Enum.map(row, fn v -> [" ", v, " "] end)
55✔
347
  end
348
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc