|
| 1 | +defmodule ExDoc.Formatter.Markdown.Templates do |
| 2 | + @moduledoc false |
| 3 | + |
| 4 | + require EEx |
| 5 | + |
| 6 | + import ExDoc.Utils, |
| 7 | + only: [before_closing_body_tag: 2, before_closing_head_tag: 2, h: 1, text_to_id: 1] |
| 8 | + |
| 9 | + alias ExDoc.Formatter.HTML.Templates, as: H |
| 10 | + |
| 11 | + @doc """ |
| 12 | + Generate content from the module template for a given `node` |
| 13 | + """ |
| 14 | + def module_page(config, module_node) do |
| 15 | + summary = H.module_summary(module_node) |
| 16 | + module_template(config, module_node, summary) |
| 17 | + end |
| 18 | + |
| 19 | + @doc """ |
| 20 | + Generated ID for static file |
| 21 | + """ |
| 22 | + def static_file_to_id(static_file) do |
| 23 | + static_file |> Path.basename() |> text_to_id() |
| 24 | + end |
| 25 | + |
| 26 | + def node_doc(%{source_doc: %{"en"=> source}}), do: source |
| 27 | + def node_doc(%{rendered_doc: source}), do: source |
| 28 | + |
| 29 | + @doc """ |
| 30 | + Gets the first paragraph of the documentation of a node. It strips |
| 31 | + surrounding white-spaces and trailing `:`. |
| 32 | +
|
| 33 | + If `doc` is `nil`, it returns `nil`. |
| 34 | + """ |
| 35 | + @spec synopsis(String.t()) :: String.t() |
| 36 | + @spec synopsis(nil) :: nil |
| 37 | + def synopsis(nil), do: nil |
| 38 | + |
| 39 | + def synopsis(doc) when is_binary(doc) do |
| 40 | + doc = |
| 41 | + case :binary.split(doc, "</p>") do |
| 42 | + [left, _] -> String.trim_trailing(left, ": ") |
| 43 | + [all] -> all |
| 44 | + end |
| 45 | + |
| 46 | + # Remove any anchors found in synopsis. |
| 47 | + # Old Erlang docs placed anchors at the top of the documentation |
| 48 | + # for links. Ideally they would have been removed but meanwhile |
| 49 | + # it is simpler to guarantee they won't be duplicated in docs. |
| 50 | + Regex.replace(~r|(<[^>]*) id="[^"]*"([^>]*>)|, doc, ~S"\1\2", []) |
| 51 | + end |
| 52 | + |
| 53 | + @doc """ |
| 54 | + Add link headings for the given `content`. |
| 55 | +
|
| 56 | + IDs are prefixed with `prefix`. |
| 57 | +
|
| 58 | + We only link `h2` and `h3` headers. This is kept consistent in ExDoc.SearchData. |
| 59 | + """ |
| 60 | + @heading_regex ~r/<(h[23]).*?>(.*?)<\/\1>/m |
| 61 | + @spec link_headings(String.t() | nil, String.t()) :: String.t() | nil |
| 62 | + def link_headings(content, prefix \\ "") |
| 63 | + def link_headings(nil, _), do: nil |
| 64 | + |
| 65 | + def link_headings(content, prefix) do |
| 66 | + @heading_regex |
| 67 | + |> Regex.scan(content) |
| 68 | + |> Enum.reduce({content, %{}}, fn [match, tag, title], {content, occurrences} -> |
| 69 | + possible_id = text_to_id(title) |
| 70 | + id_occurred = Map.get(occurrences, possible_id, 0) |
| 71 | + |
| 72 | + anchor_id = if id_occurred >= 1, do: "#{possible_id}-#{id_occurred}", else: possible_id |
| 73 | + replacement = link_heading(match, tag, title, anchor_id, prefix) |
| 74 | + linked_content = String.replace(content, match, replacement, global: false) |
| 75 | + incremented_occs = Map.put(occurrences, possible_id, id_occurred + 1) |
| 76 | + {linked_content, incremented_occs} |
| 77 | + end) |
| 78 | + |> elem(0) |
| 79 | + end |
| 80 | + |
| 81 | + @class_regex ~r/<h[23].*?(\sclass="(?<class>[^"]+)")?.*?>/ |
| 82 | + @class_separator " " |
| 83 | + defp link_heading(match, _tag, _title, "", _prefix), do: match |
| 84 | + |
| 85 | + defp link_heading(match, tag, title, id, prefix) do |
| 86 | + section_header_class_name = "section-heading" |
| 87 | + |
| 88 | + # The Markdown syntax that we support for the admonition text |
| 89 | + # blocks is something like this: |
| 90 | + # |
| 91 | + # > ### Never open this door! {: .warning} |
| 92 | + # > |
| 93 | + # > ... |
| 94 | + # |
| 95 | + |
| 96 | + """ |
| 97 | + ## [#{title}](##{prefix}#{id}) |
| 98 | + """ |
| 99 | + end |
| 100 | + |
| 101 | + def link_moduledoc_headings(content) do |
| 102 | + link_headings(content, "module-") |
| 103 | + end |
| 104 | + |
| 105 | + def link_detail_headings(content, prefix) do |
| 106 | + link_headings(content, prefix <> "-") |
| 107 | + end |
| 108 | + |
| 109 | + @doc """ |
| 110 | + Creates a chapter which contains all the details about an individual module. |
| 111 | +
|
| 112 | + This chapter can include the following sections: *functions*, *types*, *callbacks*. |
| 113 | + """ |
| 114 | + EEx.function_from_file( |
| 115 | + :def, |
| 116 | + :module_template, |
| 117 | + Path.expand("templates/module_template.eex", __DIR__), |
| 118 | + [:config, :module, :summary], |
| 119 | + trim: true |
| 120 | + ) |
| 121 | + |
| 122 | + # @doc """ |
| 123 | + # Creates the table of contents. |
| 124 | + |
| 125 | + # This template follows the EPUB Navigation Document Definition. |
| 126 | + |
| 127 | + # See http://www.idpf.org/epub/30/spec/epub30-contentdocs.html#sec-xhtml-nav. |
| 128 | + # """ |
| 129 | + # EEx.function_from_file( |
| 130 | + # :def, |
| 131 | + # :nav_template, |
| 132 | + # Path.expand("templates/nav_template.eex", __DIR__), |
| 133 | + # [:config, :nodes], |
| 134 | + # trim: true |
| 135 | + # ) |
| 136 | + |
| 137 | + # @doc """ |
| 138 | + # Creates a new chapter when the user provides additional files. |
| 139 | + # """ |
| 140 | + # EEx.function_from_file( |
| 141 | + # :def, |
| 142 | + # :extra_template, |
| 143 | + # Path.expand("templates/extra_template.eex", __DIR__), |
| 144 | + # [:config, :title, :title_content, :content], |
| 145 | + # trim: true |
| 146 | + # ) |
| 147 | + |
| 148 | + |
| 149 | + # EEx.function_from_file( |
| 150 | + # :defp, |
| 151 | + # :nav_item_template, |
| 152 | + # Path.expand("templates/nav_item_template.eex", __DIR__), |
| 153 | + # [:name, :nodes], |
| 154 | + # trim: true |
| 155 | + # ) |
| 156 | + |
| 157 | + # EEx.function_from_file( |
| 158 | + # :defp, |
| 159 | + # :nav_grouped_item_template, |
| 160 | + # Path.expand("templates/nav_grouped_item_template.eex", __DIR__), |
| 161 | + # [:nodes], |
| 162 | + # trim: true |
| 163 | + # ) |
| 164 | + |
| 165 | + # EEx.function_from_file( |
| 166 | + # :defp, |
| 167 | + # :toc_item_template, |
| 168 | + # Path.expand("templates/toc_item_template.eex", __DIR__), |
| 169 | + # [:nodes], |
| 170 | + # trim: true |
| 171 | + # ) |
| 172 | + |
| 173 | + # "templates/media-types.txt" |
| 174 | + # |> Path.expand(__DIR__) |
| 175 | + # |> File.read!() |
| 176 | + # |> String.split("\n", trim: true) |
| 177 | + # |> Enum.each(fn line -> |
| 178 | + # [extension, media] = String.split(line, ",") |
| 179 | + |
| 180 | + # def media_type("." <> unquote(extension)) do |
| 181 | + # unquote(media) |
| 182 | + # end |
| 183 | + # end) |
| 184 | + |
| 185 | + # def media_type(_arg), do: nil |
| 186 | + |
| 187 | + templates = [ |
| 188 | + detail_template: [:node, :module], |
| 189 | + summary_template: [:name, :nodes] |
| 190 | + ] |
| 191 | + |
| 192 | + Enum.each(templates, fn {name, args} -> |
| 193 | + filename = Path.expand("templates/#{name}.eex", __DIR__) |
| 194 | + @doc false |
| 195 | + EEx.function_from_file(:def, name, filename, args, trim: true) |
| 196 | + end) |
| 197 | +end |
0 commit comments