Packages
ex_doc
0.40.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"
@assets_dir "OEBPS/assets"
alias __MODULE__.{Assets, Templates}
alias ExDoc.Formatter
@doc false
def autolink_options do
[extension: ".xhtml", highlight_tag: "samp"]
end
@doc """
Generates EPUB documentation for the given modules.
"""
def run(config, project_nodes, extras) when is_map(config) do
config = normalize_config(config)
File.rm_rf!(config.output)
File.mkdir_p!(Path.join(config.output, "OEBPS"))
static_files =
Formatter.copy_assets(config.assets, Path.join(config.output, "OEBPS")) ++
Formatter.copy_assets(additional_assets(config), config.output)
Formatter.copy_logo(config, Path.join(@assets_dir, "logo"))
Formatter.copy_cover(config, Path.join(@assets_dir, "cover"))
{modules, tasks} = Enum.split_with(project_nodes, &(&1.type != :task))
generate_content(config, modules, tasks, extras, static_files)
generate_nav(config, modules, tasks, extras)
generate_title(config)
generate_extras(config, extras)
generate_modules(config, modules)
generate_modules(config, tasks)
{:ok, epub} = generate_epub(config.output)
File.rm_rf!(config.output)
entrypoint = Path.relative_to_cwd(epub)
%{entrypoint: entrypoint, build: [entrypoint]}
end
defp normalize_config(config) do
output = Path.join(config.output, "#{config.project}")
%{config | output: output}
end
defp generate_extras(config, extras) do
for %ExDoc.ExtraNode{} = node <- extras do
output = "#{config.output}/OEBPS/#{node.id}.xhtml"
html = Templates.extra_template(config, node)
if File.regular?(output) do
ExDoc.warn("file #{Path.relative_to_cwd(output)} already exists", [])
end
File.write!(output, html)
end
end
defp generate_content(config, modules, tasks, extras, static_files) do
uuid = "urn:uuid:#{uuid4()}"
datetime = format_datetime()
static_files =
for name <- static_files,
String.contains?(name, "OEBPS"),
media_type = Templates.media_type(Path.extname(name)),
do: {Path.relative_to(name, "OEBPS"), media_type}
content =
Templates.content_template(config, modules, tasks, extras, uuid, datetime, static_files)
File.write!("#{config.output}/OEBPS/content.opf", content)
end
defp generate_nav(config, modules, tasks, extras) do
modules = group_by_group(modules)
tasks = group_by_group(tasks)
extras = group_by_group(extras)
content = Templates.nav_template(config, modules, tasks, extras)
File.write!("#{config.output}/OEBPS/nav.xhtml", content)
end
defp group_by_group(nodes) do
nodes
|> Enum.chunk_by(& &1.group)
|> Enum.map(&{hd(&1).group, &1})
end
defp generate_title(config) do
content = Templates.title_template(config)
File.write!("#{config.output}/OEBPS/title.xhtml", content)
end
defp generate_modules(config, nodes) do
nodes
|> Task.async_stream(
fn module_node ->
content = Templates.module_template(config, module_node)
File.write!("#{config.output}/OEBPS/#{module_node.id}.xhtml", content)
end,
timeout: :infinity
)
|> Enum.map(fn {:ok, result} -> result end)
end
defp generate_epub(output) do
:zip.create(
String.to_charlist("#{output}.epub"),
[{~c"mimetype", @mimetype} | files_to_add(output)],
compress: ~w[.css .xhtml .html .ncx .js .opf .jpg .png .xml]c
)
end
## Helpers
defp additional_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
@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