Documentation Tooling
July 31, 2026 · View on GitHub
Cure ships three documentation surfaces that all read from the same
.cure sources:
cure doc-- static HTML generator (ExDoc-like two-pane layout).Cure.REPL.Markdown-- ANSI renderer for:doc/:helpin the REPL./stdliband/stdlib/:moduleon the Cure website.
v0.29.0 (Make Documentation Great) is the release that re-aligned
all three around a shared Markdown-to-HTML pipeline driven by the
NIF-free :md library and Makeup for syntax highlighting.
cure doc
CLI
cure doc [path|dir] [-o DIR]
[--title TITLE] [--main SLUG] [--extras PATH]
- Default discovery walks
lib/**/*.cureandlib/std/*.cure. A positional path narrows the walk; passing a file compiles a single module. --output-dir DIR(-o) sets the output directory. Default:_build/cure/doc.--title,--main,--extrasoverride the corresponding keys of the[doc]table inCure.toml.--extrasis repeatable.
Output layout
_build/cure/doc/
assets.js # filter / theme-toggle bundle
style.css # shared stylesheet
index.html # landing page (module index or a pinned main)
<extra-slug>.html # one per [doc].extras entry
<module-slug>.html # one per module extracted from .cure sources
The tree is self-contained. It can be zipped up, served from any
static host, or pointed at file:// for offline browsing.
The two panes
- Sidebar (left). Built once and embedded on every page. Lists
every entry in
[doc].extrasfollowed by every module, optionally grouped via[doc.groups_for_modules]. A keyboard-focusable filter input (press/) narrows the list in place. A theme toggle honoursprefers-color-schemeby default. - Content (right). Extras render as the Markdown-to-HTML output
of
Cure.Doc.Markdown.to_html/1. Module pages render the module's extracted doc map: a title, the module-level docstring, a local table of contents, and one anchored block per public function, type, and protocol.
Anchors
Every public symbol inside a module page gets a stable anchor so deep links remain valid across rebuilds:
#fn-<name>-- public functions.#type-<name>--typealiases and ADTs.#interface-<name>--interfacedeclarations.
When [doc].source_url is configured (or the built-in Cure site
falls back to the repository's GitHub path), each entry also carries a
"View source" link pointing at the corresponding line in the
.cure file.
Cure.toml [doc] section
All keys are optional; adding them progressively enhances the output.
[project]
name = "my_lib"
version = "0.1.0"
[doc]
main = "README"
title = "My Library"
extras = ["README.md", "CHANGELOG.md"]
logo = "priv/logo.svg"
source_url = "https://github.com/you/my_lib"
source_ref = "main"
[doc.groups_for_modules]
"Core" = ["MyLib.Core"]
"Accessories" = ["MyLib.Json", "MyLib.Http"]
Keys
main-- slug for the landing page. Matches either an extra slug (README->README.md) or a module name (MyLib.Core). When unset, the landing page is the module index.title-- title shown in the sidebar header. Falls back to<ProjectName> Documentation.extras-- array of relative Markdown paths. Each path is expanded against the directory that containsCure.toml, so the same configuration works whethercure docruns from the project root or a sub-directory.logo-- optional path / URL shown next to the sidebar title.source_url/source_ref-- GitHub prefix used for per-function / per-module source links.[doc.groups_for_modules]-- ordered mapping from group name to a list of module names. Modules that do not appear in any group fall into a trailing"Other"bucket so nothing is dropped silently.
Normalisation
The TOML parser and documentation-config normalisation live in
Cure.Project. The [doc] and [doc.groups_for_modules] tables become a
plain map shape that the generator consumes directly:
%{
main: String.t() | nil,
title: String.t() | nil,
extras: [String.t()],
logo: String.t() | nil,
source_url: String.t() | nil,
source_ref: String.t() | nil,
groups_for_modules: [{String.t(), [String.t()]}]
}
Cure.Doc.Markdown
Cure.Doc.Markdown.to_html/1 is a thin wrapper over Md.generate/1
with two escript-safe extras on top of the upstream behaviour:
Placeholder interpolation
{{cure_version}} and {{cure_vversion}} are substituted before
parsing, so release-sensitive copy can live inside docstrings without
a preprocess step at each call site. {{cure_vversion}} is just the
bare version prefixed with v.
Install with `mix escript.install hex cure {{cure_vversion}}` to get
this exact build.
Fenced-code syntax highlighting
When a fenced code block carries a known language, its contents are run through Makeup and emitted with the same CSS classes the Phoenix site uses. Known languages:
cure--Makeup.Lexers.CureLexerelixir--Makeup.Lexers.ElixirLexererlang--Makeup.Lexers.ErlangLexer
Unknown languages round-trip as <pre class="cure-doc-code"><code class="language-<lang>">...</code></pre> so downstream CSS can still
target them with a stable selector.
Why :md?
Earlier revisions routed HTML rendering through MDEx, the Rust-NIF
backing marcli. Inside an escript archive the NIF cannot be loaded
(the .so lives under priv/native/* inside a single-file archive
that the dynamic loader cannot mmap), so cure doc would fail on
every invocation outside mix run. Switching to :md is enough on
its own to keep the escript binary production-ready.
REPL Markdown renderer
Cure.REPL.Markdown.render/2 renders docstrings to ANSI text for the
REPL's :help and :doc commands. v0.29.0 promotes it from a flat
line-by-line renderer to a small block-aware parser that handles:
- ATX headings (
#,##,###). - Fenced code blocks (
```lang...```) and indented code blocks (four-space / tab indent). - Bullet lists (
-,*) and numbered lists (1.,2., ...). - Blockquotes (
>). - Inline backtick code,
**bold**,*italic*, and[text](url)links (rendered astext (url)). - Horizontal rules (
---,***) and blank-line paragraph separation.
The renderer is NIF-free (unlike the richer Marcli path) so :help
keeps working inside the escript archive.
/stdlib on the Cure website
The Cure website (site/ in the repo) ships an auto-generated
standard-library browser that mirrors cure doc output.
site/lib/cure_site/stdlib.exwalkscure/lib/std/*.cureat compile time and builds a doc map per module viaCure.Compiler.Lexer.tokenize/2,Cure.Compiler.Parser.parse/2, andCure.Doc.Extractor.extract/1. A curated@groups_for_moduleslist drives the sidebar grouping; unlisted modules fall into a trailing"Other"bucket.CureSiteWeb.StdlibControllerserves/stdlib(index) and/stdlib/:module(single-module page). The templates live undersite/lib/cure_site_web/controllers/stdlib_html/.- The old hand-written
site/priv/pages/standard-library.mdis gone;/standard-library301-redirects to/stdlibviaCureSiteWeb.RedirectController.
The site uses the same Cure.Doc.Markdown.to_html/1 as cure doc,
so the two views of the same stdlib module are visually consistent.
Writing docstrings
## blocks and blank-line merging
## comments above a definition (mod, fn, type, rec,
interface) attach as its docstring. Consecutive ## blocks separated
by a blank-line gap (or by plain # comments that the lexer drops)
are merged into a single Markdown body with a paragraph break
between blocks -- so a module-level docstring can read as natural
prose:
mod Std.List
## Eager, persistent, singly-linked lists.
##
## Every operation recurses over cons cells; there are no runtime
## arrays underneath.
##
## ## Examples
##
## ```cure
## use Std.List
##
## [1, 2, 3]
## |> Std.List.map(fn(x) -> x * 2)
## |> Std.List.sum() # => 12
## ```
### fenced multi-line docstrings
The ### fence still works for cases where a docstring would
otherwise collide with indentation-sensitive parent containers.
Leading indentation common to every body line is stripped:
mod MyApp
###
Longform prose here.
Fenced code blocks work as you would expect:
```cure
MyApp.run()
```
###
fn run() -> Atom = :ok
## Examples blocks
By convention every module-level docstring ends with an
## Examples section containing fenced cure blocks that actually
compile. The Cure stdlib enforces this end-to-end: the examples in
lib/std/*.cure round-trip through mix cure.compile_stdlib, and
four high-traffic Std.Core functions (compose, map_ok,
and_then, map_option) carry per-function examples on top of the
module-level block.
Repository Markdown and .cure docstrings follow the same rule. mix cure.check.docs
compiles every fence whose info string begins with the complete word
cure, including attributed forms such as ```cure path=demo.cure.
A fence may add expr or declarations when its shape is ambiguous.
An intentionally rejected example names its expected diagnostic, for example
```cure E093. The checker requires that exact error code and fails if a
different error appears, the example starts compiling, or the compiler crashes.
An example that compiles but cannot be written warning-free names the warning
instead, for example ```cure W000. That fence passes only when the
snippet compiles and emits the warning, so the warning stays part of the
documented behaviour rather than being waived; if the compiler later stops
emitting it, the fence fails and the example gets revisited. Macros that lift
whole modules are the usual reason to need this — see docs/MACROS.md §7.
Multiple diagnostic tags on one fence are invalid, whether E or W. An
incomplete design sketch is not Cure source and uses a plain text fence.
Ordinary cure fences always have to compile with no warnings at all.
See also
docs/TUTORIAL.mdChapter 13 -- walk-through for adding docstrings,Examples, and[doc]configuration to your own project.docs/LANGUAGE_SPEC.md-- doc-comment grammar (including blank-line merging and Markdown body rules).docs/STDLIB.md-- standard-library reference; every module ships with an## Examplesblock.lib/cure/doc/html_generator.ex-- authoritativecure docimplementation.lib/cure/doc/markdown.ex-- the shared Markdown-to-HTML pipeline.lib/cure/repl/markdown.ex-- the REPL's Markdown-to-ANSI renderer.