DocShell.Generate.Collector (DocShell v0.4.0)

Copy Markdown View Source

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/2 walks 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 — see DocShell.Build for why a half-built documentation set is the failure worth avoiding.

  • Title fallback. title/2 takes 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.

Reuses an already parsed body for title extraction when it contains no fences.

Derives the first top-level H1 title from an already parsed AST.

Functions

map_ok(items, fun)

@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}

title(markdown, fallback)

@spec title(String.t(), term()) :: String.t()

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"

title(markdown, fallback, nodes)

@spec title(String.t(), term(), [DocShell.Ast.ast_node()]) :: String.t()

Reuses an already parsed body for title extraction when it contains no fences.

Fenced input retains the defensive title-only parse used by title/2, because permissive closing-fence parsing must not expose code comments as headings. The supplied AST must be the parse of the supplied Markdown, not another page.

title_from_ast(nodes, fallback)

@spec title_from_ast([DocShell.Ast.ast_node()], term()) :: String.t()

Derives the first top-level H1 title from an already parsed AST.