lib/zig.doc.ex

defmodule Zig.Doc do
  @moduledoc """
  Incorporates Zig documentation from selected files into Elixir projects.

  ## Usage

  1. Include the zig files you wish to document in your project docs:
    ```elixir
      def project do
        [
          docs: [
            ...
            zig_doc: [name_of_module: [file: "path/to/file.zig"]]
          ]
        ]
      end
    ```

  2. (optional) alias `Zig.Doc` in your `mix.exs`:
    ```elixir
    def project do
      [
        ...
        aliases: [
          docs: ["zig_doc"]
        ]
        ...
      ]
    end
    ```
    > ### Note {: .info }
    >
    > This step is required if you want HexDocs.pm to include the zig
    > documentation with your main documentation.

  ## Documentation forms

  Currently, Zigler recognizes the following forms of documentation:

  - **files**

    This form is set using the `//!` at the top of a document, and will
    be used as the "module-level" documentation for the ExDoc result.

  - **functions**

    Public functions: `pub const <identifier> = <value that is a function>;`
    and publicly declared functions: `pub fn <identifier>(<arguments>) <type> { <block> }`
    are both recognized and converted to ExDoc-style function documentation.

  - **types**

    `pub const <type> = <expression that is a type>;` is recognized and
    converted into ExDoc-style type documentation.

    functions which return `type` are also recognized and converted into type
    documentation.

  - **constants**

    `pub const <identifier> = <expression that is a constant>;` is recognized
    and converted into ExDoc-style function documentation under the category
    `Constants`.

  - **variables**

    `pub var <identifier> = ...;` is recognized and converted into ExDoc-style
    function documentation under the category `Variables`.

  ## Tags

  preamble documentation comments like so

  ```zig
  \\\\\\ \<!-- \<tag0\>; \<tag1\>;...;\<tagn\> \--\>
  ```

  to tag the content.  Currently available tags:

  - `ignore`: ignores the content.
  - `topic: <topic>`: (non-type functions only) sets the topic of the content.  This
    will be used to group functions in the ExDoc output, the function group
    will be rendered as `Functions (<topic>)`.
  - `args: <arg0>,<arg1>,...,<argn>` (const functions only) sets argument names when
    the function is rendered as a constant.  Use _ to signify that the name should
    not be used.

  > ### Warning {: .warning }
  >
  > Currently `Zig.Doc` is lazily written to support the use case for `beam.zig`
  > found in the the main Zigler project.  It is very likely that custom zig files
  > used in a nif might not be correctly parsed.  If you find this to be the case,
  > please file an issue at: https://github.com/E-xyza/zig_doc/issues
  """

  alias Zig.Doc.Generator

  @doc """
  Generates documentation for the given `project`, `vsn` (version)
  and `options`.

  Lifted and modified from `ExDoc.generate_docs` in ex_doc 0.29.4.
  """
  @spec generate_docs(String.t(), String.t(), Keyword.t()) :: atom
  def generate_docs(project, vsn, options)
      when is_binary(project) and is_binary(vsn) and is_list(options) do
    # retrieve zig_doc specific options and remove them
    {zig_doc_options, options} =
      case Keyword.pop(options, :zig_doc) do
        {nil, _} ->
          Mix.raise("zig_doc option not found in Mix.Project.config/0")

        tuple ->
          tuple
      end

    config = ExDoc.Config.build(project, vsn, options)

    if processor = options[:markdown_processor] do
      ExDoc.Markdown.put_markdown_processor(processor)
    end

    docs =
      config.source_beam
      |> config.retriever.docs_from_dir(config)
      |> add_zig_doc_config(config, zig_doc_options)

    find_formatter(config.formatter).run(docs, [], config)
  end

  @doc false
  def add_zig_doc_config({docs, _}, exdoc_config, zig_doc_options, sema_module \\ Zig.Sema) do
    docs ++
      Enum.map(zig_doc_options, &Generator.modulenode_from_config(&1, exdoc_config, sema_module))
  end

  ################################################################
  #
  # private functions lifted from ExDoc:

  # Short path for programmatic interface
  defp find_formatter(modname) when is_atom(modname), do: modname

  defp find_formatter("ExDoc.Formatter." <> _ = name) do
    [name]
    |> Module.concat()
    |> check_formatter_module(name)
  end

  defp find_formatter(name) do
    [ExDoc.Formatter, String.upcase(name)]
    |> Module.concat()
    |> check_formatter_module(name)
  end

  defp check_formatter_module(modname, argname) do
    if Code.ensure_loaded?(modname) do
      modname
    else
      raise "formatter module #{inspect(argname)} not found"
    end
  end
end