Skip to content

Commit 2b4364e

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

5 files changed

Lines changed: 99 additions & 5 deletions

File tree

lib/ex_doc.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,11 @@ 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+
Links between extras are resolved against the source extra path, not against the flattened output path.
253+
Bare filenames such as `[Intro](intro.md)` still match by filename for compatibility, but links that include
254+
directories must point to an existing extra source path after normalization, such as
255+
`[Intro](../guides/intro.md)` or `[Intro](/guides/intro.md)`.
256+
252257
### Customizing search data
253258
254259
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: 43 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_path(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,47 @@ defmodule ExDoc.Autolink do
228228
end
229229
end
230230

231+
defp resolve_extra_path(path, config) do
232+
has_directory? = path != Path.basename(path)
233+
234+
exact_match = maybe_resolve_extra_source_path(path, config) |> then(&config.extras[&1])
235+
236+
cond do
237+
exact_match ->
238+
exact_match
239+
240+
has_directory? ->
241+
nil
242+
243+
true ->
244+
config.extras[Path.basename(path)]
245+
end
246+
end
247+
248+
defp maybe_resolve_extra_source_path(path, config) do
249+
cond do
250+
String.starts_with?(path, "/") ->
251+
normalize_extra_source_path(String.trim_leading(path, "/"), File.cwd!())
252+
253+
true ->
254+
normalize_extra_source_path(path, base_dir_for_extra_resolution(config.file))
255+
end
256+
end
257+
258+
defp normalize_extra_source_path(path, base_dir) do
259+
path
260+
|> Path.expand(base_dir)
261+
|> Path.relative_to(File.cwd!())
262+
end
263+
264+
defp base_dir_for_extra_resolution(file) when is_binary(file) do
265+
file
266+
|> Path.expand(File.cwd!())
267+
|> Path.dirname()
268+
end
269+
270+
defp base_dir_for_extra_resolution(_), do: File.cwd!()
271+
231272
defp maybe_remove_link(nil, :custom_link) do
232273
:remove_link
233274
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: 28 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,32 @@ defmodule ExDoc.Language.ElixirTest do
286288
assert autolink_doc("[Foo](#baz)", opts) == ~s|<a href="#baz">Foo</a>|
287289
end
288290

291+
test "extras relative paths use the extra source path" do
292+
opts = [
293+
file: "guides/current.md",
294+
extras: %{"guide/Foo Bar.md" => "foo-bar", "Foo Bar.md" => "foo-bar"}
295+
]
296+
297+
assert autolink_doc("[Foo](../guide/Foo Bar.md)", opts) ==
298+
~s|<a href="foo-bar.html">Foo</a>|
299+
300+
assert autolink_doc("[Foo](/guide/Foo Bar.md)", opts) ==
301+
~s|<a href="foo-bar.html">Foo</a>|
302+
end
303+
304+
test "extras with bad directories warn instead of silently matching by basename" do
305+
opts = [
306+
warnings: :send,
307+
file: "guides/current.md",
308+
extras: %{"guide/Foo Bar.md" => "foo-bar", "Foo Bar.md" => "foo-bar"}
309+
]
310+
311+
assert warn(fn ->
312+
assert autolink_doc("[Foo](/bad_dir/Foo Bar.md)", opts) ==
313+
~s|<a href="/bad_dir/Foo Bar.md">Foo</a>|
314+
end) =~ ~s|documentation references file "/bad_dir/Foo Bar.md" but it does not exist|
315+
end
316+
289317
test "special case links" do
290318
assert autolink_doc("`//2`") ==
291319
~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)