Skip to content

Commit f0fbf02

Browse files
alexocodeericmj
authored andcommitted
IO.ANSI.Docs - Properly render quote blocks (#9684)
1 parent 5111513 commit f0fbf02

File tree

2 files changed

+176
-9
lines changed

2 files changed

+176
-9
lines changed

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

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule IO.ANSI.Docs do
1414
* `:doc_code` - code blocks (cyan)
1515
* `:doc_headings` - h1, h2, h3, h4, h5, h6 headings (yellow)
1616
* `:doc_metadata` - documentation metadata keys (yellow)
17+
* `:doc_quote` - leading quote character `> ` (light black)
1718
* `:doc_inline_code` - inline code (cyan)
1819
* `:doc_table_heading` - the style for table headings
1920
* `:doc_title` - top level heading (reverse, yellow)
@@ -31,6 +32,7 @@ defmodule IO.ANSI.Docs do
3132
doc_code: [:cyan],
3233
doc_headings: [:yellow],
3334
doc_metadata: [:yellow],
35+
doc_quote: [:light_black],
3436
doc_inline_code: [:cyan],
3537
doc_table_heading: [:reverse],
3638
doc_title: [:reverse, :yellow],
@@ -73,7 +75,7 @@ defmodule IO.ANSI.Docs do
7375
{key, value}, _printed when is_binary(value) and key in @metadata_filter ->
7476
label = metadata_label(key, options)
7577
indent = String.duplicate(" ", length_without_escape(label, 0) + 1)
76-
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true)
78+
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true, "")
7779

7880
{key, value}, _printed when is_boolean(value) and key in @metadata_filter ->
7981
IO.puts([metadata_label(key, options), ' ', to_string(value)])
@@ -139,6 +141,11 @@ defmodule IO.ANSI.Docs do
139141
write_heading(heading, rest, text, indent, options)
140142
end
141143

144+
defp process([">" <> line | rest], text, indent, options) do
145+
write_text(text, indent, options)
146+
process_quote(rest, [line], indent, options)
147+
end
148+
142149
defp process(["" | rest], text, indent, options) do
143150
write_text(text, indent, options)
144151
process(rest, [], indent, options)
@@ -183,6 +190,47 @@ defmodule IO.ANSI.Docs do
183190
process(rest, [], "", options)
184191
end
185192

193+
## Quotes
194+
195+
defp process_quote([], lines, indent, options) do
196+
write_quote(lines, indent, options, false)
197+
end
198+
199+
defp process_quote([">", ">" <> line | rest], lines, indent, options) do
200+
write_quote(lines, indent, options, true)
201+
write_empty_quote_line(options)
202+
process_quote(rest, [line], indent, options)
203+
end
204+
205+
defp process_quote([">" <> line | rest], lines, indent, options) do
206+
process_quote(rest, [line | lines], indent, options)
207+
end
208+
209+
defp process_quote(rest, lines, indent, options) do
210+
write_quote(lines, indent, options, false)
211+
process(rest, [], indent, options)
212+
end
213+
214+
defp write_quote(lines, indent, options, no_wrap) do
215+
lines
216+
|> Enum.map(&String.trim/1)
217+
|> Enum.reverse()
218+
|> write_lines(
219+
indent,
220+
options,
221+
no_wrap,
222+
quote_prefix(options)
223+
)
224+
end
225+
226+
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{IO.ANSI.reset()}"
227+
228+
defp write_empty_quote_line(options) do
229+
options
230+
|> quote_prefix()
231+
|> IO.puts()
232+
end
233+
186234
## Lists
187235

188236
defp process_rest(stripped, rest, count, text, indent, options) do
@@ -267,16 +315,25 @@ defmodule IO.ANSI.Docs do
267315
end
268316

269317
defp write_text(lines, indent, options, no_wrap) do
318+
write_lines(lines, indent, options, no_wrap, "")
319+
end
320+
321+
defp write_lines(lines, indent, options, no_wrap, prefix) do
270322
lines
271323
|> Enum.join(" ")
272-
|> handle_links
273-
|> handle_inline(options)
324+
|> format_text(options)
274325
|> String.split(@spaces)
275-
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap)
326+
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
276327

277328
unless no_wrap, do: newline_after_block()
278329
end
279330

331+
defp format_text(text, options) do
332+
text
333+
|> handle_links()
334+
|> handle_inline(options)
335+
end
336+
280337
## Code blocks
281338

282339
defp process_code([], code, indent, options) do
@@ -470,14 +527,27 @@ defmodule IO.ANSI.Docs do
470527
IO.puts([color(style, options), string, IO.ANSI.reset()])
471528
end
472529

473-
defp write_with_wrap([], _available, _indent, _first) do
530+
defp write_with_wrap([], _available, _indent, _first, _prefix) do
474531
:ok
475532
end
476533

477-
defp write_with_wrap(words, available, indent, first) do
478-
{words, rest} = take_words(words, available, [])
479-
IO.puts(if(first, do: "", else: indent) <> Enum.join(words, " "))
480-
write_with_wrap(rest, available, indent, false)
534+
defp write_with_wrap(words, available, indent, first, prefix) do
535+
words
536+
|> wrap_text(available, indent, first, prefix, [])
537+
|> Enum.join("\n")
538+
|> IO.puts()
539+
end
540+
541+
defp wrap_text([], _available, _indent, _first, _prefix, wrapped_lines) do
542+
Enum.reverse(wrapped_lines)
543+
end
544+
545+
defp wrap_text(words, available, indent, first, prefix, wrapped_lines) do
546+
prefix_length = length_without_escape(prefix, 0)
547+
{words, rest} = take_words(words, available - prefix_length, [])
548+
line = [if(first, do: "", else: indent), prefix, Enum.join(words, " ")]
549+
550+
wrap_text(rest, available, indent, false, prefix, [line | wrapped_lines])
481551
end
482552

483553
defp take_words([word | words], available, acc) do

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,103 @@ defmodule IO.ANSI.DocsTest do
5757
assert result == "\e[33m### wibble\e[0m\n\e[0m\ntext\n\e[0m"
5858
end
5959

60+
test "short single-line quote block is converted into single-line quote" do
61+
result =
62+
format("""
63+
line
64+
65+
> normal *italics* `code`
66+
67+
line2
68+
""")
69+
70+
assert result ==
71+
"""
72+
line
73+
\e[0m
74+
\e[90m> \e[0mnormal \e[1mitalics\e[0m \e[36mcode\e[0m
75+
\e[0m
76+
line2
77+
\e[0m\
78+
"""
79+
end
80+
81+
test "short multi-line quote block is converted into single-line quote" do
82+
result =
83+
format("""
84+
line
85+
86+
> normal
87+
> *italics*
88+
> `code`
89+
90+
line2
91+
""")
92+
93+
assert result ==
94+
"""
95+
line
96+
\e[0m
97+
\e[90m> \e[0mnormal \e[1mitalics\e[0m \e[36mcode\e[0m
98+
\e[0m
99+
line2
100+
\e[0m\
101+
"""
102+
end
103+
104+
test "long multi-line quote block is converted into wrapped multi-line quote" do
105+
result =
106+
format("""
107+
line
108+
109+
> normal
110+
> *italics*
111+
> `code`
112+
> some-extremly-long-word-which-can-not-possibly-fit-into-the-previous-line
113+
114+
line2
115+
""")
116+
117+
assert result ==
118+
"""
119+
line
120+
\e[0m
121+
\e[90m> \e[0mnormal \e[1mitalics\e[0m \e[36mcode\e[0m
122+
\e[90m> \e[0msome-extremly-long-word-which-can-not-possibly-fit-into-the-previous-line
123+
\e[0m
124+
line2
125+
\e[0m\
126+
"""
127+
end
128+
129+
test "multi-line quote block containing empty lines is converted into wrapped multi-line quote" do
130+
result =
131+
format("""
132+
line
133+
134+
> normal
135+
> *italics*
136+
>
137+
> `code`
138+
> some-extremly-long-word-which-can-not-possibly-fit-into-the-previous-line
139+
140+
line2
141+
""")
142+
143+
assert result ==
144+
"""
145+
line
146+
\e[0m
147+
\e[90m> \e[0mnormal \e[1mitalics\e[0m
148+
\e[90m> \e[0m
149+
\e[90m> \e[0m\e[36mcode\e[0m
150+
\e[90m> \e[0msome-extremly-long-word-which-can-not-possibly-fit-into-the-previous-line
151+
\e[0m
152+
line2
153+
\e[0m\
154+
"""
155+
end
156+
60157
test "code block is converted" do
61158
result = format("line\n\n code\n code2\n\nline2\n")
62159
assert result == "line\n\e[0m\n\e[36m code\n code2\e[0m\n\e[0m\nline2\n\e[0m"

0 commit comments

Comments
 (0)