Skip to content

Commit 710ecd3

Browse files
authored
Allow configuring custom_autocompletions list (#2011)
1 parent 440cf13 commit 710ecd3

File tree

7 files changed

+132
-50
lines changed

7 files changed

+132
-50
lines changed

Diff for: README.md

-43
Original file line numberDiff line numberDiff line change
@@ -452,49 +452,6 @@ Similarly to the example above, if your Markdown includes Mermaid graph specific
452452
453453
For more details and configuration options, see the [Mermaid usage docs](https://mermaid-js.github.io/mermaid/#/usage).
454454
455-
## Changing documentation over time
456-
457-
As your project grows, your documentation may very likely change, even structurally. There are a few important things to consider in this regard:
458-
459-
- Links to your *extras* will break if you change or move file names.
460-
- Links to your *modules, and mix tasks* will change if you change their name.
461-
- Links to *functions* are actually links to modules with anchor links. If you change the function name, the link does
462-
not break but will leave users at the top of the module's documentation.
463-
464-
Because these docs are static files, the behavior of a missing page will depend on where they are hosted.
465-
In particular, [hexdocs.pm](https://hexdocs.pm) will show a 404 page.
466-
467-
You can improve the developer experience on everything but function names changing
468-
by using the `redirects` configuration. For example, if you changed the module `MyApp.MyModule`
469-
to `MyApp.My.Module` and the extra `get-started.md` to `quickstart.md`, you can
470-
setup the following redirects:
471-
472-
<!-- tabs-open -->
473-
474-
### Elixir
475-
476-
For this example, we've changed the module `MyApp.MyModule` to `MyApp.My.Module`, and the extra `get-started.md` to `quickstart.md`
477-
478-
```elixir
479-
redirects: %{
480-
"MyApp.MyModule" => "MyApp.My.Module",
481-
"get-started" => "quickstart"
482-
}
483-
```
484-
485-
### Erlang
486-
487-
For this example, we've changed the module `:my_module` to `:my_module2`, and the extra `get-started.md` to `quickstart.md`
488-
489-
```erlang
490-
{redirects, [
491-
{"my_module", "my_module2"},
492-
{"get-started", "quickstart"}
493-
]}.
494-
```
495-
496-
<!-- tabs-close -->
497-
498455
## Contributing
499456
500457
The easiest way to test changes to ExDoc is to locally rebuild the app and its own documentation:

Diff for: assets/js/autocomplete/suggestions.js

+32-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const SUGGESTION_CATEGORY = {
1818
moduleChild: 'module-child',
1919
mixTask: 'mix-task',
2020
extra: 'extra',
21-
section: 'section'
21+
section: 'section',
22+
custom: 'custom'
2223
}
2324

2425
/**
@@ -42,7 +43,8 @@ export function getSuggestions (query, limit = 8) {
4243
...findSuggestionsInTopLevelNodes(nodes.extras, query, SUGGESTION_CATEGORY.extra, 'page'),
4344
...findSuggestionsInSectionsOfNodes(nodes.modules, query, SUGGESTION_CATEGORY.section, 'module'),
4445
...findSuggestionsInSectionsOfNodes(nodes.tasks, query, SUGGESTION_CATEGORY.section, 'mix task'),
45-
...findSuggestionsInSectionsOfNodes(nodes.extras, query, SUGGESTION_CATEGORY.section, 'page')
46+
...findSuggestionsInSectionsOfNodes(nodes.extras, query, SUGGESTION_CATEGORY.section, 'page'),
47+
...findSuggestionsInCustomSidebarNodes(nodes.custom || [], query, SUGGESTION_CATEGORY.custom, 'custom')
4648
].filter(suggestion => suggestion !== null)
4749

4850
return sort(suggestions).slice(0, limit)
@@ -55,6 +57,13 @@ function findSuggestionsInTopLevelNodes (nodes, query, category, label) {
5557
return nodes.map(node => nodeSuggestion(node, query, category, label))
5658
}
5759

60+
/**
61+
* Finds suggestions in custom sidebar nodes.
62+
*/
63+
function findSuggestionsInCustomSidebarNodes (nodes, query, category, label) {
64+
return nodes.map(node => customNodeSuggestion(node, query, category, label))
65+
}
66+
5867
/**
5968
* Finds suggestions in node groups of the given parent nodes.
6069
*/
@@ -104,7 +113,25 @@ function nodeSuggestion (node, query, category, label) {
104113
description: null,
105114
matchQuality: matchQuality(node.title, query),
106115
deprecated: node.deprecated,
107-
labels: [label],
116+
labels: node.labels || [label],
117+
category
118+
}
119+
}
120+
121+
/**
122+
* Builds a suggestion for a custom top level node.
123+
* Returns null if the node doesn't match the query.
124+
*/
125+
function customNodeSuggestion (node, query, category, label) {
126+
if (!matchesAll(node.title, query)) { return null }
127+
128+
return {
129+
link: node.link,
130+
title: highlightMatches(node.title, query),
131+
description: node.description,
132+
matchQuality: matchQuality(node.title, query),
133+
deprecated: node.deprecated,
134+
labels: node.labels || [label],
108135
category
109136
}
110137
}
@@ -211,7 +238,8 @@ function categoryPriority (category) {
211238
case SUGGESTION_CATEGORY.module: return 1
212239
case SUGGESTION_CATEGORY.moduleChild: return 2
213240
case SUGGESTION_CATEGORY.mixTask: return 3
214-
default: return 4
241+
case SUGGESTION_CATEGORY.custom: return 4
242+
default: return 5
215243
}
216244
}
217245

Diff for: lib/ex_doc/config.ex

+17
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ defmodule ExDoc.Config do
1212
def skip_undefined_reference_warnings_on(_string), do: false
1313
def skip_code_autolink_to(_string), do: false
1414

15+
@type custom_autocompletion :: %{
16+
required(:link) => String.t(),
17+
required(:title) => String.t(),
18+
required(:description) => String.t(),
19+
optional(:labels) => [String.t()]
20+
}
21+
1522
defstruct annotations_for_docs: &__MODULE__.annotations_for_docs/1,
1623
api_reference: true,
1724
apps: [],
@@ -32,6 +39,7 @@ defmodule ExDoc.Config do
3239
groups_for_extras: [],
3340
groups_for_docs: [],
3441
groups_for_modules: [],
42+
custom_autocompletions: [],
3543
homepage_url: nil,
3644
language: "en",
3745
logo: nil,
@@ -63,6 +71,7 @@ defmodule ExDoc.Config do
6371
before_closing_body_tag: (atom() -> String.t()) | mfa() | map(),
6472
before_closing_footer_tag: (atom() -> String.t()) | mfa() | map(),
6573
before_closing_head_tag: (atom() -> String.t()) | mfa() | map(),
74+
custom_autocompletions: [custom_autocompletion],
6675
canonical: nil | String.t(),
6776
cover: nil | Path.t(),
6877
default_group_for_doc: (keyword() -> String.t() | nil),
@@ -114,6 +123,7 @@ defmodule ExDoc.Config do
114123
{groups_for_docs, options} = Keyword.pop(options, :groups_for_docs, [])
115124
{groups_for_extras, options} = Keyword.pop(options, :groups_for_extras, [])
116125
{groups_for_modules, options} = Keyword.pop(options, :groups_for_modules, [])
126+
{custom_autocompletions, options} = Keyword.pop(options, :custom_autocompletions, [])
117127

118128
{skip_undefined_reference_warnings_on, options} =
119129
Keyword.pop(
@@ -131,6 +141,7 @@ defmodule ExDoc.Config do
131141
end)
132142

133143
preconfig = %__MODULE__{
144+
custom_autocompletions: normalize_custom_autocompletions(custom_autocompletions),
134145
filter_modules: normalize_filter_modules(filter_modules),
135146
groups_for_docs: normalize_groups(groups_for_docs),
136147
groups_for_extras: normalize_groups(groups_for_extras),
@@ -155,6 +166,12 @@ defmodule ExDoc.Config do
155166
struct(preconfig, options)
156167
end
157168

169+
defp normalize_custom_autocompletions(custom_autocompletions) do
170+
Enum.map(custom_autocompletions, fn autocompletion ->
171+
Map.new(autocompletion)
172+
end)
173+
end
174+
158175
defp normalize_output(output) do
159176
String.trim_trailing(output, "/")
160177
end

Diff for: lib/ex_doc/formatter/html.ex

+3-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,9 @@ defmodule ExDoc.Formatter.HTML do
183183
end
184184

185185
defp generate_sidebar_items(nodes_map, extras, config) do
186-
content = Templates.create_sidebar_items(nodes_map, extras)
186+
content =
187+
Templates.create_sidebar_items(nodes_map, extras, config.custom_autocompletions || [])
188+
187189
path = "dist/sidebar_items-#{digest(content)}.js"
188190
File.write!(Path.join(config.output, path), content)
189191
[path]

Diff for: lib/ex_doc/formatter/html/templates.ex

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,13 @@ defmodule ExDoc.Formatter.HTML.Templates do
7070
@doc """
7171
Create a JS object which holds all the items displayed in the sidebar area
7272
"""
73-
def create_sidebar_items(nodes_map, extras) do
73+
def create_sidebar_items(nodes_map, extras, custom_autocompletions \\ []) do
7474
nodes =
7575
nodes_map
7676
|> Enum.map(&sidebar_module/1)
7777
|> Map.new()
7878
|> Map.put(:extras, sidebar_extras(extras))
79+
|> Map.put(:custom, custom_autocompletions)
7980

8081
["sidebarNodes=" | ExDoc.Utils.to_json(nodes)]
8182
end

Diff for: lib/mix/tasks/docs.ex

+50-1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ defmodule Mix.Tasks.Docs do
102102
the "assets" directory in the output path under the name "cover" and the
103103
appropriate extension. This option has no effect when using the "html" formatter.
104104
105+
* `:custom_autocompletions` - A list of maps or keywords representing custom autocomplete
106+
results that will appear in the search. See the "Customizing Search" section below for more.
107+
105108
* `:deps` - A keyword list application names and their documentation URL.
106109
ExDoc will by default include all dependencies and assume they are hosted on
107110
HexDocs. This can be overridden by your own values. Example: `[plug: "https://myserver/plug/"]`
@@ -153,7 +156,8 @@ defmodule Mix.Tasks.Docs do
153156
May be overridden by command line argument.
154157
155158
* `:redirects` - A map or list of tuples, where the key is the path to redirect from and the
156-
value is the path to redirect to. The extension is omitted in both cases, i.e `%{"old-readme" => "readme"}`
159+
value is the path to redirect to. The extension is omitted in both cases, i.e `%{"old-readme" => "readme"}`.
160+
See the "Changing documentation over time" section below for more.
157161
158162
* `:skip_undefined_reference_warnings_on` - ExDoc warns when it can't create a `Mod.fun/arity`
159163
reference in the current project docs e.g. because of a typo. This list controls where to
@@ -337,6 +341,51 @@ defmodule Mix.Tasks.Docs do
337341
attention to them in the docs, you should probably use `:groups_for_modules`
338342
(which can be used in conjunction with `:nest_modules_by_prefix`).
339343
344+
## Changing documentation over time
345+
346+
As your project grows, your documentation may very likely change, even structurally. There are a few important things to consider in this regard:
347+
348+
- Links to your *extras* will break if you change or move file names.
349+
- Links to your *modules, and mix tasks* will change if you change their name.
350+
- Links to *functions* are actually links to modules with anchor links. If you change the function name, the link does
351+
not break but will leave users at the top of the module's documentation.
352+
353+
Because these docs are static files, the behavior of a missing page will depend on where they are hosted.
354+
In particular, [hexdocs.pm](https://hexdocs.pm) will show a 404 page.
355+
356+
You can improve the developer experience on everything but function names changing
357+
by using the `redirects` configuration. For example, if you changed the module `MyApp.MyModule`
358+
to `MyApp.My.Module` and the extra `get-started.md` to `quickstart.md`, you can
359+
setup the following redirects:
360+
361+
redirects: %{
362+
"MyApp.MyModule" => "MyApp.My.Module",
363+
"get-started" => "quickstart"
364+
}
365+
366+
## Customizing Search
367+
368+
In ExDoc, there are two kinds of searches. There is the "autocomplete", which is what shows up when you type in the top search bar,
369+
and "search" which is the separate page with results that you arrive at if you submit the search bar without selecting an autocompleted
370+
item. Currently, only the autocompletions are customizable.
371+
372+
You can add to the available autocompletions by specifying the `custom_autocompletions` option. This must be a list of
373+
maps or keyword lists with the following shape:
374+
375+
custom_autocompletions: [
376+
%{
377+
link: "a-page.html#anchor",
378+
title: "custom-text",
379+
description: "Some Custom Text",
380+
labels: ["Text"]
381+
}
382+
]
383+
384+
- `link` is expected to be a relative link to a page in your documentation. You may user anchor links.
385+
- `title` is the term that will be searched, and what will be shown as the primary text in the search result.
386+
- `description` is text that will be shown below the search result
387+
- `labels` will be shown as badges next to that content.
388+
340389
## Umbrella project
341390
342391
ExDoc can be used in an umbrella project and generates a single documentation

Diff for: test/ex_doc/formatter/html_test.exs

+28
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,34 @@ defmodule ExDoc.Formatter.HTMLTest do
592592
] = Jason.decode!(content)["extras"]
593593
end
594594

595+
test "custom autocompletions can be added", %{tmp_dir: tmp_dir} = context do
596+
generate_docs(
597+
doc_config(context,
598+
source_beam: "unknown",
599+
extras: [],
600+
custom_autocompletions: [
601+
%{
602+
link: "a-page.html#anchor",
603+
title: "custom-text",
604+
description: "Some Custom Text",
605+
labels: ["Text"]
606+
}
607+
]
608+
)
609+
)
610+
611+
"sidebarNodes=" <> content = read_wildcard!(tmp_dir <> "/html/dist/sidebar_items-*.js")
612+
613+
assert [
614+
%{
615+
"link" => "a-page.html#anchor",
616+
"title" => "custom-text",
617+
"description" => "Some Custom Text",
618+
"labels" => ["Text"]
619+
}
620+
] = Jason.decode!(content)["custom"]
621+
end
622+
595623
test "containing settext headers while discarding links on header",
596624
%{tmp_dir: tmp_dir} = context do
597625
generate_docs(

0 commit comments

Comments
 (0)