Interface for Julia’s Docsystem

Not much is needed from the docsystem, we just build each Binding’s docstrings as separate pages, as well as a table with links to each docstring page. There’s no concept of @docs and @autodocs found in Documenter.jl.

"""
    docstrings(p)

Extract all available docstrings for given `Project` `p` and return a
`files::Dict` containing a mapping from absolute path to `File` object and an
`index` object, which is [`File`](#) object containing a formatted table of all
docstrings with information on `name`, `module`, `visibility`, and `category`
of each docstring.
"""
function docstrings(p::Project)
    toc_root = dirname(joinpath(dirname(p.project.name), p.env["publish"]["toc"]))
    docstring_dir = joinpath(toc_root, "docstrings")
    mod = project_to_module(p.env)
    files = Pair{String,File}[]
    table = IOBuffer()
    println(table, "{#docstring-index}")
    println(table, "| Name | Module | Visibility | Category |")
    println(table, "|:---- |:------:|:----------:| --------:|")
    for each in p.mods
        root = rootmodule(each)
        if root === mod
            for (k, v) in Docs.meta(each)
                object_name = string(k.var)
                module_name = string(k.mod)
                visibility = Base.isexported(k.mod, k.var) ? "public" : "private"
                category = categorise(k)
                # Write the docstring file, may concatenate several similar
                # docstrings -- those with the same name, but different
                # signatures.
                filename = joinpath(docstring_dir, "$k.md")
                io = IOBuffer()
                println(io, "```{=html}\n<div class='docs' id='$(k.var)'>\n```")
                println(io, "*`$visibility`* **`$object_name`** --- `$category`\n")
                for (n, sig) in enumerate(v.order)
                    println(io, "```{=html}\n<div class='doc' id='$n'>\n```")
                    println
                    doc = v.docs[sig]
                    println(io)
                    printdoc(io, doc)
                    println(io)
                    println(io, "```{=html}\n</div>\n```")
                end
                println(io, "```{=html}\n</div>\n```")
                file = File(
                    name = filename,
                    mime = MIMETYPES[".md"],
                    node = load_markdown(io),
                    dict = Dict("module" => each),
                )
                push!(files, filename => file)
                # Add an entry to the table index for this name.
                println(table, "| [`$object_name`](docstrings/$(k).html) | `$module_name` | `$visibility` | `$category` |")
            end
        end
    end
    sort!(files; by=first)
    index = File(
        name = joinpath(toc_root, "docstrings.md"),
        mime = MIMETYPES[".md"],
        node = load_markdown(table),
    )
    docs = OrderedDict{String,File}()
    docs[index.name] = index
    for (path, file) in files
        docs[path] = file
    end
    return docs
end

Helpers

"""
    printdoc(io, docstr)

This method prints out the parts of an individual `Docs.DocStr` object to the
given `IO` object `io`. It is used to get the "formatted" content of the
docstring without it being pre-parsed by the `Markdown` standard library.
"""
function printdoc(io::IO, docstr)
    for part in docstr.text
        Docs.formatdoc(io, docstr, part)
    end
    return io
end

"""
    rootmodule(m)

The "root" module of a given `Module` `m`.
"""
rootmodule(m::Module) = (p = parentmodule(m); p === m ? m : rootmodule(p))

"""
    categorise(binding)

For the given `Docs.Binding` object, determine and return its "category",
namely either "constant", "global", "struct", "type", "parametric struct",
"parametric type", "module", "macro", or "function.
"""
function categorise(binding)
    # Helpers.
    ismacro(binding) = startswith(string(binding.var), '@')
    # Categoriser.
    category(other)         = isconst(binding.mod, binding.var) ? "constant" : "global"
    category(obj::Module)   = "module"
    category(obj::DataType) = isconcretetype(obj) ? "struct" : "type"
    category(obj::UnionAll) = isconcretetype(obj) ? "parametric struct" : "parametric type"
    category(obj::Function) = ismacro(binding) ? "macro" : "function"

    if Docs.defined(binding)
        object = Docs.resolve(binding)
        return category(object)
    else
        return "undefined"
    end
end