New callout features in @lossless-group/lfm
remark-callouts grew up: multi-line callouts no longer silently fall through, hyphenated type names like [!llm-response] now parse, Obsidian's foldable [!type]- syntax is supported, and the pipeline doc codifies the policy that callout bodies must support every LFM feature — directives, citations, embeds, even nested callouts.
Why Care?
If you’ve ever written an Obsidian-style callout that ran across more than one line and watched it render as a plain blockquote in your published site, you weren’t crazy. The published @lossless-group/lfm had a quiet bug: the regex that detects > [!type] Title was matched against the entire first text node, and CommonMark folds soft-wrapped lines into one node separated by \n. With no /m flag, ^...$ failed, the transform was skipped, and your callout silently downgraded itself to a <blockquote>.
Worse, hyphenated type names — [!llm-response], [!image-gallery] — never matched at all because \w+ doesn’t include hyphens. An audit of one site’s essay archive turned up six callouts marked [!LLM-Response] that had been rendering as plain blockquotes for months without anyone noticing. There was no error path. Unmatched blockquotes just stay blockquotes.
Both bugs are now fixed. The plugin is also opinionated about something Obsidian and CommonMark are not: callout bodies are full participants in the LFM render pipeline. Citations, directives, code blocks, embeds, even nested callouts — they all work inside a callout. That used to be implicit. Now it’s documented.
What’s New?
-
Multi-line first-paragraph fix. The plugin now splits the first text node on
\nand matches the regex against line 1 only. Body lines flow into the callout’s first paragraph alongside any inline siblings. Multi-line callouts work whether the title and body are separated by a blank>line or not. -
Hyphen-friendly type names. Regex broadened from
\[!(\w+)\]to\[!([\w-]+)\]. This unlocks two real authoring conventions: hyphenated semantic types ([!llm-response]) and the directive-style hyphen vocabulary already used elsewhere in LFM (:::image-gallery,:::tooling-gallery). -
Obsidian foldable syntax.
[!type]-now emits an explicit empty title (renderer suppresses the header row), distinct from[!type]with no title (renderer falls back to the default label). Three states, three behaviors:Authoring attrs.titleRenders [!info] Title"Title"Header shows “Title” [!info]undefined Header shows default label “Info” [!info]-""(explicit empty)No header row -
Codified nesting policy. A new section in
context-v/Maintain-Lossless-Markdown-and-Extended-Markdown-Render-Pipeline.md(§7) makes the contract explicit:Callouts in LFM are containers, not rendering-only blocks. Every LFM and AstroMarkdown feature available at the document level — directives, citations, fenced code blocks, GFM tables, link previews, video embeds, and other callouts — MUST work when nested inside a callout body.
This is a deliberate divergence from CommonMark (blockquotes that don’t participate in extended pipelines) and Obsidian (callouts that support some inline markdown but not the full directive/embed/citation stack). The structural reason it’s safe: a callout is just a
containerDirectivewhose children are ordinary block-level MDAST nodes, so recursing through the same renderer used for the document body is simpler than maintaining an “allowed inside callouts” list that would inevitably drift.The doc states a hard implementation requirement: any Callout component MUST recurse via
<AstroMarkdown node={child} />. NevertoString(node)or a plain-text fallback — that path silently disables nesting and is the most common reason a freshly-copied Callout looks “fine” until an author tries to embed something inside it.
How We Found It
The bugs surfaced during a Callout system integration in mpstaton-site (separate changelog over there). A test markdown page authored with every supported callout type rendered fine for single-line examples — but as soon as a callout had a body line under its title, the styling collapsed back to plain blockquote. Walking the MDAST in the dev tools showed that containerDirective nodes simply weren’t being produced for those callouts. The transform had silently failed.
Reproducing the regex behavior in a one-liner clinched it:
'[!note] Note\nA standard note.'.match(/^\[!(\w+)\]\s*(.*)?$/)
// → null
'[!note] Note\nA standard note.'.match(/^\[!(\w+)\]\s*(.*)?$/m)
// → [match, 'note', 'Note', ...]
The fix is intentionally broader than just adding /m — splitting on the first newline first is more deterministic across edge cases (escaped newlines, lone \r, etc.) and made the foldable-- and hyphenated-type fixes natural to land in the same pass.
What’s Next
- Rename to
remark-lossless-callouts. This plugin and its siblings (remark-lossless-citations,remark-lossless-link-preview) deserve a namespace that signals “this is the LFM flavor, not the standard remark plugin.” Collisions with community packages on npm are real and the@lossless-group/lfmpackage name only half-protects us. Tracked for a 0.2.0 minor bump. - More canonical types.
llm-responseandexcerptgot promoted to canonical in mpstaton-site. They’re worth promoting in the canonical Callout pattern (packages/lfm-astro/components/) too, with their own icons and accent contracts.