zf

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

templates.ex (9803B)


      1 defmodule ExDoc.Formatter.HTML.Templates do
      2   @moduledoc false
      3   require EEx
      4 
      5   import ExDoc.Utils, only: [h: 1]
      6 
      7   # TODO: It should not depend on the parent module. Move required HTML functions to Utils.
      8   # TODO: Add tests that assert on the returned structured, not on JSON
      9   alias ExDoc.Formatter.HTML
     10 
     11   @doc """
     12   Generate content from the module template for a given `node`
     13   """
     14   def module_page(module_node, nodes_map, config) do
     15     summary = module_summary(module_node)
     16     module_template(config, module_node, summary, nodes_map)
     17   end
     18 
     19   @doc """
     20   Get the full specs from a function, already in HTML form.
     21   """
     22   def get_specs(%ExDoc.TypeNode{spec: spec}) do
     23     [spec]
     24   end
     25 
     26   def get_specs(%ExDoc.FunctionNode{specs: specs}) when is_list(specs) do
     27     presence(specs)
     28   end
     29 
     30   def get_specs(_node) do
     31     nil
     32   end
     33 
     34   @doc """
     35   Format the attribute type used to define the spec of the given `node`.
     36   """
     37   def format_spec_attribute(module, node) do
     38     module.language.format_spec_attribute(node)
     39   end
     40 
     41   @doc """
     42   Get defaults clauses.
     43   """
     44   def get_defaults(%{defaults: defaults}) do
     45     defaults
     46   end
     47 
     48   def get_defaults(_) do
     49     []
     50   end
     51 
     52   @doc """
     53   Get the pretty name of a function node
     54   """
     55   def pretty_type(%{type: t}) do
     56     Atom.to_string(t)
     57   end
     58 
     59   @doc """
     60   Returns the HTML formatted title for the module page.
     61   """
     62   def module_type(%{type: :task}), do: ""
     63   def module_type(%{type: :module}), do: ""
     64   def module_type(%{type: type}), do: "<small>#{type}</small>"
     65 
     66   @doc """
     67   Gets the first paragraph of the documentation of a node. It strips
     68   surrounding white-spaces and trailing `:`.
     69 
     70   If `doc` is `nil`, it returns `nil`.
     71   """
     72   @spec synopsis(String.t()) :: String.t()
     73   @spec synopsis(nil) :: nil
     74   def synopsis(nil), do: nil
     75 
     76   def synopsis(doc) when is_binary(doc) do
     77     case :binary.split(doc, "</p>") do
     78       [left, _] -> String.trim_trailing(left, ":") <> "</p>"
     79       [all] -> all
     80     end
     81   end
     82 
     83   defp presence([]), do: nil
     84   defp presence(other), do: other
     85 
     86   defp enc(binary), do: URI.encode(binary)
     87 
     88   @doc """
     89   Create a JS object which holds all the items displayed in the sidebar area
     90   """
     91   def create_sidebar_items(nodes_map, extras) do
     92     nodes =
     93       nodes_map
     94       |> Enum.map(&sidebar_module/1)
     95       |> Map.new()
     96       |> Map.put(:extras, sidebar_extras(extras))
     97 
     98     ["sidebarNodes=" | ExDoc.Utils.to_json(nodes)]
     99   end
    100 
    101   defp sidebar_extras(extras) do
    102     for extra <- extras do
    103       %{id: id, title: title, group: group, content: content} = extra
    104 
    105       %{
    106         id: to_string(id),
    107         title: to_string(title),
    108         group: to_string(group),
    109         headers: extract_headers(content)
    110       }
    111     end
    112   end
    113 
    114   defp sidebar_module({id, modules}) do
    115     modules =
    116       for module <- modules do
    117         extra =
    118           module
    119           |> module_summary()
    120           |> Enum.reject(fn {_type, nodes_map} -> nodes_map == [] end)
    121           |> case do
    122             [] -> []
    123             entries -> [nodeGroups: Enum.map(entries, &sidebar_entries/1)]
    124           end
    125 
    126         sections = module_sections(module)
    127 
    128         pairs =
    129           for key <- [:id, :title, :nested_title, :nested_context],
    130               value = Map.get(module, key),
    131               do: {key, value}
    132 
    133         Map.new([group: to_string(module.group)] ++ extra ++ pairs ++ sections)
    134       end
    135 
    136     {id, modules}
    137   end
    138 
    139   defp sidebar_entries({group, nodes}) do
    140     nodes =
    141       for node <- nodes do
    142         id =
    143           if "struct" in node.annotations do
    144             node.signature
    145           else
    146             "#{node.name}/#{node.arity}"
    147           end
    148 
    149         %{id: id, anchor: URI.encode(node.id)}
    150       end
    151 
    152     %{key: HTML.text_to_id(group), name: group, nodes: nodes}
    153   end
    154 
    155   defp module_sections(%ExDoc.ModuleNode{rendered_doc: nil}), do: [sections: []]
    156 
    157   defp module_sections(module) do
    158     {sections, _} =
    159       module.rendered_doc
    160       |> extract_headers()
    161       |> Enum.map_reduce(%{}, fn header, acc ->
    162         # TODO Duplicates some of the logic of link_headings/3
    163         case Map.fetch(acc, header.id) do
    164           {:ok, id} ->
    165             {%{header | anchor: "module-#{header.anchor}-#{id}"}, Map.put(acc, header.id, id + 1)}
    166 
    167           :error ->
    168             {%{header | anchor: "module-#{header.anchor}"}, Map.put(acc, header.id, 1)}
    169         end
    170       end)
    171 
    172     [sections: sections]
    173   end
    174 
    175   @h2_regex ~r/<h2.*?>(.*?)<\/h2>/m
    176   defp extract_headers(content) do
    177     @h2_regex
    178     |> Regex.scan(content, capture: :all_but_first)
    179     |> List.flatten()
    180     |> Enum.filter(&(&1 != ""))
    181     |> Enum.map(&HTML.strip_tags/1)
    182     |> Enum.map(&%{id: &1, anchor: URI.encode(HTML.text_to_id(&1))})
    183   end
    184 
    185   def module_summary(module_node) do
    186     [Types: module_node.typespecs] ++
    187       function_groups(module_node.function_groups, module_node.docs)
    188   end
    189 
    190   defp function_groups(groups, docs) do
    191     for group <- groups, do: {group, Enum.filter(docs, &(&1.group == group))}
    192   end
    193 
    194   defp logo_path(%{logo: nil}), do: nil
    195   defp logo_path(%{logo: logo}), do: "assets/logo#{Path.extname(logo)}"
    196 
    197   defp sidebar_type(:exception), do: "modules"
    198   defp sidebar_type(:module), do: "modules"
    199   defp sidebar_type(:behaviour), do: "modules"
    200   defp sidebar_type(:protocol), do: "modules"
    201   defp sidebar_type(:task), do: "tasks"
    202 
    203   defp sidebar_type(:search), do: "search"
    204   defp sidebar_type(:cheatmd), do: "extras"
    205   defp sidebar_type(:livemd), do: "extras"
    206   defp sidebar_type(:extra), do: "extras"
    207 
    208   def asset_rev(output, pattern) do
    209     output = Path.expand(output)
    210 
    211     output
    212     |> Path.join(pattern)
    213     |> Path.wildcard()
    214     |> relative_asset(output, pattern)
    215   end
    216 
    217   defp relative_asset([], output, pattern),
    218     do: raise("could not find matching #{output}/#{pattern}")
    219 
    220   defp relative_asset([h | _], output, _pattern), do: Path.relative_to(h, output)
    221 
    222   @doc """
    223   Link headings found with `regex` with in the given `content`. IDs are
    224   prefixed with `prefix`.
    225   """
    226   @heading_regex ~r/<(h[23]).*?>(.*?)<\/\1>/m
    227   @spec link_headings(String.t() | nil, Regex.t(), String.t()) :: String.t() | nil
    228   def link_headings(content, regex \\ @heading_regex, prefix \\ "")
    229   def link_headings(nil, _, _), do: nil
    230 
    231   def link_headings(content, regex, prefix) do
    232     regex
    233     |> Regex.scan(content)
    234     |> Enum.reduce({content, %{}}, fn [match, tag, title], {content, occurrences} ->
    235       possible_id = HTML.text_to_id(title)
    236       id_occurred = Map.get(occurrences, possible_id, 0)
    237 
    238       anchor_id = if id_occurred >= 1, do: "#{possible_id}-#{id_occurred}", else: possible_id
    239       replacement = link_heading(match, tag, title, anchor_id, prefix)
    240       linked_content = String.replace(content, match, replacement, global: false)
    241       incremented_occs = Map.put(occurrences, possible_id, id_occurred + 1)
    242       {linked_content, incremented_occs}
    243     end)
    244     |> elem(0)
    245   end
    246 
    247   @class_regex ~r/<h[23].*?(\sclass="(?<class>[^"]+)")?.*?>/
    248   @class_separator " "
    249   defp link_heading(match, _tag, _title, "", _prefix), do: match
    250 
    251   defp link_heading(match, tag, title, id, prefix) do
    252     section_header_class_name = "section-heading"
    253 
    254     # NOTE: This addition is mainly to preserve the previous `class` attributes
    255     # from the headers, in case there is one. Now with the _admonition_ text
    256     # block, we inject CSS classes. So far, the supported classes are:
    257     # `warning`, `info`, `error`, and `neutral`.
    258     #
    259     # The Markdown syntax that we support for the admonition text
    260     # blocks is something like this:
    261     #
    262     #     > ### Never open this door! {: .warning}
    263     #     >
    264     #     > ...
    265     #
    266     # That should produce the following HTML:
    267     #
    268     #      <blockquote>
    269     #        <h3 class="warning">Never open this door!</h3>
    270     #        <p>...</p>
    271     #      </blockquote>
    272     #
    273     # The original implementation discarded the previous CSS classes. Instead,
    274     # it was setting `#{section_header_class_name}` as the only CSS class
    275     # associated with the given header.
    276     class_attribute =
    277       case Regex.named_captures(@class_regex, match) do
    278         %{"class" => ""} ->
    279           section_header_class_name
    280 
    281         %{"class" => previous_classes} ->
    282           # Let's make sure that the `section_header_class_name` is not already
    283           # included in the previous classes for the header
    284           previous_classes
    285           |> String.split(@class_separator)
    286           |> Enum.reject(&(&1 == section_header_class_name))
    287           |> Enum.join(@class_separator)
    288           |> Kernel.<>(" #{section_header_class_name}")
    289       end
    290 
    291     """
    292     <#{tag} id="#{prefix}#{id}" class="#{class_attribute}">
    293       <a href="##{prefix}#{id}" class="hover-link"><i class="ri-link-m" aria-hidden="true"></i>
    294       <p class="sr-only">#{id}</p>
    295       </a>
    296       #{title}
    297     </#{tag}>
    298     """
    299   end
    300 
    301   defp link_moduledoc_headings(content) do
    302     link_headings(content, @heading_regex, "module-")
    303   end
    304 
    305   defp link_detail_headings(content, prefix) do
    306     link_headings(content, @heading_regex, prefix <> "-")
    307   end
    308 
    309   templates = [
    310     detail_template: [:node, :module],
    311     footer_template: [:config, :node],
    312     head_template: [:config, :page],
    313     module_template: [:config, :module, :summary, :nodes_map],
    314     not_found_template: [:config, :nodes_map],
    315     api_reference_entry_template: [:module_node],
    316     api_reference_template: [:nodes_map],
    317     extra_template: [:config, :node, :type, :nodes_map, :refs],
    318     search_template: [:config, :nodes_map],
    319     sidebar_template: [:config, :nodes_map],
    320     summary_template: [:name, :nodes],
    321     redirect_template: [:config, :redirect_to],
    322     settings_button_template: []
    323   ]
    324 
    325   Enum.each(templates, fn {name, args} ->
    326     filename = Path.expand("templates/#{name}.eex", __DIR__)
    327     @doc false
    328     EEx.function_from_file(:def, name, filename, args, trim: true)
    329   end)
    330 end