Skip to content

Commit ac71531

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

5 files changed

Lines changed: 102 additions & 5 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: 34 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,38 @@ defmodule ExDoc.Autolink do
228228
end
229229
end
230230

231+
defp resolve_extra_target(path, config) do
232+
path_qualified_link? = path != Path.basename(path)
233+
234+
if path_qualified_link? do
235+
config.extras[normalize_extra_link_path(path, config.file)]
236+
else
237+
config.extras[Path.basename(path)]
238+
end
239+
end
240+
241+
defp normalize_extra_link_path("/" <> path, _current_file) do
242+
normalize_extra_link_path_from(path, File.cwd!())
243+
end
244+
245+
defp normalize_extra_link_path(path, current_file) do
246+
normalize_extra_link_path_from(path, extra_link_base_dir(current_file))
247+
end
248+
249+
defp normalize_extra_link_path_from(path, base_dir) do
250+
path
251+
|> Path.expand(base_dir)
252+
|> Path.relative_to(File.cwd!())
253+
end
254+
255+
defp extra_link_base_dir(file) when is_binary(file) do
256+
file
257+
|> Path.expand(File.cwd!())
258+
|> Path.dirname()
259+
end
260+
261+
defp extra_link_base_dir(_), do: File.cwd!()
262+
231263
defp maybe_remove_link(nil, :custom_link) do
232264
:remove_link
233265
end

lib/ex_doc/formatter.ex

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,21 @@ defmodule ExDoc.Formatter do
313313
acc
314314

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

319323
_extra, acc ->
320324
acc
321325
end)
322326
end
327+
328+
defp normalize_extra_path(path) do
329+
path
330+
|> Path.relative_to(File.cwd!())
331+
|> String.replace_leading("./", "")
332+
end
323333
end

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)