The two behaviours every extractor shares, in one place.
DocShell.Generate.ExDoc, Guides, and Livebooks differ in what they read
and agree on how they read it. Both agreements live here so they cannot
quietly diverge:
Collect or stop.
map_ok/2walks a list, keeps{:ok, entry}results in order, drops{:ok, nil}for sources with nothing to document, and abandons the whole run on the first{:error, reason}. Extraction is all-or-nothing by design — seeDocShell.Buildfor why a half-built documentation set is the failure worth avoiding.Title fallback.
title/2takes the first Markdown H1 as a document's title, since that is where every convention puts it, and falls back to a caller-supplied value when the document has no heading at all.
This module is public because hosts writing their own extractors want the same semantics, not because the pipeline needs it to be.
Summary
Functions
Maps fun over items, collecting {:ok, entry} results in order.
Derives a title from the first Markdown H1, falling back to fallback.
Derives the first top-level H1 title from an already parsed AST.
Functions
@spec map_ok(Enumerable.t(), (term() -> {:ok, term() | nil} | {:error, term()})) :: {:ok, [term()]} | {:error, term()}
Maps fun over items, collecting {:ok, entry} results in order.
{:ok, nil} entries are skipped; the first {:error, reason} short-circuits
and is returned as-is.
Examples
iex> DocShell.Generate.Collector.map_ok([1, 2, 3], &{:ok, &1 * 2})
{:ok, [2, 4, 6]}
iex> DocShell.Generate.Collector.map_ok([1, 2, 3], fn
...> 2 -> {:ok, nil}
...> value -> {:ok, value}
...> end)
{:ok, [1, 3]}
iex> DocShell.Generate.Collector.map_ok([1, 2, 3], fn
...> 2 -> {:error, :bad}
...> value -> {:ok, value}
...> end)
{:error, :bad}
Derives a title from the first Markdown H1, falling back to fallback.
The parsed AST determines headings, including Setext headings. Code fences follow the same Markdown grammar as the rendered body. Inline markup is flattened without inserting spaces into words.
Examples
iex> DocShell.Generate.Collector.title("# Getting Started\n\nBody.", "intro")
"Getting Started"
iex> DocShell.Generate.Collector.title("Body with no heading.", "intro")
"intro"
@spec title_from_ast([DocShell.Ast.ast_node()], term()) :: String.t()
Derives the first top-level H1 title from an already parsed AST.