Skip to content
Closed
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
162 changes: 68 additions & 94 deletions lib/ex_doc.ex
Original file line number Diff line number Diff line change
@@ -1,98 +1,83 @@
defmodule ExDoc do
@moduledoc false
@ex_doc_version Mix.Project.config()[:version]

alias ExDoc.Config

@doc """
Returns the ExDoc version (used in templates).
"""
@doc "Returns the ExDoc version. (Used in templates.)"
@spec version :: String.t()

@ex_doc_version Mix.Project.config()[:version]
def version, do: @ex_doc_version

@doc """
Generates documentation for the given `project`, `vsn` (version)
and `options`.
"""
@doc "Generates docs for the given `project`, `vsn` (version) & `options`."
@spec generate_docs(String.t(), String.t(), Keyword.t()) :: atom
def generate_docs(project, vsn, options)
when is_binary(project) and is_binary(vsn) and is_list(options) do
config = build_config(project, vsn, options)
def generate_docs(project, version, options)
when is_binary(project) and is_binary(version) and is_list(options) do
options = normalize_options(options)

if processor = options[:markdown_processor] do
ExDoc.Markdown.put_markdown_processor(processor)
end
config =
struct(
%Config{
project: project,
version: version,
source_root: Keyword.get(options, :source_root, File.cwd!())
},
options
)

if markdown_processor_options = options[:markdown_processor_options] do
ExDoc.Markdown.configure_processor(markdown_processor_options)
end
# two side-effects
ExDoc.Markdown.put_markdown_processor(options[:markdown_processor])
ExDoc.Markdown.configure_processor(options[:markdown_processor_options])

docs = config.retriever.docs_from_dir(config.source_beam, config)
find_formatter(config.formatter).run(docs, config)
# below `normalize_module_nesting_prefixes/1`
end

# Builds configuration by merging `options`, and normalizing the options.
@spec build_config(String.t(), String.t(), Keyword.t()) :: ExDoc.Config.t()
defp build_config(project, vsn, options) do
options = normalize_options(options)

preconfig = %Config{
project: project,
version: vsn,
main: options[:main],
homepage_url: options[:homepage_url],
source_root: options[:source_root] || File.cwd!()
}

struct(preconfig, options)
end

# Short path for programmatic interface
defp find_formatter(modname) when is_atom(modname), do: modname
defp normalize_options(options) do
pattern =
options[:source_url_pattern] ||
guess_url(options[:source_url], options[:source_ref] || ExDoc.Config.default_source_ref())

defp find_formatter("ExDoc.Formatter." <> _ = name) do
[name]
|> Module.concat()
|> check_formatter_module(name)
end
Keyword.put(options, :source_url_pattern, pattern)
|> normalize_output()

defp find_formatter(name) do
[ExDoc.Formatter, String.upcase(name)]
|> Module.concat()
|> check_formatter_module(name)
# below `append_slash/1`
end

defp check_formatter_module(modname, argname) do
if Code.ensure_loaded?(modname) do
modname
defp guess_url(url, ref) do
with {:ok, host_with_path} <- http_or_https(url),
{:ok, pattern} <- known_pattern(host_with_path, ref) do
"https://" <> append_slash(host_with_path) <> pattern
else
raise "formatter module #{inspect(argname)} not found"
_ -> url
end
end

# Helpers
defp http_or_https("http://" <> rest), do: {:ok, rest}
defp http_or_https("https://" <> rest), do: {:ok, rest}
defp http_or_https(_), do: :error

defp normalize_options(options) do
pattern =
options[:source_url_pattern] ||
guess_url(options[:source_url], options[:source_ref] || ExDoc.Config.default_source_ref())
defp known_pattern("github.com/" <> _, ref), do: {:ok, "blob/#{ref}/%{path}#L%{line}"}
defp known_pattern("gitlab.com/" <> _, ref), do: {:ok, "blob/#{ref}/%{path}#L%{line}"}
defp known_pattern("bitbucket.org/" <> _, ref), do: {:ok, "src/#{ref}/%{path}#cl-%{line}"}
defp known_pattern(_host_with_path, _ref), do: :error

options
|> Keyword.put(:source_url_pattern, pattern)
|> normalize_output()
|> normalize_module_nesting_prefixes()
end
defp append_slash(url), do: if(:binary.last(url) == ?/, do: url, else: url <> "/")

defp normalize_output(options) do
if is_binary(options[:output]) do
Keyword.put(options, :output, String.trim_trailing(options[:output], "/"))
output = options[:output]

if is_binary(output) do
Keyword.put(options, :output, String.trim_trailing(output, "/"))
else
options
end
|> normalize_module_nesting_prefixes()
end

# Sorts `:nest_modules_by_prefix` in descending order. Helps to find longest match.
defp normalize_module_nesting_prefixes(options) do
# sort in descending order to facilitate finding longest match
normalized_prefixes =
options
|> Keyword.get(:nest_modules_by_prefix, [])
Expand All @@ -103,37 +88,26 @@ defmodule ExDoc do
Keyword.put(options, :nest_modules_by_prefix, normalized_prefixes)
end

defp guess_url(url, ref) do
with {:ok, host_with_path} <- http_or_https(url),
{:ok, pattern} <- known_pattern(host_with_path, ref) do
"https://" <> append_slash(host_with_path) <> pattern
else
_ -> url
end
end

defp http_or_https("http://" <> rest),
do: {:ok, rest}

defp http_or_https("https://" <> rest),
do: {:ok, rest}

defp http_or_https(_),
do: :error

defp known_pattern("github.com/" <> _, ref),
do: {:ok, "blob/#{ref}/%{path}#L%{line}"}

defp known_pattern("gitlab.com/" <> _, ref),
do: {:ok, "blob/#{ref}/%{path}#L%{line}"}

defp known_pattern("bitbucket.org/" <> _, ref),
do: {:ok, "src/#{ref}/%{path}#cl-%{line}"}

defp known_pattern(_host_with_path, _ref),
do: :error

defp append_slash(url) do
if :binary.last(url) == ?/, do: url, else: url <> "/"
end
# Short path for programmatic interface
defp find_formatter(modname) when is_atom(modname),
do: modname

defp find_formatter("ExDoc.Formatter." <> _ = name),
do:
[name]
|> Module.concat()
|> check_formatter_module(name)

defp find_formatter(name),
do:
[ExDoc.Formatter, String.upcase(name)]
|> Module.concat()
|> check_formatter_module(name)

defp check_formatter_module(modname, argname),
do:
if(Code.ensure_loaded?(modname),
do: modname,
else: raise("formatter module #{inspect(argname)} not found")
)
end
11 changes: 3 additions & 8 deletions lib/ex_doc/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@ defmodule ExDoc.Config do
@moduledoc false

@default_formatter "html"
@default_source_ref "master"

def default_source_ref do
@default_source_ref
end
def default_formatter, do: @default_formatter

def default_formatter do
@default_formatter
end
@default_source_ref "master"
def default_source_ref, do: @default_source_ref

def before_closing_head_tag(_), do: ""
def before_closing_body_tag(_), do: ""
Expand Down
21 changes: 9 additions & 12 deletions lib/ex_doc/markdown.ex
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,17 @@ defmodule ExDoc.Markdown do
end
end

@doc "Changes the markdown processor globally."
def put_markdown_processor(processor),
do: if(processor, do: Application.put_env(:ex_doc, @markdown_processor_key, processor))

@doc """
Changes the markdown processor globally.
This function configures the markdown processor with the given options.
It's called exactly once when ExDoc reads its own configuration options.
It's supposed to be called for its side-effects.
"""
def put_markdown_processor(processor) do
Application.put_env(:ex_doc, @markdown_processor_key, processor)
end

@doc false
def configure_processor(options) do
# This function configures the markdown processor with the given options.
# It's called exactly once when ExDoc reads its own configuration options.
# It's supposed to be called for its side-effects.
get_markdown_processor().configure(options)
end
def configure_processor(options),
do: if(options, do: get_markdown_processor().configure(options))

defp find_markdown_processor do
Enum.find(@markdown_processors, fn module ->
Expand Down