Packages
ex_doc
0.31.0
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([ExDoc.ModuleNode.t()], [ExDoc.ModuleNode.t()], ExDoc.Config.t()) :: String.t()
def run(project_nodes, filtered_modules, config) when is_map(config) do
config = normalize_config(config)
File.rm_rf!(config.output)
File.mkdir_p!(Path.join(config.output, "OEBPS"))
project_nodes =
HTML.render_all(project_nodes, filtered_modules, ".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 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
ExDoc.Utils.warn("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
@two_power_16 65536
@two_power_32 4_294_967_296
@two_power_48 281_474_976_710_656
defp uuid4 do
Enum.map_join(
[
<<:rand.uniform(@two_power_32) - 1::32>>,
<<:rand.uniform(@two_power_16) - 1::16>>,
<<:rand.uniform(@two_power_16) - 1::16>>,
<<:rand.uniform(@two_power_16) - 1::16>>,
<<:rand.uniform(@two_power_48) - 1::48>>
],
<<45>>,
&Base.encode16(&1, case: :lower)
)
end
end