From f3db9b1037bdd28d3d2847ccdd74e16609d08868 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Mon, 28 Feb 2022 15:21:09 -0500 Subject: [PATCH 1/2] Sort items "naturally" in menu, summary, function list, etc. Closes https://github.com/elixir-lang/ex_doc/issues/1440 --- lib/ex_doc/retriever.ex | 18 ++++++++++------ lib/ex_doc/utils/natural_order.ex | 26 +++++++++++++++++++++++ test/ex_doc/retriever_test.exs | 35 +++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 lib/ex_doc/utils/natural_order.ex diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex index 9467b489b..875c72fe6 100644 --- a/lib/ex_doc/retriever.ex +++ b/lib/ex_doc/retriever.ex @@ -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] @@ -128,10 +130,10 @@ 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, @@ -139,9 +141,13 @@ defmodule ExDoc.Retriever do } 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 @@ -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, diff --git a/lib/ex_doc/utils/natural_order.ex b/lib/ex_doc/utils/natural_order.ex new file mode 100644 index 000000000..ff7e5ffee --- /dev/null +++ b/lib/ex_doc/utils/natural_order.ex @@ -0,0 +1,26 @@ +defmodule ExDoc.Utils.NaturalOrder do + 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 diff --git a/test/ex_doc/retriever_test.exs b/test/ex_doc/retriever_test.exs index 5d02a7cb9..ab646d8cc 100644 --- a/test/ex_doc/retriever_test.exs +++ b/test/ex_doc/retriever_test.exs @@ -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 From d749e9c88dc33f42903e5e9e247e08d0bea8678a Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Mon, 28 Feb 2022 22:47:00 -0500 Subject: [PATCH 2/2] Add @moduledoc false to ExDoc.Utils.NaturalOrder --- lib/ex_doc/utils/natural_order.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ex_doc/utils/natural_order.ex b/lib/ex_doc/utils/natural_order.ex index ff7e5ffee..41e1a3b29 100644 --- a/lib/ex_doc/utils/natural_order.ex +++ b/lib/ex_doc/utils/natural_order.ex @@ -1,4 +1,6 @@ defmodule ExDoc.Utils.NaturalOrder do + @moduledoc false + def to_sortable_charlist(string) do string |> :unicode.characters_to_nfkd_list()