Packages
ex_doc
0.30.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 false
@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
parent = config.output
config = normalize_config(config)
HTML.setup_output(
parent,
&cleanup_output_dir(&1, config),
&create_output_dir(&1, config)
)
project_nodes = HTML.render_all(project_nodes, ".xhtml", config, highlight_tag: "samp")
nodes_map = %{
modules: HTML.filter_list(:module, project_nodes),
tasks: HTML.filter_list(:task, project_nodes)
}
extras = config |> HTML.build_extras(".xhtml") |> group_extras()
config = %{config | extras: extras}
assets_dir = "OEBPS/assets"
static_files = HTML.generate_assets(config, assets_dir, default_assets(config))
HTML.generate_logo(assets_dir, config)
HTML.generate_cover(assets_dir, config)
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.tasks)
{:ok, epub} = generate_epub(config.output)
File.rm_rf!(config.output)
Path.relative_to_cwd(epub)
end
defp create_output_dir(root, config) do
File.mkdir_p!(Path.join(config.output, "OEBPS"))
File.touch!(Path.join(root, ".ex_doc"))
end
defp cleanup_output_dir(docs_root, config) do
File.rm_rf!(config.output)
create_output_dir(docs_root, config)
end
defp normalize_config(config) do
output =
config.output
|> Path.expand()
|> Path.join("#{config.project}")
%{config | output: output}
end
defp generate_extras(config) do
for {_title, extras} <- config.extras do
Enum.each(extras, fn %{id: id, title: title, title_content: title_content, content: content} ->
output = "#{config.output}/OEBPS/#{id}.xhtml"
html = Templates.extra_template(config, title, title_content, 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
|> Task.async_stream(&generate_module_page(&1, config), timeout: :infinity)
|> Enum.map(&elem(&1, 1))
end
defp generate_epub(output) do
:zip.create(
String.to_charlist("#{output}.epub"),
[{~c"mimetype", @mimetype} | files_to_add(output)],
compress: [
~c".css",
~c".xhtml",
~c".html",
~c".ncx",
~c".js",
~c".opf",
~c".jpg",
~c".png",
~c".xml"
]
)
end
## Helpers
defp default_assets(config) do
[
{Assets.dist(config.proglang), "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(module_node, config) do
content = Templates.module_page(config, module_node)
File.write("#{config.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