zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

epub.ex (5365B)


      1 defmodule ExDoc.Formatter.EPUB do
      2   @moduledoc false
      3 
      4   @mimetype "application/epub+zip"
      5   alias __MODULE__.{Assets, Templates}
      6   alias ExDoc.Formatter.HTML
      7 
      8   @doc """
      9   Generate EPUB documentation for the given modules.
     10   """
     11   @spec run(list, ExDoc.Config.t()) :: String.t()
     12   def run(project_nodes, config) when is_map(config) do
     13     config = normalize_config(config)
     14     File.rm_rf!(config.output)
     15     File.mkdir_p!(Path.join(config.output, "OEBPS"))
     16 
     17     project_nodes = HTML.render_all(project_nodes, ".xhtml", config, highlight_tag: "samp")
     18 
     19     nodes_map = %{
     20       modules: HTML.filter_list(:module, project_nodes),
     21       tasks: HTML.filter_list(:task, project_nodes)
     22     }
     23 
     24     extras = config |> HTML.build_extras(".xhtml") |> group_extras()
     25     config = %{config | extras: extras}
     26 
     27     assets_dir = "OEBPS/assets"
     28     static_files = HTML.generate_assets(config, assets_dir, default_assets(config))
     29     HTML.generate_logo(assets_dir, config)
     30     HTML.generate_cover(assets_dir, config)
     31 
     32     uuid = "urn:uuid:#{uuid4()}"
     33     datetime = format_datetime()
     34 
     35     generate_content(config, nodes_map, uuid, datetime, static_files)
     36     generate_nav(config, nodes_map)
     37     generate_title(config)
     38     generate_extras(config)
     39     generate_list(config, nodes_map.modules)
     40     generate_list(config, nodes_map.tasks)
     41 
     42     {:ok, epub} = generate_epub(config.output)
     43     File.rm_rf!(config.output)
     44     Path.relative_to_cwd(epub)
     45   end
     46 
     47   defp normalize_config(config) do
     48     output =
     49       config.output
     50       |> Path.expand()
     51       |> Path.join("#{config.project}")
     52 
     53     %{config | output: output}
     54   end
     55 
     56   defp generate_extras(config) do
     57     for {_title, extras} <- config.extras do
     58       Enum.each(extras, fn %{id: id, title: title, title_content: title_content, content: content} ->
     59         output = "#{config.output}/OEBPS/#{id}.xhtml"
     60         html = Templates.extra_template(config, title, title_content, content)
     61 
     62         if File.regular?(output) do
     63           IO.puts(:stderr, "warning: file #{Path.relative_to_cwd(output)} already exists")
     64         end
     65 
     66         File.write!(output, html)
     67       end)
     68     end
     69   end
     70 
     71   defp generate_content(config, nodes, uuid, datetime, static_files) do
     72     static_files =
     73       static_files
     74       |> Enum.filter(fn name ->
     75         String.contains?(name, "OEBPS") and config.output |> Path.join(name) |> File.regular?()
     76       end)
     77       |> Enum.map(&Path.relative_to(&1, "OEBPS"))
     78 
     79     content = Templates.content_template(config, nodes, uuid, datetime, static_files)
     80     File.write("#{config.output}/OEBPS/content.opf", content)
     81   end
     82 
     83   defp generate_nav(config, nodes) do
     84     content = Templates.nav_template(config, nodes)
     85     File.write("#{config.output}/OEBPS/nav.xhtml", content)
     86   end
     87 
     88   defp group_extras(extras) do
     89     {extras_by_group, groups} =
     90       extras
     91       |> Enum.with_index()
     92       |> Enum.reduce({%{}, %{}}, fn {x, index}, {extras_by_group, groups} ->
     93         group = if x.group != "", do: x.group, else: "Extras"
     94         extras_by_group = Map.update(extras_by_group, group, [x], &[x | &1])
     95         groups = Map.put_new(groups, group, index)
     96         {extras_by_group, groups}
     97       end)
     98 
     99     groups
    100     |> Map.to_list()
    101     |> List.keysort(1)
    102     |> Enum.map(fn {k, _} -> {k, Enum.reverse(Map.get(extras_by_group, k))} end)
    103   end
    104 
    105   defp generate_title(config) do
    106     content = Templates.title_template(config)
    107     File.write("#{config.output}/OEBPS/title.xhtml", content)
    108   end
    109 
    110   defp generate_list(config, nodes) do
    111     nodes
    112     |> Task.async_stream(&generate_module_page(&1, config), timeout: :infinity)
    113     |> Enum.map(&elem(&1, 1))
    114   end
    115 
    116   defp generate_epub(output) do
    117     :zip.create(
    118       String.to_charlist("#{output}.epub"),
    119       [{~c"mimetype", @mimetype} | files_to_add(output)],
    120       compress: [
    121         ~c".css",
    122         ~c".xhtml",
    123         ~c".html",
    124         ~c".ncx",
    125         ~c".js",
    126         ~c".opf",
    127         ~c".jpg",
    128         ~c".png",
    129         ~c".xml"
    130       ]
    131     )
    132   end
    133 
    134   ## Helpers
    135 
    136   defp default_assets(config) do
    137     [
    138       {Assets.dist(config.proglang), "OEBPS/dist"},
    139       {Assets.metainfo(), "META-INF"}
    140     ]
    141   end
    142 
    143   defp files_to_add(path) do
    144     Enum.reduce(Path.wildcard(Path.join(path, "**/*")), [], fn file, acc ->
    145       case File.read(file) do
    146         {:ok, bin} ->
    147           [{file |> Path.relative_to(path) |> String.to_charlist(), bin} | acc]
    148 
    149         {:error, _} ->
    150           acc
    151       end
    152     end)
    153   end
    154 
    155   # Helper to format Erlang datetime tuple
    156   defp format_datetime do
    157     {{year, month, day}, {hour, min, sec}} = :calendar.universal_time()
    158     list = [year, month, day, hour, min, sec]
    159 
    160     "~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ"
    161     |> :io_lib.format(list)
    162     |> IO.iodata_to_binary()
    163   end
    164 
    165   defp generate_module_page(module_node, config) do
    166     content = Templates.module_page(config, module_node)
    167     File.write("#{config.output}/OEBPS/#{module_node.id}.xhtml", content)
    168   end
    169 
    170   # Helper to generate an UUID v4. This version uses pseudo-random bytes generated by
    171   # the `crypto` module.
    172   defp uuid4 do
    173     <<u0::48, _::4, u1::12, _::2, u2::62>> = :crypto.strong_rand_bytes(16)
    174     bin = <<u0::48, 4::4, u1::12, 2::2, u2::62>>
    175     <<u0::32, u1::16, u2::16, u3::16, u4::48>> = bin
    176 
    177     Enum.map_join(
    178       [<<u0::32>>, <<u1::16>>, <<u2::16>>, <<u3::16>>, <<u4::48>>],
    179       <<45>>,
    180       &Base.encode16(&1, case: :lower)
    181     )
    182   end
    183 end