Skip to content

Commit 15f3a59

Browse files
authored
Allow listing outside URLs in extras (#2103)
This makes it possible to delcare URLs as extras and have them listed as links in the sidebar. For example, to set a "Wikipedia" url: ```elixir extras: [ "Wikipedia": [url: "https://wikipedia.com"] ] ``` Closes #2084
1 parent 26129ef commit 15f3a59

File tree

16 files changed

+195
-82
lines changed

16 files changed

+195
-82
lines changed

assets/css/icons.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
--icon-add: "\ea13";
2525
--icon-subtract: "\f1af";
2626
--icon-error-warning: "\eca1";
27+
--icon-external-link-line: "\ecaf";
2728
--icon-information: "\ee59";
2829
--icon-alert: "\ea21";
2930
--icon-double-quotes-l: "\ec51";
@@ -44,6 +45,7 @@
4445
.ri-arrow-up-s-line:before { content: var(--icon-arrow-up-s); }
4546
.ri-arrow-down-s-line:before { content: var(--icon-arrow-down-s); }
4647
.ri-arrow-right-s-line:before { content: var(--icon-arrow-right-s); }
48+
.ri-external-link-line:before { content: var(--icon-external-link-line); }
4749
.ri-search-2-line:before { content: var(--icon-search-2-line); }
4850
.ri-menu-line:before { content: var(--icon-menu-line); }
4951
.ri-close-line:before { content: var(--icon-close-line); }

assets/css/sidebar.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
color: var(--sidebarHover);
3838
}
3939

40+
.sidebar .external-link {
41+
margin: 0 2.5px 0 0;
42+
}
43+
4044
.sidebar .sidebar-header {
4145
background-color: var(--sidebarHeader);
4246
width: 100%;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
settings-3-line,add-line,subtract-line,arrow-up-s-line,arrow-down-s-line,arrow-right-s-line,search-2-line,menu-line,close-line,link-m,code-s-slash-line,error-warning-line,information-line,alert-line,double-quotes-l,printer-line
1+
add-line,alert-line,arrow-down-s-line,arrow-right-s-line,arrow-up-s-line,close-line,code-s-slash-line,double-quotes-l,error-warning-line,external-link-line,information-line,link-m,menu-line,printer-line,search-2-line,settings-3-line,subtract-line

assets/fonts/remixicon.woff2

140 Bytes
Binary file not shown.

assets/js/sidebar/sidebar-list.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function initialize () {
5959
const items = []
6060
const hasHeaders = Array.isArray(node.headers)
6161
const translate = hasHeaders ? undefined : 'no'
62+
const href = node?.url || `${node.id}.html`
6263

6364
// Group header.
6465
if (node.group !== group) {
@@ -78,7 +79,10 @@ export function initialize () {
7879
}
7980

8081
items.push(el('li', {}, [
81-
el('a', {href: `${node.id}.html`, translate}, [node.nested_title || node.title]),
82+
el('a', {href, translate}, [
83+
node.nested_title || node.title,
84+
node.url ? el('i', {class: 'external-link ri-external-link-line'}) : null
85+
].filter(Boolean)),
8286
...childList(`node-${node.id}-headers`,
8387
hasHeaders
8488
? renderHeaders(node)

formatters/html/dist/html-5PS32TPG.js renamed to formatters/html/dist/html-AG646WP7.js

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/ex_doc/formatter/epub.ex

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,19 @@ defmodule ExDoc.Formatter.EPUB do
6161
end
6262

6363
defp generate_extras(config) do
64-
for {_title, extras} <- config.extras do
65-
Enum.each(extras, fn %{id: id, title: title, title_content: title_content, content: content} ->
66-
output = "#{config.output}/OEBPS/#{id}.xhtml"
67-
html = Templates.extra_template(config, title, title_content, content)
64+
for {_title, extras} <- config.extras,
65+
extra_config <- extras,
66+
not is_map_key(extra_config, :url) do
67+
%{id: id, title: title, title_content: title_content, content: content} = extra_config
6868

69-
if File.regular?(output) do
70-
Utils.warn("file #{Path.relative_to_cwd(output)} already exists", [])
71-
end
69+
output = "#{config.output}/OEBPS/#{id}.xhtml"
70+
html = Templates.extra_template(config, title, title_content, content)
7271

73-
File.write!(output, html)
74-
end)
72+
if File.regular?(output) do
73+
Utils.warn("file #{Path.relative_to_cwd(output)} already exists", [])
74+
end
75+
76+
File.write!(output, html)
7577
end
7678
end
7779

lib/ex_doc/formatter/html.ex

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ defmodule ExDoc.Formatter.HTML do
209209
defp generate_extras(extras, config) do
210210
generated_extras =
211211
extras
212+
|> Enum.reject(&is_map_key(&1, :url))
212213
|> with_prev_next()
213214
|> Enum.map(fn {node, prev, next} ->
214215
filename = "#{node.id}.html"
@@ -349,6 +350,7 @@ defmodule ExDoc.Formatter.HTML do
349350

350351
extras =
351352
config.extras
353+
|> Enum.map(&normalize_extras/1)
352354
|> Task.async_stream(
353355
&build_extra(&1, groups, language, autolink_opts, source_url_pattern),
354356
timeout: :infinity
@@ -384,10 +386,21 @@ defmodule ExDoc.Formatter.HTML do
384386
end)
385387
end
386388

389+
defp normalize_extras(base) when is_binary(base), do: {base, %{}}
390+
defp normalize_extras({base, opts}), do: {base, Map.new(opts)}
391+
387392
defp disambiguate_id(extra, discriminator) do
388393
Map.put(extra, :id, "#{extra.id}-#{discriminator}")
389394
end
390395

396+
defp build_extra({input, %{url: _} = input_options}, groups, _lang, _auto, _url_pattern) do
397+
input = to_string(input)
398+
title = input_options[:title] || input
399+
group = GroupMatcher.match_extra(groups, input_options[:url])
400+
401+
%{group: group, id: Utils.text_to_id(title), title: title, url: input_options[:url]}
402+
end
403+
391404
defp build_extra({input, input_options}, groups, language, autolink_opts, source_url_pattern) do
392405
input = to_string(input)
393406
id = input_options[:filename] || input |> filename_to_title() |> Utils.text_to_id()
@@ -447,10 +460,6 @@ defmodule ExDoc.Formatter.HTML do
447460
}
448461
end
449462

450-
defp build_extra(input, groups, language, autolink_opts, source_url_pattern) do
451-
build_extra({input, []}, groups, language, autolink_opts, source_url_pattern)
452-
end
453-
454463
defp normalize_search_data!(nil), do: nil
455464

456465
defp normalize_search_data!(search_data) when is_list(search_data) do
@@ -595,14 +604,23 @@ defmodule ExDoc.Formatter.HTML do
595604
end
596605

597606
defp extra_paths(config) do
598-
Map.new(config.extras, fn
599-
path when is_binary(path) ->
607+
Enum.reduce(config.extras, %{}, fn
608+
path, acc when is_binary(path) ->
600609
base = Path.basename(path)
601-
{base, Utils.text_to_id(Path.rootname(base))}
602610

603-
{path, opts} ->
604-
base = path |> to_string() |> Path.basename()
605-
{base, opts[:filename] || Utils.text_to_id(Path.rootname(base))}
611+
Map.put(acc, base, Utils.text_to_id(Path.rootname(base)))
612+
613+
{path, opts}, acc ->
614+
if Keyword.has_key?(opts, :url) do
615+
acc
616+
else
617+
base = path |> to_string() |> Path.basename()
618+
619+
name =
620+
Keyword.get_lazy(opts, :filename, fn -> Utils.text_to_id(Path.rootname(base)) end)
621+
622+
Map.put(acc, base, name)
623+
end
606624
end)
607625
end
608626
end

lib/ex_doc/formatter/html/search_data.ex

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,10 @@ defmodule ExDoc.Formatter.HTML.SearchData do
1818
["searchData=" | ExDoc.Utils.to_json(data)]
1919
end
2020

21-
defp extra(map) do
22-
if custom_search_data = map[:search_data] do
23-
extra_search_data(map, custom_search_data)
24-
else
25-
{intro, sections} = extract_sections_from_markdown(map.source)
26-
27-
intro_json_item =
28-
encode(
29-
"#{map.id}.html",
30-
map.title,
31-
:extras,
32-
intro
33-
)
34-
35-
section_json_items =
36-
for {header, body} <- sections do
37-
encode(
38-
"#{map.id}.html##{Utils.text_to_id(header)}",
39-
header <> " - #{map.title}",
40-
:extras,
41-
body
42-
)
43-
end
44-
45-
[intro_json_item | section_json_items]
46-
end
47-
end
21+
defp extra(%{url: _}), do: []
4822

49-
defp extra_search_data(map, custom_search_data) do
50-
Enum.map(custom_search_data, fn item ->
23+
defp extra(%{search_data: search_data} = map) when is_list(search_data) do
24+
Enum.map(search_data, fn item ->
5125
link =
5226
if item.anchor === "" do
5327
"#{map.id}.html"
@@ -59,6 +33,30 @@ defmodule ExDoc.Formatter.HTML.SearchData do
5933
end)
6034
end
6135

36+
defp extra(map) do
37+
{intro, sections} = extract_sections_from_markdown(map.source)
38+
39+
intro_json_item =
40+
encode(
41+
"#{map.id}.html",
42+
map.title,
43+
:extras,
44+
intro
45+
)
46+
47+
section_json_items =
48+
for {header, body} <- sections do
49+
encode(
50+
"#{map.id}.html##{Utils.text_to_id(header)}",
51+
header <> " - #{map.title}",
52+
:extras,
53+
body
54+
)
55+
end
56+
57+
[intro_json_item | section_json_items]
58+
end
59+
6260
defp module(%ExDoc.ModuleNode{} = node) do
6361
{intro, sections} = extract_sections(node.doc_format, node)
6462

lib/ex_doc/formatter/html/templates.ex

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,9 @@ defmodule ExDoc.Formatter.HTML.Templates do
5858

5959
defp sidebar_extras(extras) do
6060
for extra <- extras do
61-
%{id: id, title: title, group: group, content: content} = extra
61+
%{id: id, title: title, group: group} = extra
6262

63-
item =
64-
%{
65-
id: to_string(id),
66-
title: to_string(title),
67-
group: to_string(group),
68-
headers: extract_headers(content)
69-
}
63+
item = %{id: to_string(id), title: to_string(title), group: to_string(group)}
7064

7165
case extra do
7266
%{search_data: search_data} when is_list(search_data) ->
@@ -79,10 +73,15 @@ defmodule ExDoc.Formatter.HTML.Templates do
7973
}
8074
end)
8175

82-
Map.put(item, :searchData, search_data)
76+
item
77+
|> Map.put(:headers, extract_headers(extra.content))
78+
|> Map.put(:searchData, search_data)
79+
80+
%{url: url} when is_binary(url) ->
81+
Map.put(item, :url, url)
8382

8483
_ ->
85-
item
84+
Map.put(item, :headers, extract_headers(extra.content))
8685
end
8786
end
8887
end

lib/ex_doc/group_matcher.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ defmodule ExDoc.GroupMatcher do
4646
end
4747

4848
@doc """
49-
Finds a matching group for the given extra filename
49+
Finds a matching group for the given filename or url.
5050
"""
51-
def match_extra(group_patterns, filename) do
51+
def match_extra(group_patterns, path) do
5252
match_group_patterns(group_patterns, fn pattern ->
5353
case pattern do
54-
%Regex{} = regex -> Regex.match?(regex, filename)
55-
string when is_binary(string) -> filename == string
54+
%Regex{} = regex -> Regex.match?(regex, path)
55+
string when is_binary(string) -> path == string
5656
end
5757
end)
5858
end

lib/mix/tasks/docs.ex

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,12 @@ defmodule Mix.Tasks.Docs do
111111
Markdown and plain text pages; default: "PAGES". Example: "GUIDES"
112112
113113
* `:extras` - List of paths to additional Markdown (`.md` extension), Live Markdown
114-
(`.livemd` extension), Cheatsheets (`.cheatmd` extension) and plain text pages to
115-
add to the documentation. You can also specify keyword pairs to customize the
116-
generated filename, title and source file, and search content of each extra page; default: `[]`. Example:
117-
`["README.md", "LICENSE", "CONTRIBUTING.md": [filename: "contributing", title: "Contributing", source: "CONTRIBUTING.mdx"]]`
118-
See the Customizing Extras section for more.
114+
(`.livemd` extension), Cheatsheets (`.cheatmd` extension), external urls (`:url` option),
115+
and plain text pages to add to the documentation. You can also specify keyword pairs to
116+
customize the generated filename, title and source file, and search content of each extra page;
117+
default: `[]`. Example: `["README.md", "LICENSE", "CONTRIBUTING.md": [filename: "contributing",
118+
title: "Contributing", source: "CONTRIBUTING.mdx"]]` See the Customizing Extras section for
119+
more.
119120
120121
* `:favicon` - Path to a favicon image file for the project. Must be PNG, JPEG or SVG. When
121122
specified, the image file will be placed in the output "assets" directory, named
@@ -276,6 +277,13 @@ defmodule Mix.Tasks.Docs do
276277
"Advanced": ~r"/advanced/"
277278
]
278279
280+
External extras from a URL can also be grouped:
281+
282+
groups_for_extras: [
283+
"Elixir": ~r"https://elixir-lang.org/",
284+
"Erlang": ~r"https://www.erlang.org/"
285+
]
286+
279287
Similar can be done for modules:
280288
281289
groups_for_modules: [
@@ -402,6 +410,10 @@ defmodule Mix.Tasks.Docs do
402410
403411
## Customizing Extras
404412
413+
There are two sources for extras, filenames and urls.
414+
415+
For filenames, the allowed configuration is:
416+
405417
* `:title` - The title of the extra page. If not provided, the title will be inferred from the filename.
406418
* `:filename` - The name of the generated file. If not provided, the filename will be inferred from
407419
the source file.
@@ -410,6 +422,11 @@ defmodule Mix.Tasks.Docs do
410422
* `:search_data` - A list of terms to be indexed for autocomplete and search. If not provided, the content
411423
of the extra page will be indexed for search. See the section below for more.
412424
425+
For urls:
426+
427+
* `:title` - The title of the extra page. If not provided, the title will be inferred from the extra name.
428+
* `:url` - The external url to link to from the sidebar.
429+
413430
### Customizing Search Data
414431
415432
It is possible to fully customize the way a given extra is indexed, both in autocomplete and in search.

test/ex_doc/formatter/epub_test.exs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ defmodule ExDoc.Formatter.EPUBTest do
146146
assert content =~ ~r{<li><a href="readme.xhtml">README</a></li>}
147147
end
148148

149+
test "ignores any external url extras", %{tmp_dir: tmp_dir} = context do
150+
config =
151+
context
152+
|> doc_config()
153+
|> Keyword.put(:extras, elixir: [url: "https://elixir-lang.org"])
154+
155+
generate_docs_and_unzip(context, config)
156+
157+
refute File.exists?(tmp_dir <> "/epub/OEBPS/elixir.xhtml")
158+
end
159+
149160
test "uses samp as highlight tag for markdown", %{tmp_dir: tmp_dir} = context do
150161
generate_docs_and_unzip(context, doc_config(context))
151162

test/ex_doc/formatter/html/search_data_test.exs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,12 @@ defmodule ExDoc.Formatter.HTML.SearchDataTest do
204204
Section _1_ content.
205205
""")
206206

207-
config = %ExDoc.Config{output: "#{c.tmp_dir}/doc", extras: [readme_path]}
207+
extras = [
208+
readme_path,
209+
"Elixir": [url: "https://elixir-lang.org"]
210+
]
211+
212+
config = %ExDoc.Config{output: "#{c.tmp_dir}/doc", extras: extras}
208213
[item1, item2] = search_data([], config)["items"]
209214

210215
assert item1["ref"] == "readme.html"

0 commit comments

Comments
 (0)