Skip to main content

Module image

Module image 

Source
Expand description

Image HTML synthesizer — the single entry point for emitting <img> / <picture> markup in moss output.

See docs/reference/structural-html-emission.md for the architectural principle: structural HTML decisions are made at the typed-data layer (pulldown-cmark events, shortcode AST, typed component props), with all three call sites converging on the function in this module. Regex post-passes are reserved for non-markdown-origin attribute injection only.

§Migration state (post-Step-7, 2026-05-16)

Steps 1-7 of the structural-html-emission migration are complete:

  • Step 1: extracted synthesize_image_html
  • Step 2: routed markdown Tag::Image events
  • Step 3: routed :::hero shortcode image
  • Step 4: routed link-preview favicon
  • Step 6: routed every cover image path (folder cards, child summary cards, folder index hero, photo/video gallery thumbnails) through render_cover_html
  • Step 7: retired wrap_img_in_picture (the structural part of the legacy regex post-pass). The synthesizer now owns every <picture> wrap in moss output. add_image_placeholder_attributes survives as the attribute-injection seam for the documented carve-outs (see below).

The byte-shape contract is captured by snapshot tests at the bottom of this file. They are the line of defense against accidental output drift; any future change to attribute order, quoting, or whitespace must update them deliberately.

Later steps will:

  • Step 8: switch MarkdownStandalone to a <figure class="moss-image"> wrapper (breaking change for user themes — staged separately)
  • Add AVIF <source> lines once the image pipeline produces AVIF
  • Drop the inline LQIP style= in favor of a wrapper CSS custom prop

§Step-8 contract: synthesizer owns the outer <figure> (planned)

transform_events currently wraps the synthesizer’s MarkdownStandalone output in its own <figure> for the three caption-pattern branches (image+emphasis, separate-emphasis, implicit-figure). After Step 8 the synthesizer emits <figure class="moss-image"> itself; if transform_events still wraps, the output will be `

...
` — invalid double-wrap.

The Step-8 contract: caption flows into the synthesizer via MarkdownStandalone { caption: Option<&str> } (or a richer CaptionMarkdown for emphasis-in-caption support), and the three caption-pattern branches collapse into a single Event::Html( synthesize_image_html(..., MarkdownStandalone { caption })) emission. The <figcaption> becomes the synthesizer’s responsibility, NOT transform_events. Captures the spec at docs/reference/structural-html-emission.md#output-shape.

§Carve-outs: bare <img> emitters not routed through the synthesizer

Four emission paths land bare <img> HTML in the output stream that does NOT flow through synthesize_image_html. They rely on the regex post-pass (build/media/placeholder.rs::add_image_placeholder_attributes) for attribute injection (dims/loading/decoding/LQIP). None of them are bugs — each has a documented architectural reason to stay outside the synthesizer:

  • Site logo (build/components/nav.rs::render_logo) — themed UI affordance, not content. The logo has its own CSS sizing (.site-logo { height: 1.8em }) and does not participate in LQIP/dims/WebP-variant rendering.
  • RSS read-tracking pixel (build/feeds/rss.rs) — 1×1 <img> not rendered visibly; the synthesizer’s dims fallback (800×600) and LQIP would be wrong for this case.
  • Email body images (infra/newsletter.rs) — email clients (Gmail, Outlook, Apple Mail) do not consistently support <picture> or data-placeholder-src-driven hydration. Keep flat for cross-client degradation.
  • Raw HTML <img> in markdown source — author-written <img src="..."> literally embedded in .md files. pulldown-cmark emits these as Event::Html opaque pass-through, so they never reach Tag::Image and are not a moss-controlled emitter. Treated as user input, the markdown HTML is opaque to the synthesizer and gets only the additive attribute injection pass.

Photography/video gallery thumbnails — previously a carve-out — were folded into the synthesizer in Step 7’s commit. The review colophon cover (build/features/review.rs::render_colophon) — also previously a carve-out — was folded in 2026-05-16. Both use ImageContext::FolderCardCover (container-bounded thumbnail semantics).

These four remaining carve-outs are flagged here so future maintenance does not drop their attribute injection. Step 7 retired the structural part of the regex (wrap_img_in_picture); the surviving add_image_placeholder_attributes provides additive attrs only for these bare-img paths.

Structs§

ImageRenderOptions
Optional rendering attributes a caller may pass.

Enums§

ImageContext
Where the image lives in the document, which determines the wrapper element and attribute set.

Functions§

synthesize_image_html
Synthesize the HTML for an image reference.
wrap_in_figure
Wrap an already-shaped image fragment in the standalone-image figure container.