Skip to content

Commit 3dc0e87

Browse files
pragdaveJosé Valim
authored andcommitted
Add basic support for markdown tables to built-in formatter
Signed-off-by: José Valim <[email protected]>
1 parent a19613c commit 3dc0e87

File tree

4 files changed

+189
-58
lines changed

4 files changed

+189
-58
lines changed

lib/elixir/lib/io/ansi/docs.ex

Lines changed: 134 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,29 @@ defmodule IO.ANSI.Docs do
88
99
The supported values are:
1010
11-
* `:enabled` - toggles coloring on and off (true)
12-
* `:doc_code` - code blocks (cyan, bright)
13-
* `:doc_inline_code` - inline code (cyan)
14-
* `:doc_headings` - h1 and h2 headings (yellow, bright)
15-
* `:doc_title` - top level heading (reverse, yellow, bright)
16-
* `:doc_bold` - bold text (bright)
17-
* `:doc_underline` - underlined text (underline)
18-
* `:width` - the width to format the text (80)
11+
* `:enabled` - toggles coloring on and off (true)
12+
* `:doc_bold` - bold text (bright)
13+
* `:doc_code` - code blocks (cyan, bright)
14+
* `:doc_headings` - h1 and h2 headings (yellow, bright)
15+
* `:doc_inline_code` - inline code (cyan)
16+
* `:doc_table_heading` - style for table headings
17+
* `:doc_title` - top level heading (reverse, yellow, bright)
18+
* `:doc_underline` - underlined text (underline)
19+
* `:width` - the width to format the text (80)
1920
2021
Values for the color settings are strings with
2122
comma-separated ANSI values.
2223
"""
2324
def default_options do
24-
[enabled: true,
25-
doc_code: [:cyan, :bright],
26-
doc_inline_code: [:cyan],
27-
doc_headings: [:yellow, :bright],
28-
doc_title: [:reverse, :yellow, :bright],
29-
doc_bold: [:bright],
30-
doc_underline: [:underline],
31-
width: 80]
25+
[enabled: true,
26+
doc_bold: [:bright],
27+
doc_code: [:cyan, :bright],
28+
doc_headings: [:yellow, :bright],
29+
doc_inline_code: [:cyan],
30+
doc_table_heading: [:reverse],
31+
doc_title: [:reverse, :yellow, :bright],
32+
doc_underline: [:underline],
33+
width: 80]
3234
end
3335

3436
@doc """
@@ -43,12 +45,13 @@ defmodule IO.ANSI.Docs do
4345
padding = div(width + String.length(heading), 2)
4446
heading = heading |> String.rjust(padding) |> String.ljust(width)
4547
write(:doc_title, heading, options)
48+
newline_after_block
4649
end
4750

4851
@doc """
4952
Prints the documentation body.
5053
51-
In addition to the priting string, takes a set of options
54+
In addition to the printing string, takes a set of options
5255
defined in `default_options/1`.
5356
"""
5457
def print(doc, options \\ []) do
@@ -84,13 +87,17 @@ defmodule IO.ANSI.Docs do
8487
process_code(rest, [line], indent, options)
8588
end
8689

87-
defp process([line | rest], indent, options) do
90+
defp process(all=[line | rest], indent, options) do
8891
{stripped, count} = strip_spaces(line, 0)
89-
case stripped do
90-
<<bullet, ?\s, item :: binary >> when bullet in @bullets ->
91-
process_list(item, rest, count, indent, options)
92-
_ ->
93-
process_text(rest, [line], indent, false, options)
92+
if is_table_line?(stripped) && length(rest) > 0 && is_table_line?(hd(rest)) do
93+
process_table(all, indent, options)
94+
else
95+
case stripped do
96+
<<bullet, ?\s, item :: binary >> when bullet in @bullets ->
97+
process_list(item, rest, count, indent, options)
98+
_ ->
99+
process_text(rest, [line], indent, false, options)
100+
end
94101
end
95102
end
96103

@@ -110,11 +117,13 @@ defmodule IO.ANSI.Docs do
110117

111118
defp write_h2(heading, options) do
112119
write(:doc_headings, heading, options)
120+
newline_after_block
113121
end
114122

115123
defp write_h3(heading, indent, options) do
116124
IO.write(indent)
117125
write(:doc_headings, heading, options)
126+
newline_after_block
118127
end
119128

120129
## Lists
@@ -194,7 +203,7 @@ defmodule IO.ANSI.Docs do
194203
|> String.split(~r{\s})
195204
|> write_with_wrap(options[:width] - byte_size(indent), indent, from_list)
196205

197-
unless from_list, do: IO.puts(IO.ANSI.reset)
206+
unless from_list, do: newline_after_block
198207
end
199208

200209
## Code blocks
@@ -219,13 +228,103 @@ defmodule IO.ANSI.Docs do
219228

220229
defp write_code(code, indent, options) do
221230
write(:doc_code, "#{indent}#{Enum.join(Enum.reverse(code), "\n#{indent}┃ ")}", options)
231+
newline_after_block
232+
end
233+
234+
## Tables
235+
236+
defp process_table(lines, indent, options) do
237+
{table, rest} = Enum.split_while(lines, &is_table_line?/1)
238+
table_lines(table, options)
239+
newline_after_block
240+
process(rest, indent, options)
241+
end
242+
243+
defp table_lines(lines, options) do
244+
lines = Enum.map(lines, &split_into_columns/1)
245+
count = lines |> Enum.map(&length/1) |> Enum.max
246+
lines = Enum.map(lines, &pad_to_number_of_columns(&1, count))
247+
248+
widths = for line <- lines, do:
249+
(for col <- line, do: effective_length(col))
250+
251+
col_widths = Enum.reduce(widths,
252+
List.duplicate(0, count),
253+
&max_column_widths/2)
254+
255+
render_table(lines, col_widths, options)
256+
end
257+
258+
defp split_into_columns(line) do
259+
line
260+
|> String.strip(?|)
261+
|> String.strip()
262+
|> String.split(~r/\s\|\s/)
263+
end
264+
265+
defp pad_to_number_of_columns(cols, col_count),
266+
do: cols ++ List.duplicate("", col_count - length(cols))
267+
268+
defp max_column_widths(cols, widths) do
269+
Enum.zip(cols, widths) |> Enum.map(fn {a,b} -> max(a,b) end)
270+
end
271+
272+
defp effective_length(text) do
273+
String.length(Regex.replace(~r/((^|\b)[`*_]+)|([`*_]+\b)/, text, ""))
274+
end
275+
276+
# If second line is heading separator, use the heading style on the first
277+
defp render_table([first, second | rest], widths, options) do
278+
combined = Enum.zip(first, widths)
279+
if table_header?(second) do
280+
draw_table_row(combined, options, :heading)
281+
render_table(rest, widths, options)
282+
else
283+
draw_table_row(combined, options)
284+
render_table([second | rest], widths, options)
285+
end
286+
end
287+
288+
defp render_table([first | rest], widths, options) do
289+
combined = Enum.zip(first, widths)
290+
draw_table_row(combined, options)
291+
render_table(rest, widths, options)
292+
end
293+
294+
defp render_table([], _, _), do: nil
295+
296+
defp table_header?(row), do:
297+
Enum.all?(row, fn col -> col =~ ~r/^:?-+:?$/ end)
298+
299+
defp draw_table_row(cols_and_widths, options, heading \\ false) do
300+
columns = for { col, width } <- cols_and_widths do
301+
padding = width - effective_length(col)
302+
col = Regex.replace(~r/\\ \|/x, col, "|") # escaped bars
303+
text = col
304+
|> handle_links
305+
|> handle_inline(nil, [], [], options)
306+
text <> String.duplicate(" ", padding)
307+
end |> Enum.join(" | ")
308+
309+
if heading do
310+
write(:doc_table_heading, columns, options)
311+
else
312+
IO.puts columns
313+
end
222314
end
223315

224316
## Helpers
225317

318+
@table_line_re ~r'''
319+
( ^ \s{0,3} \| (?: [^|]+ \|)+ \s* $ )
320+
|
321+
(\s \| \s)
322+
'''x
323+
324+
defp is_table_line?(line), do: Regex.match?(@table_line_re, line)
325+
226326
defp write(style, string, options) do
227327
IO.puts [color(style, options), string, IO.ANSI.reset]
228-
IO.puts IO.ANSI.reset
229328
end
230329

231330
defp write_with_wrap([], _available, _indent, _first) do
@@ -363,10 +462,14 @@ defmodule IO.ANSI.Docs do
363462
[color_for(h, options)|t]
364463
end
365464

366-
defp color_for("`", colors), do: color(:doc_inline_code, colors)
367-
defp color_for("_", colors), do: color(:doc_underline, colors)
368-
defp color_for("*", colors), do: color(:doc_bold, colors)
369-
defp color_for("**", colors), do: color(:doc_bold, colors)
465+
defp color_for(mark, colors) do
466+
case mark do
467+
"`" -> color(:doc_inline_code, colors)
468+
"_" -> color(:doc_underline, colors)
469+
"*" -> color(:doc_bold, colors)
470+
"**" -> color(:doc_bold, colors)
471+
end
472+
end
370473

371474
defp color(style, colors) do
372475
color = colors[style]
@@ -377,4 +480,6 @@ defmodule IO.ANSI.Docs do
377480
end
378481
IO.ANSI.format_fragment(color, colors[:enabled])
379482
end
483+
484+
defp newline_after_block, do: IO.puts(IO.ANSI.reset)
380485
end

lib/elixir/lib/kernel/typespec.ex

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule Kernel.Typespec do
2-
@moduledoc """
2+
@moduledoc ~S"""
33
Provides macros and functions for working with typespecs.
44
55
Elixir comes with a notation for declaring types and specifications. Elixir is
@@ -122,37 +122,38 @@ defmodule Kernel.Typespec do
122122
123123
## Built-in types
124124
125-
Built-in type | Defined as
126-
:-------------------- | :---------
127-
`term` | `any`
128-
`binary` | `<< _ :: _ * 8 >>`
129-
`bitstring` | `<< _ :: _ * 1 >>`
130-
`boolean` | `false` &#124; `true`
131-
`byte` | `0..255`
132-
`char` | `0..0x10ffff`
133-
`number` | `integer` &#124; `float`
134-
`char_list` | `[char]`
135-
`list` | `[any]`
136-
`maybe_improper_list` | `maybe_improper_list(any, any)`
137-
`nonempty_list` | `nonempty_list(any)`
138-
`iodata` | `iolist` &#124; `binary`
139-
`iolist` | `maybe_improper_list(byte` &#124; `binary` &#124; `iolist, binary` &#124; `[])`
140-
`module` | `atom` | `tuple`
141-
`mfa` | `{atom, atom, arity}`
142-
`arity` | `0..255`
143-
`node` | `atom`
144-
`timeout` | `:infinity` &#124; `non_neg_integer`
145-
`no_return` | `none`
146-
`fun` | `(... -> any)`
125+
Built-in type | Defined as
126+
:-------------------- | :---------
127+
`term` | `any`
128+
`binary` | `<< _ :: _ * 8 >>`
129+
`bitstring` | `<< _ :: _ * 1 >>`
130+
`boolean` | `false` \| `true`
131+
`byte` | `0..255`
132+
`char` | `0..0x10ffff`
133+
`number` | `integer` \| `float`
134+
`char_list` | `[char]`
135+
`list` | `[any]`
136+
`maybe_improper_list` | `maybe_improper_list(any, any)`
137+
`nonempty_list` | `nonempty_list(any)`
138+
`iodata` | `iolist` \| `binary`
139+
`iolist` | `maybe_improper_list(byte` \| `binary` \| `iolist, binary` \| `[])`
140+
`module` | `atom` \| `tuple`
141+
`mfa` | `{atom, atom, arity}`
142+
`arity` | `0..255`
143+
`node` | `atom`
144+
`timeout` | `:infinity` \| `non_neg_integer`
145+
`no_return` | `none`
146+
`fun` | `(... -> any)`
147+
147148
148149
Some built-in types cannot be expressed with valid syntax according to the
149150
language defined above.
150151
151-
Built-in type | Can be interpreted as
152-
:---------------- | :--------------------
153-
`non_neg_integer` | `0..`
154-
`pos_integer` | `1..`
155-
`neg_integer` | `..-1`
152+
Built-in type | Can be interpreted as
153+
:---------------- | :--------------------
154+
`non_neg_integer` | `0..`
155+
`pos_integer` | `1..`
156+
`neg_integer` | `..-1`
156157
157158
Types defined in other modules are referred to as "remote types", they are
158159
referenced as `Module.type_name` (ex. `Enum.t` or `String.t`).

lib/elixir/test/elixir/io/ansi/docs_test.exs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,30 @@ defmodule IO.ANSI.DocsTest do
216216
result = format("[ANSI escape code](http://en.wikipedia.org/wiki/ANSI_escape_code)")
217217
assert result == "ANSI escape code (http://en.wikipedia.org/wiki/ANSI_escape_code)\n\e[0m"
218218
end
219+
220+
test "lone thing that looks like a table line isn't" do
221+
result = format("one\n2 | 3\ntwo\n")
222+
assert result == "one 2 | 3 two\n\e[0m"
223+
end
224+
225+
test "lone table line at end of input isn't" do
226+
result = format("one\n2 | 3")
227+
assert result == "one 2 | 3\n\e[0m"
228+
end
229+
230+
test "two successive table lines are a table" do
231+
result = format("a | b\none | two\n")
232+
assert result == "a | b \none | two\n\e[0m" # note spacing
233+
end
234+
235+
test "table with heading" do
236+
result = format("column 1 | and 2\n-- | --\na | b\none | two\n")
237+
assert result == "\e[7mcolumn 1 | and 2\e[0m\na | b \none | two \n\e[0m"
238+
end
239+
240+
test "formatting in a table cell works" do
241+
result = format("`a` | _b_\nc | d")
242+
assert result == "\e[36ma\e[0m | \e[4mb\e[0m\nc | d\n\e[0m"
243+
end
244+
219245
end

lib/iex/lib/iex/introspection.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ defmodule IEx.Introspection do
7474
if docs = Code.get_docs(mod, :docs) do
7575
result = for {{f, arity}, _line, _type, _args, doc} <- docs, fun == f, doc != false do
7676
h(mod, fun, arity)
77-
IO.puts ""
7877
end
7978

8079
if result != [], do: :ok, else: :not_found

0 commit comments

Comments
 (0)