Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions lib/ex_doc/retriever.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule ExDoc.Retriever do
# Functions to extract documentation information from modules.
@moduledoc false

alias ExDoc.Utils.NaturalOrder

defmodule Error do
@moduledoc false
defexception [:message]
Expand Down Expand Up @@ -128,20 +130,24 @@ defmodule ExDoc.Retriever do
type: module_data.type,
deprecated: metadata[:deprecated],
function_groups: function_groups,
docs: Enum.sort_by(docs, &sort_key(&1.name, &1.arity)),
docs: natural_sort(docs, &"#{&1.name}/#{&1.arity}"),
doc: moduledoc,
doc_line: doc_line,
typespecs: Enum.sort_by(types, &{&1.name, &1.arity}),
typespecs: natural_sort(types, &"#{&1.name}/#{&1.arity}"),
source_path: source_path,
source_url: source_link(source, module_data.line),
language: module_data.language,
annotations: List.wrap(metadata[:tags])
}
end

defp sort_key(name, arity) do
first = name |> Atom.to_charlist() |> hd()
{first in ?a..?z, name, arity}
defp natural_sort(enumerable, mapper) when is_function(mapper, 1) do
enumerable
|> Enum.sort_by(fn elem ->
elem
|> mapper.()
|> NaturalOrder.to_sortable_charlist()
end)
end

defp doc_ast(format, %{"en" => doc_content}, options) do
Expand Down Expand Up @@ -201,7 +207,7 @@ defmodule ExDoc.Retriever do
deprecated: metadata[:deprecated],
doc: doc_ast,
doc_line: doc_line,
defaults: Enum.sort_by(defaults, fn {name, arity} -> sort_key(name, arity) end),
defaults: natural_sort(defaults, fn {name, arity} -> "#{name}/#{arity}" end),
signature: signature(signature),
specs: function_data.specs,
source_path: source.path,
Expand Down
28 changes: 28 additions & 0 deletions lib/ex_doc/utils/natural_order.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule ExDoc.Utils.NaturalOrder do
@moduledoc false

def to_sortable_charlist(string) do
string
|> :unicode.characters_to_nfkd_list()
|> make_sortable()
end

@offset -1_000_000_000

# Numbers come first, so group and pad them with offset
defp make_sortable([digit | chars]) when digit in ?0..?9 do
{digits, chars} = Enum.split_while(chars, &(&1 in ?0..?9))
[@offset + List.to_integer([digit | digits]) | make_sortable(chars)]
end

# Then underscore
defp make_sortable([?_ | chars]), do: [?0 | make_sortable(chars)]

# Then uppercased letters and lowercased letters
defp make_sortable([char | chars]) when char in ?a..?z do
[char - 31.5 | make_sortable(chars)]
end

defp make_sortable([char | chars]), do: [char | make_sortable(chars)]
defp make_sortable([]), do: []
end
35 changes: 35 additions & 0 deletions test/ex_doc/retriever_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,39 @@ defmodule ExDoc.RetrieverTest do
assert a.id == "A"
assert a_a.id == "A.A"
end

test "natural sorting", c do
elixirc(c, ~S"""
defmodule NaturallySorted do
@type type_b :: any()
@type type_B :: any()
@type type_A :: any()
@type type_a :: any()

def function_b(), do: :ok

def function_B(), do: :ok

def function_A(), do: :ok

def function_a(), do: :ok

def function_A(arg), do: arg

def function_a(arg), do: arg
end
""")

[mod] = Retriever.docs_from_modules([NaturallySorted], %ExDoc.Config{})

[function_A_0, function_A_1, function_a_0, function_a_1, function_B_0, function_b_0] =
mod.docs

assert function_A_0.id == "function_A/0"
assert function_A_1.id == "function_A/1"
assert function_a_0.id == "function_a/0"
assert function_a_1.id == "function_a/1"
assert function_B_0.id == "function_B/0"
assert function_b_0.id == "function_b/0"
end
end