diff --git a/lib/ex_doc.ex b/lib/ex_doc.ex
index f9a98a6c8..0568c874d 100644
--- a/lib/ex_doc.ex
+++ b/lib/ex_doc.ex
@@ -249,6 +249,10 @@ defmodule ExDoc do
* `:title` - The title of the extra page. If not provided, the title will be inferred from the extra name.
* `:url` - The external url to link to from the sidebar.
+ Bare filenames such as `[Intro](intro.md)` use the legacy filename-based lookup against the flattened output.
+ Links with a directory component, such as `[Intro](guides/intro.md)`, `[Intro](../guides/intro.md)`, or
+ `[Intro](/guides/intro.md)`, are resolved against the extra source path (or project root for `/`).
+
### Customizing search data
It is possible to fully customize the way a given extra is indexed, both in autocomplete and in search.
diff --git a/lib/ex_doc/autolink.ex b/lib/ex_doc/autolink.ex
index 8fa342b70..0cd2a54be 100644
--- a/lib/ex_doc/autolink.ex
+++ b/lib/ex_doc/autolink.ex
@@ -52,7 +52,7 @@ defmodule ExDoc.Autolink do
:language,
file: "nofile",
apps: [],
- extras: [],
+ extras: %{},
deps: [],
ext: ".html",
current_kfa: nil,
@@ -217,7 +217,7 @@ defmodule ExDoc.Autolink do
with %{scheme: nil, host: nil, path: path} = uri <- URI.parse(link),
true <- is_binary(path) and path != "" and not (path =~ ref_regex()),
true <- Path.extname(path) in @builtin_ext do
- if file = config.extras[Path.basename(path)] do
+ if file = resolve_extra_target(path, config) do
append_fragment(file <> config.ext, uri.fragment)
else
maybe_warn(config, nil, nil, %{file_path: path, original_text: link})
@@ -228,6 +228,26 @@ defmodule ExDoc.Autolink do
end
end
+ defp resolve_extra_target(path, config) do
+ filename = Path.basename(path)
+
+ case path do
+ "/" <> absolute_path ->
+ config.extras[absolute_path]
+
+ ^filename ->
+ config.extras[filename]
+
+ relative_path ->
+ path =
+ relative_path
+ |> Path.expand(Path.dirname(config.file))
+ |> Path.relative_to_cwd()
+
+ config.extras[path]
+ end
+ end
+
defp maybe_remove_link(nil, :custom_link) do
:remove_link
end
diff --git a/lib/ex_doc/formatter.ex b/lib/ex_doc/formatter.ex
index 6ce1e73af..8998802a4 100644
--- a/lib/ex_doc/formatter.ex
+++ b/lib/ex_doc/formatter.ex
@@ -314,7 +314,10 @@ defmodule ExDoc.Formatter do
%ExDoc.ExtraNode{source_path: source_path, id: id}, acc when is_binary(source_path) ->
base = Path.basename(source_path)
- Map.put(acc, base, id)
+
+ acc
+ |> Map.put(source_path, id)
+ |> Map.put(base, id)
_extra, acc ->
acc
diff --git a/test/ex_doc/language/elixir_test.exs b/test/ex_doc/language/elixir_test.exs
index 4023708c1..1d2e26f77 100644
--- a/test/ex_doc/language/elixir_test.exs
+++ b/test/ex_doc/language/elixir_test.exs
@@ -259,7 +259,9 @@ defmodule ExDoc.Language.ElixirTest do
test "extras" do
opts = [
+ file: "guides/current.md",
extras: %{
+ "guide/Foo Bar.md" => "foo-bar",
"Foo Bar.md" => "foo-bar",
"Bar Baz.livemd" => "bar-baz",
"Bar Baz.cheatmd" => "bar-baz"
@@ -286,6 +288,45 @@ defmodule ExDoc.Language.ElixirTest do
assert autolink_doc("[Foo](#baz)", opts) == ~s|Foo|
end
+ test "path-qualified extra links use the extra source path" do
+ opts = [
+ file: "guides/current.md",
+ extras: %{"guides/Foo Bar.md" => "foo-bar", "Foo Bar.md" => "legacy-foo"}
+ ]
+
+ assert autolink_doc("[Foo](./Foo Bar.md)", opts) ==
+ ~s|Foo|
+
+ assert autolink_doc("[Foo](../guides/Foo Bar.md)", opts) ==
+ ~s|Foo|
+
+ assert autolink_doc("[Foo](/guides/Foo Bar.md)", opts) ==
+ ~s|Foo|
+ end
+
+ test "bare filename extra links use legacy lookup" do
+ opts = [
+ file: "guides/current.md",
+ extras: %{"guides/Foo Bar.md" => "relative-foo", "Foo Bar.md" => "legacy-foo"}
+ ]
+
+ assert autolink_doc("[Foo](Foo Bar.md)", opts) ==
+ ~s|Foo|
+ end
+
+ test "extras with bad directories warn instead of silently matching by basename" do
+ opts = [
+ warnings: :send,
+ file: "guides/current.md",
+ extras: %{"guide/Foo Bar.md" => "foo-bar", "Foo Bar.md" => "foo-bar"}
+ ]
+
+ assert warn(fn ->
+ assert autolink_doc("[Foo](/bad_dir/Foo Bar.md)", opts) ==
+ ~s|Foo|
+ end) =~ ~s|documentation references file "/bad_dir/Foo Bar.md" but it does not exist|
+ end
+
test "special case links" do
assert autolink_doc("`//2`") ==
~s|//2|
diff --git a/test/ex_doc/language/erlang_test.exs b/test/ex_doc/language/erlang_test.exs
index a6caf2531..37bc91735 100644
--- a/test/ex_doc/language/erlang_test.exs
+++ b/test/ex_doc/language/erlang_test.exs
@@ -669,6 +669,16 @@ defmodule ExDoc.Language.ErlangTest do
extras: %{"Foo Bar.md" => "foo-bar", "Bar Baz.livemd" => "bar-baz"}
]
+ @relative_opts [
+ file: "guides/current.md",
+ extras: %{
+ "guide/Foo Bar.md" => "foo-bar",
+ "guide/Bar Baz.livemd" => "bar-baz",
+ "Foo Bar.md" => "foo-bar",
+ "Bar Baz.livemd" => "bar-baz"
+ }
+ ]
+
test "extras", c do
assert autolink_doc("[Foo](Foo Bar.md)", c, @opts) ==
~s|Foo|
@@ -690,7 +700,7 @@ defmodule ExDoc.Language.ErlangTest do
end
test "extras relative", c do
- assert autolink_doc("[Foo](../guide/Foo Bar.md)", c, @opts) ==
+ assert autolink_doc("[Foo](../guide/Foo Bar.md)", c, @relative_opts) ==
~s|Foo|
end
end