Packages
ex_doc
0.16.2
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/epub.ex
defmodule ExDoc.Formatter.EPUB do
@moduledoc """
Provide EPUB documentation
"""
@mimetype "application/epub+zip"
alias __MODULE__.{Assets, Templates}
alias ExDoc.Formatter.HTML
@doc """
Generate EPUB documentation for the given modules
"""
@spec run(list, ExDoc.Config.t) :: String.t
def run(project_nodes, config) when is_map(config) do
config = normalize_config(config)
config = %{config | extras: project_nodes |> HTML.build_extras(config, ".xhtml") |> group_extras()}
File.rm_rf!(config.output)
File.mkdir_p!(Path.join(config.output, "OEBPS"))
assets_dir = "OEBPS/assets"
static_files = HTML.generate_assets(config, assets_dir, default_assets())
HTML.generate_logo(assets_dir, config)
all = HTML.Autolink.all(project_nodes, ".xhtml", config.deps)
nodes_map = %{
modules: HTML.filter_list(:module, all),
exceptions: HTML.filter_list(:exception, all),
protocols: HTML.filter_list(:protocol, all),
tasks: HTML.filter_list(:task, all)
}
uuid = "urn:uuid:#{uuid4()}"
datetime = format_datetime()
generate_content(config, nodes_map, uuid, datetime, static_files)
generate_nav(config, nodes_map)
generate_title(config)
generate_extras(config)
generate_list(config, nodes_map.modules)
generate_list(config, nodes_map.exceptions)
generate_list(config, nodes_map.protocols)
generate_list(config, nodes_map.tasks)
{:ok, epub} = generate_epub(config.output)
File.rm_rf!(config.output)
Path.relative_to_cwd(epub)
end
defp normalize_config(config) do
output =
config.output
|> Path.expand()
|> Path.join("#{config.project}-v#{config.version}")
%{config | output: output}
end
defp generate_extras(config) do
for {_title, extras} <- config.extras do
Enum.each(extras, fn %{id: id, title: title, content: content} ->
output = "#{config.output}/OEBPS/#{id}.xhtml"
html = Templates.extra_template(config, title, content)
if File.regular? output do
IO.puts :stderr, "warning: file #{Path.relative_to_cwd output} already exists"
end
File.write!(output, html)
end)
end
end
defp generate_content(config, nodes, uuid, datetime, static_files) do
static_files =
static_files
|> Enum.filter(fn(name) ->
String.contains?(name, "OEBPS") and config.output |> Path.join(name) |> File.regular?()
end)
|> Enum.map(&Path.relative_to(&1, "OEBPS"))
content = Templates.content_template(config, nodes, uuid, datetime, static_files)
File.write("#{config.output}/OEBPS/content.opf", content)
end
defp generate_nav(config, nodes) do
content = Templates.nav_template(config, nodes)
File.write("#{config.output}/OEBPS/nav.xhtml", content)
end
defp group_extras(extras) do
{extras_by_group, groups} =
extras
|> Enum.with_index()
|> Enum.reduce({%{}, %{}}, fn({x, index}, {extras_by_group, groups}) ->
group = if x.group != "", do: x.group, else: "Extras"
extras_by_group = Map.update(extras_by_group, group, [x], &([x | &1]))
groups = Map.put_new(groups, group, index)
{extras_by_group, groups}
end)
groups
|> Map.to_list()
|> List.keysort(1)
|> Enum.map(fn({k, _}) -> {k, Enum.reverse(Map.get(extras_by_group, k))} end)
end
defp generate_title(config) do
content = Templates.title_template(config)
File.write("#{config.output}/OEBPS/title.xhtml", content)
end
defp generate_list(config, nodes) do
nodes
|> Enum.map(&Task.async(fn ->
generate_module_page(config.output, config, &1)
end))
|> Enum.map(&Task.await(&1, :infinity))
end
defp generate_epub(output) do
:zip.create(String.to_charlist("#{output}.epub"),
[{'mimetype', @mimetype} | files_to_add(output)],
compress: ['.css', '.xhtml', '.html', '.ncx', '.js',
'.opf', '.jpg', '.png', '.xml'])
end
## Helpers
defp default_assets do
[{Assets.dist(), "OEBPS/dist"}, {Assets.metainfo(), "META-INF"}]
end
defp files_to_add(path) do
Enum.reduce Path.wildcard(Path.join(path, "**/*")), [], fn file, acc ->
case File.read(file) do
{:ok, bin} ->
[{file |> Path.relative_to(path) |> String.to_charlist(), bin} | acc]
{:error, _} ->
acc
end
end
end
# Helper to format Erlang datetime tuple
defp format_datetime do
{{year, month, day}, {hour, min, sec}} = :calendar.universal_time()
list = [year, month, day, hour, min, sec]
"~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ"
|> :io_lib.format(list)
|> IO.iodata_to_binary()
end
defp generate_module_page(output, config, module_node) do
content = Templates.module_page(config, module_node)
File.write("#{output}/OEBPS/#{module_node.id}.xhtml", content)
end
# Helper to generate an UUID v4. This version uses pseudo-random bytes generated by
# the `crypto` module.
defp uuid4 do
<<u0::48, _::4, u1::12, _::2, u2::62>> = :crypto.strong_rand_bytes(16)
bin = <<u0::48, 4::4, u1::12, 2::2, u2::62>>
<<u0::32, u1::16, u2::16, u3::16, u4::48>> = bin
Enum.map_join([<<u0::32>>, <<u1::16>>, <<u2::16>>, <<u3::16>>, <<u4::48>>], <<45>>,
&(Base.encode16(&1, case: :lower)))
end
end