Packages
ex_doc
0.11.5
0.40.3
0.40.2
0.40.1
0.40.0
0.39.3
0.39.2
0.39.1
0.39.0
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.3
0.37.2
0.37.1
0.37.0
0.37.0-rc.2
0.37.0-rc.1
0.37.0-rc.0
0.36.1
0.36.0
0.35.1
0.35.0
0.34.2
0.34.1
0.34.0
0.33.0
0.32.2
0.32.1
0.32.0
0.31.2
0.31.1
0.31.0
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.6
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.3
0.27.2
0.27.1
0.27.0
0.26.0
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.2
0.24.1
0.24.0
0.23.0
0.22.6
0.22.5
0.22.4
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.19.0-rc
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
retired
0.17.1
0.17.0
retired
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.2
0.13.1
0.13.0
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
ExDoc is a documentation generation tool for Elixir
Current section
Files
Jump to
Current section
Files
lib/ex_doc/formatter/html.ex
defmodule ExDoc.Formatter.HTML do
@moduledoc """
Generate HTML documentation for Elixir projects
"""
alias ExDoc.Formatter.HTML.Templates
alias ExDoc.Formatter.HTML.Autolink
@main "api-reference"
@doc """
Generate HTML documentation for the given modules
"""
@spec run(list, %ExDoc.Config{}) :: String.t
def run(module_nodes, config) when is_map(config) do
config = normalize_config(config)
output = Path.expand(config.output)
File.rm_rf! output
:ok = File.mkdir_p output
assets |> templates_path() |> generate_assets(output)
all = Autolink.all(module_nodes)
modules = filter_list(:modules, all)
exceptions = filter_list(:exceptions, all)
protocols = filter_list(:protocols, all)
config =
if config.logo do
process_logo_metadata(config)
else
config
end
generate_api_reference(modules, exceptions, protocols, output, config)
extras = generate_extras(output, module_nodes, modules, exceptions, protocols, config)
generate_index(output, config)
generate_not_found(modules, exceptions, protocols, output, config)
generate_sidebar_items(modules, exceptions, protocols, extras, output)
generate_list(modules, modules, exceptions, protocols, output, config)
generate_list(exceptions, modules, exceptions, protocols, output, config)
generate_list(protocols, modules, exceptions, protocols, output, config)
Path.join(config.output, "index.html")
end
defp normalize_config(%{main: "index"}) do
raise ArgumentError, message: ~S("main" cannot be set to "index", otherwise it will recursively link to itself)
end
defp normalize_config(%{main: main} = config) do
%{config | main: main || @main}
end
defp generate_index(output, config) do
generate_redirect(output, "index.html", config, "#{config.main}.html")
end
defp generate_api_reference(modules, exceptions, protocols, output, config) do
file_name = "api-reference.html"
config = set_canonical_url(config, file_name)
content = Templates.api_reference_template(config, modules, exceptions, protocols)
File.write!("#{output}/#{file_name}", content)
end
defp generate_not_found(modules, exceptions, protocols, output, config) do
file_name = "404.html"
config = set_canonical_url(config, file_name)
content = Templates.not_found_template(config, modules, exceptions, protocols)
File.write!("#{output}/#{file_name}", content)
end
defp generate_sidebar_items(modules, exceptions, protocols, extras, output) do
nodes = %{modules: modules, protocols: protocols,
exceptions: exceptions, extras: extras}
content = Templates.create_sidebar_items(nodes)
File.write!("#{output}/dist/sidebar_items.js", content)
end
defp assets do
[{"dist/*.{css,js}", "dist"},
{"fonts/*.{eot,svg,ttf,woff,woff2}", "fonts"}]
end
# TODO: decouple EPUB/HTML
@doc """
Copy a list of assets into a given directory
"""
@spec generate_assets(list, String.t) :: :ok
def generate_assets(source, output) do
Enum.each source, fn({ pattern, dir }) ->
output = "#{output}/#{dir}"
File.mkdir! output
Enum.map Path.wildcard(pattern), fn(file) ->
base = Path.basename(file)
File.copy file, "#{output}/#{base}"
end
end
end
defp generate_extras(output, module_nodes, modules, exceptions, protocols, config) do
extras =
config.extras
|> Enum.map(&Task.async(fn ->
generate_extra(&1, output, module_nodes, modules, exceptions, protocols, config)
end))
|> Enum.map(&Task.await(&1, :infinity))
[{"api-reference", "API Reference", []}|extras]
end
defp generate_extra({input_file, options}, output, module_nodes, modules, exceptions, protocols, config) do
input_file = to_string(input_file)
output_file_name = options[:path] || input_file |> input_to_title() |> title_to_filename()
options = %{
title: options[:title],
output_file_name: output_file_name,
input: input_file,
output: output
}
create_extra_files(module_nodes, modules, exceptions, protocols, config, options)
end
defp generate_extra(input, output, module_nodes, modules, exceptions, protocols, config) do
output_file_name = input |> input_to_title |> title_to_filename
options = %{
output_file_name: output_file_name,
input: input,
output: output
}
create_extra_files(module_nodes, modules, exceptions, protocols, config, options)
end
defp create_extra_files(module_nodes, modules, exceptions, protocols, config, options) do
if valid_extension_name?(options.input) do
content =
options.input
|> File.read!()
|> Autolink.project_doc(module_nodes)
title = options[:title] || extract_title(content) || input_to_title(options[:input])
output_file_name = "#{options.output_file_name}.html"
config = set_canonical_url(config, output_file_name)
html = Templates.extra_template(config, title, modules,
exceptions, protocols, link_headers(content))
output = "#{options.output}/#{output_file_name}"
if File.regular? output do
IO.puts "warning: file #{Path.basename output} already exists"
end
File.write!(output, html)
{options.output_file_name, title, extract_headers(content)}
else
raise ArgumentError, "file format not recognized, allowed format is: .md"
end
end
defp valid_extension_name?(input) do
file_ext =
input
|> Path.extname()
|> String.downcase()
if file_ext in [".md"] do
true
else
false
end
end
@h1_regex ~r/^#([^#].*)\n$/m
defp extract_title(content) do
title = Regex.run(@h1_regex, content, capture: :all_but_first)
if title do
title |> List.first |> String.strip
end
end
@h2_regex ~r/^##([^#].*)\n$/m
defp extract_headers(content) do
@h2_regex
|> Regex.scan(content, capture: :all_but_first)
|> List.flatten()
|> Enum.map(&{&1, header_to_id(&1)})
end
defp link_headers(content) do
Regex.replace(@h2_regex, content, fn _, part ->
"<h2 id=\"#{header_to_id(part)}\">#{Templates.h(part)}</h2>\n"
end)
end
defp input_to_title(input) do
input |> Path.basename() |> Path.rootname()
end
defp title_to_filename(title) do
title |> String.replace(" ", "-") |> String.downcase()
end
defp header_to_id(header) do
header
|> String.strip()
|> String.replace(~r/\W+/, "-")
|> String.downcase()
|> Templates.h()
end
defp process_logo_metadata(config) do
output = "#{config.output}/assets"
File.mkdir_p! output
file_extname =
config.logo
|> Path.extname()
|> String.downcase()
if file_extname in ~w(.png .jpg) do
file_name = "#{output}/logo#{file_extname}"
File.copy!(config.logo, file_name)
Map.put(config, :logo, Path.basename(file_name))
else
raise ArgumentError, "image format not recognized, allowed formats are: .jpg, .png"
end
end
defp generate_redirect(output, file_name, config, redirect_to) do
content = Templates.redirect_template(config, redirect_to)
File.write!("#{output}/#{file_name}", content)
end
defp filter_list(:modules, nodes) do
Enum.filter nodes, &(not &1.type in [:exception, :protocol, :impl])
end
defp filter_list(:exceptions, nodes) do
Enum.filter nodes, &(&1.type in [:exception])
end
defp filter_list(:protocols, nodes) do
Enum.filter nodes, &(&1.type in [:protocol])
end
defp generate_list(nodes, modules, exceptions, protocols, output, config) do
nodes
|> Enum.map(&Task.async(fn ->
generate_module_page(&1, modules, exceptions, protocols, output, config)
end))
|> Enum.map(&Task.await(&1, :infinity))
end
defp generate_module_page(node, modules, exceptions, protocols, output, config) do
file_name = "#{node.id}.html"
config = set_canonical_url(config, file_name)
content = Templates.module_page(node, modules, exceptions, protocols, config)
File.write!("#{output}/#{file_name}", content)
end
defp templates_path(patterns) do
Enum.into(patterns, [], fn {pattern, dir} ->
{Path.expand("html/templates/#{pattern}", __DIR__), dir}
end)
end
defp set_canonical_url(config, file_name) do
if config.canonical do
canonical_url =
config.canonical
|> String.rstrip(?/)
|> Path.join(file_name)
Map.put(config, :canonical, canonical_url)
else
config
end
end
end