Gamend.Content.Markdown (gamend_core v1.0.1296)

Copy Markdown View Source

Markdown to HTML, the way every collection renders it.

One pipeline: strip the frontmatter, mend table separators, parse with MDEx, rewrite the tree — code-language aliases, mermaid fences, admonitions, links written to a neighbouring .md — render through the sanitiser, then fix up image paths on the HTML.

The rewrites work on MDEx.Document nodes rather than on rendered HTML: a fence inside a list item is still a MDEx.CodeBlock, a :::tip inside a quote still a MDEx.BlockDirective, and a link's url is a field rather than an attribute a regex has to find. The one pass left on the HTML is the image one, because a <figure> written in raw HTML carries an <img> no node stands for, and every image gets the same loading/decoding attributes either way.

Raw HTML

Rendered with unsafe: true and always sanitised. The allowlist below is what makes that safe: a fixed set of tags a guide legitimately contains — <figure>, <video>, <details>, a <div class> for a gallery, an inline <svg> for an icon — with the attributes each of them needs and nothing that runs. Content is the operator's own files, so the sanitiser is a guard against a pasted snippet, not against an adversary; it stays on because a rule that is on for everyone is a rule nobody has to remember.

Admonitions

Two spellings, one markup. :::tip[Title] … ::: is what Docusaurus authors write; > [!TIP] is GitHub's. MDEx parses the first as a MDEx.BlockDirective whose info is tip[Title] and the second as a MDEx.Alert. Both become a directive rendered as <div class="admonition admonition-tip"> with a <p class="admonition-title"> as its first child, which is the one thing a stylesheet has to know. A raw <div class="note"> an author wrote is HTML, not a directive, and stays as written.

Summary

Functions

The first picture in a markdown body, at the URL the rendered page serves it from (image_src/2, before any :image_url), or nil.

The URL an image written in a collection's markdown is served from.

Inline markup removed, entities left as they are.

Render markdown source. The frontmatter, if any, is dropped.

Render a file, or nil when it cannot be read or parsed.

The h2 and h3 sections of rendered HTML: each heading as toc/1 gives it, plus :lede, the first sentence of the paragraph that opens the section, or nil when the section opens with a list, a table or code.

Drop a leading <h1>: the page renders the title itself, so leaving it in prints it twice. Attribute-tolerant, because headings carry ids now.

The headings of rendered HTML, for a table of contents: h2 and h3 with their ids and plain text.

Types

opt()

@type opt() ::
  {:collection, String.t()}
  | {:assets, :content | :static}
  | {:dir, String.t()}
  | {:base_path, String.t() | nil}
  | {:slug, String.t() | nil}
  | {:index, boolean()}
  | {:id, String.t()}
  | {:image_url, (String.t() -> String.t()) | nil}
  • :collection — the registered name, for /content/<collection>/ asset paths
  • :assets — :content (the default) rewrites image paths into the collection's asset route; :static leaves root-absolute paths alone, for a site whose images are served from priv/static
  • :dir — the file's folder relative to the collection root, for relative image paths
  • :base_path — the route prefix a .md link rewrites to; nil leaves such links untouched
  • :slug — the document's slug, which relative links resolve against
  • :index — the document is a folder's index.md, whose slug is its folder, so its links resolve against the slug rather than its parent. render_file/2 sets it from the file name.
  • :id — a stable prefix for element ids (mermaid diagrams need one)
  • :image_url — given each image's URL once it is resolved (never an http or data: one), answers the URL to serve instead: a smaller copy the host has built, say. Gamend.Content passes the collection's registered :image_url here

Functions

first_image(body, opts \\ [])

@spec first_image(String.t(), [opt()]) :: String.t() | nil

The first picture in a markdown body, at the URL the rendered page serves it from (image_src/2, before any :image_url), or nil.

A markdown image or an <img> written in raw HTML, whichever comes first; one inside a code block is code, not a picture. For a card that has only the post itself to take a picture from. Give it the body, after the frontmatter.

image_src(src, opts \\ [])

@spec image_src(String.t(), [opt()]) :: String.t()

The URL an image written in a collection's markdown is served from.

Points it at /content/<collection>/…, the host content asset route, in the three ways authors write it:

  1. Relative: gamend/auth.png → /content/blog/gamend/auth.png
  2. Absolute: /gamend/auth.png → /content/blog/gamend/auth.png
  3. Type-prefixed: /blog/gamend/auth.png → /content/blog/gamend/auth.png

With assets: :static, an absolute path is a URL the host serves and is left alone; only relative ones are resolved, against the file's folder (:dir). External URLs (http…, data:) and paths already under /content/ come back as they are.

plain_text(html)

@spec plain_text(String.t()) :: String.t()

Inline markup removed, entities left as they are.

render(content, opts \\ [])

@spec render(String.t(), [opt()]) :: {:ok, String.t()} | {:error, term()}

Render markdown source. The frontmatter, if any, is dropped.

render_file(path, opts \\ [])

@spec render_file(Path.t(), [opt()]) :: String.t() | nil

Render a file, or nil when it cannot be read or parsed.

sections(html)

@spec sections(String.t() | nil) :: [
  %{id: String.t(), text: String.t(), level: 2 | 3, lede: String.t() | nil}
]

The h2 and h3 sections of rendered HTML: each heading as toc/1 gives it, plus :lede, the first sentence of the paragraph that opens the section, or nil when the section opens with a list, a table or code.

strip_first_h1(html)

@spec strip_first_h1(String.t()) :: String.t()

Drop a leading <h1>: the page renders the title itself, so leaving it in prints it twice. Attribute-tolerant, because headings carry ids now.

toc(html)

@spec toc(String.t() | nil) :: [%{id: String.t(), text: String.t(), level: 2 | 3}]

The headings of rendered HTML, for a table of contents: h2 and h3 with their ids and plain text.

Read from the HTML because that is what the cache holds; the renderer puts the id first on the heading tag, so the pattern is stable.