Skip to content

Commit 25c7159

Browse files
committed
Auto link extras: fail on bad path
1 parent 9e5899a commit 25c7159

5 files changed

Lines changed: 82 additions & 4 deletions

File tree

lib/ex_doc.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ defmodule ExDoc do
249249
* `:title` - The title of the extra page. If not provided, the title will be inferred from the extra name.
250250
* `:url` - The external url to link to from the sidebar.
251251
252+
Bare filenames such as `[Intro](intro.md)` use the legacy filename-based lookup against the flattened output.
253+
Links with a directory component, such as `[Intro](guides/intro.md)`, `[Intro](../guides/intro.md)`, or
254+
`[Intro](/guides/intro.md)`, are resolved against the extra source path (or project root for `/`).
255+
252256
### Customizing search data
253257
254258
It is possible to fully customize the way a given extra is indexed, both in autocomplete and in search.

lib/ex_doc/autolink.ex

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ defmodule ExDoc.Autolink do
5252
:language,
5353
file: "nofile",
5454
apps: [],
55-
extras: [],
55+
extras: %{},
5656
deps: [],
5757
ext: ".html",
5858
current_kfa: nil,
@@ -217,7 +217,7 @@ defmodule ExDoc.Autolink do
217217
with %{scheme: nil, host: nil, path: path} = uri <- URI.parse(link),
218218
true <- is_binary(path) and path != "" and not (path =~ ref_regex()),
219219
true <- Path.extname(path) in @builtin_ext do
220-
if file = config.extras[Path.basename(path)] do
220+
if file = resolve_extra_target(path, config) do
221221
append_fragment(file <> config.ext, uri.fragment)
222222
else
223223
maybe_warn(config, nil, nil, %{file_path: path, original_text: link})
@@ -228,6 +228,26 @@ defmodule ExDoc.Autolink do
228228
end
229229
end
230230

231+
defp resolve_extra_target(path, config) do
232+
filename = Path.basename(path)
233+
234+
case path do
235+
"/" <> absolute_path ->
236+
config.extras[absolute_path]
237+
238+
^filename ->
239+
config.extras[filename]
240+
241+
relative_path ->
242+
path =
243+
relative_path
244+
|> Path.expand(Path.dirname(config.file))
245+
|> Path.relative_to_cwd()
246+
247+
config.extras[path]
248+
end
249+
end
250+
231251
defp maybe_remove_link(nil, :custom_link) do
232252
:remove_link
233253
end

lib/ex_doc/formatter.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,10 @@ defmodule ExDoc.Formatter do
314314

315315
%ExDoc.ExtraNode{source_path: source_path, id: id}, acc when is_binary(source_path) ->
316316
base = Path.basename(source_path)
317-
Map.put(acc, base, id)
317+
318+
acc
319+
|> Map.put(source_path, id)
320+
|> Map.put(base, id)
318321

319322
_extra, acc ->
320323
acc

test/ex_doc/language/elixir_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,9 @@ defmodule ExDoc.Language.ElixirTest do
259259

260260
test "extras" do
261261
opts = [
262+
file: "guides/current.md",
262263
extras: %{
264+
"guide/Foo Bar.md" => "foo-bar",
263265
"Foo Bar.md" => "foo-bar",
264266
"Bar Baz.livemd" => "bar-baz",
265267
"Bar Baz.cheatmd" => "bar-baz"
@@ -286,6 +288,45 @@ defmodule ExDoc.Language.ElixirTest do
286288
assert autolink_doc("[Foo](#baz)", opts) == ~s|<a href="#baz">Foo</a>|
287289
end
288290

291+
test "path-qualified extra links use the extra source path" do
292+
opts = [
293+
file: "guides/current.md",
294+
extras: %{"guides/Foo Bar.md" => "foo-bar", "Foo Bar.md" => "legacy-foo"}
295+
]
296+
297+
assert autolink_doc("[Foo](./Foo Bar.md)", opts) ==
298+
~s|<a href="foo-bar.html">Foo</a>|
299+
300+
assert autolink_doc("[Foo](../guides/Foo Bar.md)", opts) ==
301+
~s|<a href="foo-bar.html">Foo</a>|
302+
303+
assert autolink_doc("[Foo](/guides/Foo Bar.md)", opts) ==
304+
~s|<a href="foo-bar.html">Foo</a>|
305+
end
306+
307+
test "bare filename extra links use legacy lookup" do
308+
opts = [
309+
file: "guides/current.md",
310+
extras: %{"guides/Foo Bar.md" => "relative-foo", "Foo Bar.md" => "legacy-foo"}
311+
]
312+
313+
assert autolink_doc("[Foo](Foo Bar.md)", opts) ==
314+
~s|<a href="legacy-foo.html">Foo</a>|
315+
end
316+
317+
test "extras with bad directories warn instead of silently matching by basename" do
318+
opts = [
319+
warnings: :send,
320+
file: "guides/current.md",
321+
extras: %{"guide/Foo Bar.md" => "foo-bar", "Foo Bar.md" => "foo-bar"}
322+
]
323+
324+
assert warn(fn ->
325+
assert autolink_doc("[Foo](/bad_dir/Foo Bar.md)", opts) ==
326+
~s|<a href="/bad_dir/Foo Bar.md">Foo</a>|
327+
end) =~ ~s|documentation references file "/bad_dir/Foo Bar.md" but it does not exist|
328+
end
329+
289330
test "special case links" do
290331
assert autolink_doc("`//2`") ==
291332
~s|<a href="https://hexdocs.pm/elixir/Kernel.html#//2"><code class="inline">//2</code></a>|

test/ex_doc/language/erlang_test.exs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,16 @@ defmodule ExDoc.Language.ErlangTest do
669669
extras: %{"Foo Bar.md" => "foo-bar", "Bar Baz.livemd" => "bar-baz"}
670670
]
671671

672+
@relative_opts [
673+
file: "guides/current.md",
674+
extras: %{
675+
"guide/Foo Bar.md" => "foo-bar",
676+
"guide/Bar Baz.livemd" => "bar-baz",
677+
"Foo Bar.md" => "foo-bar",
678+
"Bar Baz.livemd" => "bar-baz"
679+
}
680+
]
681+
672682
test "extras", c do
673683
assert autolink_doc("[Foo](Foo Bar.md)", c, @opts) ==
674684
~s|<a href="foo-bar.html">Foo</a>|
@@ -690,7 +700,7 @@ defmodule ExDoc.Language.ErlangTest do
690700
end
691701

692702
test "extras relative", c do
693-
assert autolink_doc("[Foo](../guide/Foo Bar.md)", c, @opts) ==
703+
assert autolink_doc("[Foo](../guide/Foo Bar.md)", c, @relative_opts) ==
694704
~s|<a href="foo-bar.html">Foo</a>|
695705
end
696706
end

0 commit comments

Comments
 (0)