Packages

Extractly `mix xtra` task to render `EEx` templates with easy access to hexdocs. The Extractly module gives easy access to Elixir metainformation of the application using the extractly package, notably, module and function documentation.

Current section

Files

Jump to
extractly lib extractly.ex
Raw

lib/extractly.ex

defmodule Extractly do
alias Extractly.DoNotEdit
import Extractly.Helpers
@moduledoc """
Provide easy access to information inside the templates rendered by `mix xtra`
"""
@doc """
Emits a comment including a message not to edit the created file, as it will be recreated from this template.
It is a convenience to include this into your templates as follows
<%= xtra.do_not_edit_warning %>
or I18n'ed
<%= xtra.do_not_edit_warning, lang: :fr %>
If you are not generating html or markdown the comment can be parametrized
<%= xtra.do_not_edit_warning, comment_start: "-- ", comment_end: "" %>
If you want to include the name of the source template use `template: template` option, so
a call may be as complex as:
<%= xtra.do_not_edit_warning, comment_start: "-- ", comment_end: "", template: template, lang: :it %>
"""
def do_not_edit_warning(opts \\ []), do: DoNotEdit.warning(opts)
@doc ~S"""
Returns docstring of a function
Ex:
iex(1)> {:ok, lines} = Extractly.functiondoc("Extractly.moduledoc/2") |> hd()
...(1)> lines |> String.split("\n") |> Enum.take(3)
[" Returns docstring of a module", "", " E.g. verbatim"]
We can also pass a list of functions to get their docs concatenated
iex(2)> [{:ok, moduledoc}, {:error, message}] = Extractly.functiondoc(["Extractly.moduledoc/2", "Extactly.functiondoc/2"])
...(2)> {moduledoc |> String.split("\n") |> Enum.take(4), message}
{
[ " Returns docstring of a module",
"",
" E.g. verbatim", ""],
"Function doc for function Extactly.functiondoc/2 not found"}
If all the functions are in the same module the following form can be used
iex(3)> [{:ok, out}, _] = Extractly.functiondoc(["moduledoc/2", "functiondoc/2"], module: "Extractly")
...(3)> String.split(out, "\n") |> hd()
" Returns docstring of a module"
However it is convenient to add a markdown headline before each functiondoc, especially in these cases,
it can be done by indicating the `headline: level` option
iex(4)> [{:ok, moduledoc}, {:ok, functiondoc}] = Extractly.functiondoc(["moduledoc/2", "functiondoc/2"], module: "Extractly", headline: 2)
...(4)> {
...(4)> moduledoc |> String.split("\n") |> Enum.take(3),
...(4)> functiondoc |> String.split("\n") |> Enum.take(3)
...(4)> }
{
[ "## Extractly.moduledoc/2",
"",
" Returns docstring of a module"],
[ "## Extractly.functiondoc/2",
"",
" Returns docstring of a function"]}
Often times we are interested by **all** public functiondocs...
iex(5)> [{:ok, out}|_] = Extractly.functiondoc(:all, module: "Extractly", headline: 2)
...(5)> String.split(out, "\n") |> Enum.take(3)
[ "## Extractly.do_not_edit_warning/1",
"",
" Emits a comment including a message not to edit the created file, as it will be recreated from this template."]
We can specify a language to wrap indented code blocks into ` ```elixir\n...\n``` `
Here is an example
iex(6)> [ok: doc] = Extractly.functiondoc("Extractly.functiondoc/2", wrap_code_blocks: "elixir")
...(6)> doc |> String.split("\n") |> Enum.take(10)
[ " Returns docstring of a function",
" Ex:",
"",
"```elixir",
" iex(1)> {:ok, lines} = Extractly.functiondoc(\"Extractly.moduledoc/2\") |> hd()",
" ...(1)> lines |> String.split(\"\\n\") |> Enum.take(3)",
" [\" Returns docstring of a module\", \"\", \" E.g. verbatim\"]",
"```",
"",
" We can also pass a list of functions to get their docs concatenated"]
"""
def functiondoc(name, opts \\ [])
def functiondoc(:all, opts) do
case Keyword.get(opts, :module) do
nil -> [{:error, "No module given for `functiondoc(:all, ...)`"}]
module_name -> _all_functiondocs(module_name, opts)
end
end
def functiondoc(names, opts) when is_list(names) do
prefix =
case Keyword.get(opts, :module) do
nil -> ""
module_name -> "#{module_name}."
end
names
|> Enum.flat_map(&functiondoc("#{prefix}#{&1}", opts))
|> Enum.map(fn {status, result} -> {status, _postprocess(result, opts)} end)
end
def functiondoc(name, opts) when is_binary(name) do
headline = fdoc_headline(name, opts)
case _functiondoc(name) do
nil -> [{:error, "Function doc for function #{name} not found"}]
doc -> [{:ok, headline <> (doc |> _postprocess(opts))}]
end
end
@doc """
Returns docstring of a macro
"""
def macrodoc(name, opts \\ []) do
{module, macro_name, arity} = _parse_entity_name(name)
case Code.ensure_loaded(module) do
{:module, _} ->
{:ok, _get_entity_doc(module, macro_name, arity, :macro) |> _postprocess(opts)}
_ ->
{:error, "macro not found #{name}"}
end
end
@doc ~S"""
Returns docstring of a module
E.g. verbatim
iex(7)> {:ok, doc} = Extractly.moduledoc("Extractly")
...(7)> doc
" Provide easy access to information inside the templates rendered by `mix xtra`\n"
We can use the same options as with `functiondoc`
iex(8)> {:ok, doc} = Extractly.moduledoc("Extractly", headline: 2)
...(8)> doc |> String.split("\n") |> Enum.take(3)
[
"## Extractly", "", " Provide easy access to information inside the templates rendered by `mix xtra`"
]
If we also want to use `functiondoc :all, module: "Extractly"` **after** the call of `moduledoc` we can
include `:all` in the call of `moduledoc`, which will include function and macro docstrings as well
iex(9)> [{:ok, moduledoc} | _] =
...(9)> moduledoc("Extractly", headline: 3, include: :all)
...(9)> moduledoc
"### Extractly\n\n Provide easy access to information inside the templates rendered by `mix xtra`\n"
iex(10)> [_, {:ok, first_functiondoc} | _] =
...(10)> moduledoc("Extractly", headline: 3, include: :all)
...(10)> first_functiondoc |> String.split("\n") |> Enum.take(5)
[
"### Extractly.do_not_edit_warning/1",
"",
" Emits a comment including a message not to edit the created file, as it will be recreated from this template.",
"",
" It is a convenience to include this into your templates as follows"
]
"""
def moduledoc(name, opts \\ []) do
module = String.replace(name, ~r{\A(?:Elixir\.)?}, "Elixir.") |> String.to_atom()
headline = fdoc_headline(name, opts)
moduledoc_ =
case Code.ensure_loaded(module) do
{:module, _} ->
_get_moduledoc(module) |> _postprocess(opts) |> _check_nil_moduledoc(name, headline)
_ ->
{:error, "module not found #{module}"}
end
case Keyword.get(opts, :include) do
:all ->
more_docs = functiondoc(:all, Keyword.put(opts, :module, name))
[moduledoc_ | more_docs]
nil ->
moduledoc_
x ->
[
moduledoc_,
{:error,
"Illegal value #{x} for include: keyword in moduledoc for module #{name}, legal values are nil and :all"}
]
end
end
@doc ~S"""
Extract Table Of Contents from a markdown document
The files used for the following doctest can be found [here](https://github.com/RobertDober/extractly/tree/master/test/fixtures)
iex(11)> lines = [
...(11)> "## Usage",
...(11)> "### API",
...(11)> "#### EarmarkParser.as_ast/2",
...(11)> "### Support",
...(11)> ]
...(11)> toc(lines, gh_links: true)
{:ok, [
"- [Usage](#usage)",
" - [API](#api)",
" - [EarmarkParser.as_ast/2](#earmarkparseras_ast2)",
" - [Support](#support)",
]}
But if you do not want links
iex(12)> lines = [
...(12)> "## Usage",
...(12)> "### API",
...(12)> "#### EarmarkParser.as_ast/2",
...(12)> "### Support",
...(12)> ]
...(12)> toc(lines)
{:ok, [
"- Usage",
" - API",
" - EarmarkParser.as_ast/2",
" - Support",
]}
In case of bad options an error tuple is returned (no utf8 encoded
input should ever result in an error_tuple
iex(13)> lines = [] # options are checked even if input is empty
...(13)> toc(lines, no_such_option: "x")
{:error, "Unsupported option no_such_option"}
A more detailed description can be found in `Extractly.Toc`'s docstrings
"""
def toc(markdown_doc, options \\ []) do
case markdown_doc |> Extractly.Tools.lines_from_source() |> Extractly.Toc.render(options) do
{:error, message} -> {:error, message}
data -> {:ok, data}
end
end
defp _check_nil_moduledoc(moduledoc_or_nil, name, headline)
defp _check_nil_moduledoc(nil, name, _hl),
do: {:error, "module #{name} does not have a moduledoc"}
defp _check_nil_moduledoc(doc, _name, headline), do: {:ok, headline <> doc}
@doc ~S"""
Returns the output of a mix task
Ex:
iex(14)> Extractly.task("cmd", ~W[echo 42])
"42\n"
iex(15)> try do
...(15)> Extractly.task("xxx")
...(15)> rescue
...(15)> e in RuntimeError -> e.message |> String.split("\n") |> hd()
...(15)> end
"The following output was produced wih error code 1"
"""
def task(task, args \\ [])
def task(task, args) do
case System.cmd("mix", [task | args]) do
{output, 0} ->
output
{output, error} ->
raise "The following output was produced wih error code #{error}\n#{output}"
end
end
@doc """
A convenience method to access this libraries version
"""
def version do
:application.ensure_started(:extractly)
with {:ok, version} = :application.get_key(:extractly, :vsn), do: to_string(version)
end
defp _all_functiondocs(module_name, opts) do
module = "Elixir.#{module_name}" |> String.to_atom()
case Code.ensure_loaded(module) do
{:module, _} -> _get_functiondocs(module, opts)
_ -> [{:error, "cannot load module `#{module}'"}]
end
end
defp _extract_functiondoc(function_info)
defp _extract_functiondoc({_, _, _, doc_map, _}) when is_map(doc_map) do
case doc_map do
%{"en" => docstring} -> docstring
_ -> nil
end
end
defp _extract_functiondoc(_) do
nil
end
defp _extract_functiondoc_with_headline(
{{_, function_name, function_arity}, _, _, _, _} = function_info,
opts
) do
module_name = Keyword.get(opts, :module)
full_name = "#{module_name}.#{function_name}/#{function_arity}"
case _extract_functiondoc(function_info) do
nil -> {:error, "functiondoc for #{full_name} not found"}
doc -> {:ok, fdoc_headline(full_name, opts) <> (doc |> _postprocess(opts))}
end
end
defp _functiondoc(name) do
{module, function_name, arity} = _parse_entity_name(name)
case Code.ensure_loaded(module) do
{:module, _} -> _get_entity_doc(module, function_name, arity, :function)
_ -> nil
end
end
defp _get_entity_doc(module, name, arity, entity_type) do
if function_exported?(module, :__info__, 1) do
{:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(module)
Enum.find_value(docs, &_find_entity_doc(&1, name, arity, entity_type))
end
end
defp _get_functiondocs(module, opts) do
if function_exported?(module, :__info__, 1) do
{:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(module)
docs
|> Enum.map(&_extract_functiondoc_with_headline(&1, opts))
|> Enum.filter(fn {status, _} -> status == :ok end)
else
[{:error, "cannot access #{module.__info__ / 1}"}]
end
end
defp _get_moduledoc(module) do
if function_exported?(module, :__info__, 1) do
case Code.fetch_docs(module) do
{:docs_v1, _, :elixir, _, %{"en" => module_doc}, _, _} -> module_doc
_ -> nil
end
end
end
defp _find_entity_doc(doctuple, function_name, arity, entity_type) do
case doctuple do
{{^entity_type, ^function_name, ^arity}, _anno, _sign, %{"en" => doc}, _metadata} -> doc
_ -> nil
end
end
defp _parse_entity_name(name) do
names = String.split(name, ".")
[func | modules] = Enum.reverse(names)
module = ["Elixir" | Enum.reverse(modules)] |> Enum.join(".") |> String.to_atom()
[function_name, arity] = String.split(func, "/")
function_name = String.to_atom(function_name)
{arity, _} = Integer.parse(arity)
{module, function_name, arity}
end
defp _postprocess(input, opts)
defp _postprocess(nil, _opts), do: nil
defp _postprocess(input, opts) do
wrap? = Keyword.get(opts, :wrap_code_blocks)
input_ = Extractly.Directives.process(input, !!wrap?)
case wrap? do
nil -> input_
lang -> wrap_code_blocks(input_, lang)
end
end
end