Skip to content

Commit 8e7ce49

Browse files
milmazzjosevalim
andauthored
add support for admonition text blocks (#1400)
Co-authored-by: José Valim <jose.valim@gmail.com>
1 parent 32ace5a commit 8e7ce49

14 files changed

+239
-29
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
* Enhancements
6+
* Add support for admonition text blocks
7+
* Improve accessibility for light and dark themes
8+
9+
* Bug fixes
10+
* Ensure that `mix docs --open` works on Windows
11+
312
## v0.28.0 (2022-01-24)
413

514
ExDoc v0.28.0 requires Elixir v1.11+.

README.md

+32-2
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,15 @@ def deps do
4242
end
4343
```
4444

45-
> Note: if you are using Elixir v1.7, v1.8, or v1.9, use `~> 0.22.0`.
45+
> #### Note {: .info}
46+
>
47+
> If you are using Elixir v1.7, v1.8, or v1.9, use `~> 0.22.0`.
4648
4749
After adding ExDoc as a dependency, run `mix deps.get` to install it.
4850

49-
> Note: Some Operating System distributions split Erlang into multiple packages and at least one ExDoc dependency (`earmark_parser`) requires Erlang development environment. If you get a message like "/usr/lib/erlang/lib/parsetools-2.3.1/include/yeccpre.hrl: no such file or directory", it means you lack this environment. For instance, on the Debian operating system and its derivatives, you need to `apt install erlang-dev`.
51+
> #### Erlang development environment {: .warning}
52+
>
53+
> Some Operating System distributions split Erlang into multiple packages and at least one ExDoc dependency (`earmark_parser`) requires Erlang development environment. If you get a message like "/usr/lib/erlang/lib/parsetools-2.3.1/include/yeccpre.hrl: no such file or directory", it means you lack this environment. For instance, on the Debian operating system and its derivatives, you need to `apt install erlang-dev`.
5054
5155
ExDoc will automatically pull in information from your projects, like the application and version. However, you may want to set `:name`, `:source_url` and `:homepage_url` to have a nicer output from ExDoc, such as:
5256

@@ -147,6 +151,32 @@ You can also use a custom text, e.g.: `` [custom text](`MyModule.function/1`) ``
147151
Link to extra pages like this: `` [Up and running](Up and running.md) `` (skipping the directory
148152
the page is in), the final link will be automatically converted to `up-and-running.html`.
149153

154+
## Admonition blocks
155+
156+
You may want to draw attention to certain statements by taking them out of the
157+
content's flow and labeling them with a priority. These are called admonitions,
158+
sometimes are also known as asides or callouts. An admonition block is rendered
159+
based on the assigned label or class. `ex_doc` supports the following tags:
160+
`warning`, `error`, `info`, and `neutral` over header levels `h3` and `h4`.
161+
162+
The syntax is as follows:
163+
164+
> #### Error {: .error}
165+
>
166+
> This syntax will render an error block
167+
168+
The result for the previous syntax is as follows:
169+
170+
> #### Error {: .error}
171+
>
172+
> This syntax will render an error block
173+
174+
For example, if you change the class name to `neutral`, you get:
175+
176+
> #### Error {: .neutral}
177+
>
178+
> This syntax will render an error block
179+
150180
## Extensions
151181

152182
ExDoc renders Markdown content for you, but you can extend it to render complex objects on the page using JavaScript. To inject custom JavaScript into every page, add this to your configuration:

assets/js/content.js

+15
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function initialize () {
1212
fixLinks()
1313
fixSpacebar()
1414
setLivebookBadgeUrl()
15+
fixBlockquotes()
1516
}
1617

1718
/**
@@ -25,6 +26,20 @@ function fixLinks () {
2526
})
2627
}
2728

29+
/**
30+
* Add CSS classes to `blockquote` elements when those are used to
31+
* support admonition text blocks
32+
*/
33+
function fixBlockquotes () {
34+
const classes = ['warning', 'info', 'error', 'neutral']
35+
36+
classes.forEach(element => {
37+
qsAll(`blockquote h3.${element}, blockquote h4.${element}`).forEach(header => {
38+
header.closest('blockquote').classList.add(element)
39+
})
40+
})
41+
}
42+
2843
/**
2944
* Focuses the content element.
3045
*

assets/less/content/general.less

+88-8
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,98 @@ h1 .note {
101101

102102
blockquote {
103103
font-style: italic;
104-
margin: .5em 0;
105-
padding: .25em 1.5em;
106104
border-left: 3px solid @gray;
107-
display: inline-block;
105+
position: relative;
106+
margin: 1.5625em 0;
107+
padding: 0 1.2rem;
108+
overflow: auto;
109+
background-color: #f6f6f6;
110+
border-radius: 3px;
108111

109-
*:first-child {
110-
padding-top: 0;
111-
margin-top: 0;
112+
&.warning,
113+
&.error,
114+
&.info,
115+
&.neutral {
116+
color: #000;
117+
border-radius: 10px;
118+
border-left: 0;
112119
}
113120

114-
*:last-child {
115-
padding-bottom: 0;
121+
&.warning {
122+
background-color: #fff7ed;
123+
}
124+
125+
&.error {
126+
background-color: #fdeeec;
127+
}
128+
129+
&.info {
130+
background-color: #e9f5fe;
131+
}
132+
133+
&.neutral {
134+
background-color: #e2e8ef;
135+
}
136+
137+
h3.warning,
138+
h3.error,
139+
h3.info,
140+
h3.neutral,
141+
h4.warning,
142+
h4.error,
143+
h4.info,
144+
h4.neutral {
145+
margin: 0 -1.2rem;
146+
padding: 0.7rem 1.2rem 0.7rem 3.3rem;
147+
font-weight: 700;
148+
font-style: normal;
149+
color: #fff;
150+
151+
&::before {
152+
position: absolute;
153+
left: 1rem;
154+
font-size: 1.8rem;
155+
156+
.remix-icon
157+
}
158+
159+
&.warning {
160+
background-color: #f3ac55;
161+
color: #000;
162+
163+
&::before {
164+
color: #000;
165+
content: @icon-error-warning;
166+
}
167+
}
168+
169+
&.error {
170+
background-color: #eb5949;
171+
172+
&::before {
173+
content: @icon-error-warning;
174+
}
175+
}
176+
177+
&.info {
178+
background-color: #4496f7;
179+
180+
&::before {
181+
content: @icon-information;
182+
}
183+
}
184+
185+
&.neutral {
186+
background-color: #101828;
187+
188+
&::before {
189+
content: @icon-double-quotes-l;
190+
}
191+
}
192+
}
193+
194+
p:last-child {
195+
padding-bottom: 1em;
116196
margin-bottom: 0;
117197
}
118198
}

assets/less/icons.less

+25-11
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,30 @@
2121
@icon-arrow-right-s: "\ea6e";
2222
@icon-add: "\ea13";
2323
@icon-subtract: "\f1af";
24+
@icon-error-warning: "\eca1";
25+
@icon-information: "\ee59";
26+
@icon-alert: "\ea21";
27+
@icon-double-quotes-l: "\ec51";
28+
@icon-link-m: "\eeaf";
29+
@icon-close-line: "\eb99";
30+
@icon-code-s-slash-line: "\ebad";
31+
@icon-menu-line: "\ef3e";
32+
@icon-search-2-line: "\f0cd";
33+
@icon-settings-3-line: "\f0e6";
2434

2535
.ri-lg { font-size: 1.3333em; line-height: 0.75em; vertical-align: -.0667em; }
26-
.ri-settings-3-line:before { content: "\f0e6"; }
27-
.ri-add-line:before { content: "\ea13"; }
28-
.ri-subtract-line:before { content: "\f1af"; }
29-
.ri-arrow-up-s-line:before { content: "\ea78"; }
30-
.ri-arrow-down-s-line:before { content: "\ea4e"; }
31-
.ri-arrow-right-s-line:before { content: "\ea6e"; }
32-
.ri-search-2-line:before { content: "\f0cd"; }
33-
.ri-menu-line:before { content: "\ef3e"; }
34-
.ri-close-line:before { content: "\eb99"; }
35-
.ri-link-m:before { content: "\eeaf"; }
36-
.ri-code-s-slash-line:before { content: "\ebad"; }
36+
.ri-settings-3-line:before { content: @icon-settings-3-line; }
37+
.ri-add-line:before { content: @icon-add; }
38+
.ri-subtract-line:before { content: @icon-subtract; }
39+
.ri-arrow-up-s-line:before { content: @icon-arrow-up-s; }
40+
.ri-arrow-down-s-line:before { content: @icon-arrow-down-s; }
41+
.ri-arrow-right-s-line:before { content: @icon-arrow-right-s; }
42+
.ri-search-2-line:before { content: @icon-search-2-line; }
43+
.ri-menu-line:before { content: @icon-menu-line; }
44+
.ri-close-line:before { content: @icon-close-line; }
45+
.ri-link-m:before { content: @icon-link-m; }
46+
.ri-code-s-slash-line:before { content: @icon-code-s-slash-line; }
47+
.ri-error-warning-line:before { content: @icon-error-warning; }
48+
.ri-information-line:before { content: @icon-information; }
49+
.ri-alert-line:before { content: @icon-alert; }
50+
.ri-double-quotes-l:before { content: @icon-double-quotes-l; }

assets/less/night/night.less

+5
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ body.night-mode {
1616
color: @main;
1717
}
1818
}
19+
20+
blockquote {
21+
border-left: 3px solid rgb(68, 68, 76);
22+
background-color: rgb(44, 44, 49)
23+
}
1924
}

formatters/html/dist/app-a9eda93dd96a056cfa41.js formatters/html/dist/app-073abeeda7b7e6d5d7e9.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

formatters/html/dist/elixir-314eadf80a97188c39e4.css

-2
This file was deleted.

formatters/html/dist/elixir-863ed9c5a4dcef38ae6c.css

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

formatters/html/dist/erlang-710a465deb3dcc3d992c.css

-2
This file was deleted.

formatters/html/dist/erlang-e4da3667c76b56ef1e6e.css

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/ex_doc/formatter/html/templates.ex

+43-2
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,52 @@ defmodule ExDoc.Formatter.HTML.Templates do
248248
|> elem(0)
249249
end
250250

251+
@class_regex ~r/<h[23].*?(\sclass="(?<class>[^"]+)")?.*?>/
252+
@class_separator " "
251253
defp link_heading(match, _tag, _title, "", _prefix), do: match
252254

253-
defp link_heading(_match, tag, title, id, prefix) do
255+
defp link_heading(match, tag, title, id, prefix) do
256+
section_header_class_name = "section-heading"
257+
258+
# NOTE: This addition is mainly to preserve the previous `class` attributes
259+
# from the headers, in case there is one. Now with the _admonition_ text
260+
# block, we inject CSS classes. So far, the supported classes are:
261+
# `warning`, `info`, `error`, and `neutral`.
262+
#
263+
# The Markdown syntax that we support for the admonition text
264+
# blocks is something like this:
265+
#
266+
# > ### Never open this door! {: .warning}
267+
# >
268+
# > ...
269+
#
270+
# That should produce the following HTML:
271+
#
272+
# <blockquote>
273+
# <h3 class="warning">Never open this door!</h3>
274+
# <p>...</p>
275+
# </blockquote>
276+
#
277+
# The original implementation discarded the previous CSS classes. Instead,
278+
# it was setting `#{section_header_class_name}` as the only CSS class
279+
# associated with the given header.
280+
class_attribute =
281+
case Regex.named_captures(@class_regex, match) do
282+
%{"class" => ""} ->
283+
section_header_class_name
284+
285+
%{"class" => previous_classes} ->
286+
# Let's make sure that the `section_header_class_name` is not already
287+
# included in the previous classes for the header
288+
previous_classes
289+
|> String.split(@class_separator)
290+
|> Enum.reject(&(&1 == section_header_class_name))
291+
|> Enum.join(@class_separator)
292+
|> Kernel.<>(" #{section_header_class_name}")
293+
end
294+
254295
"""
255-
<#{tag} id="#{prefix}#{id}" class="section-heading">
296+
<#{tag} id="#{prefix}#{id}" class="#{class_attribute}">
256297
<a href="##{prefix}#{id}" class="hover-link"><i class="ri-link-m" aria-hidden="true"></i>
257298
<p class="sr-only">#{id}</p>
258299
</a>

test/ex_doc/formatter/html/templates_test.exs

+16
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,22 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do
144144
</h3>
145145
"""
146146
end
147+
148+
test "generates headers for admonition support" do
149+
admonition_block = """
150+
<blockquote><h3 class="warning">Foo</h3></blockquote>
151+
"""
152+
153+
assert Templates.link_headings(admonition_block) == """
154+
<blockquote><h3 id="foo" class="warning section-heading">
155+
<a href="#foo" class="hover-link"><i class="ri-link-m" aria-hidden="true"></i>
156+
<p class="sr-only">foo</p>
157+
</a>
158+
Foo
159+
</h3>
160+
</blockquote>
161+
"""
162+
end
147163
end
148164

149165
describe "synopsis" do

0 commit comments

Comments
 (0)