Markdown Fence Extensions

A MeshWeaver document is markdown, and some of its fenced blocks are alive: a ```csharp --render fence runs and shows the control it produced, a ```layout fence embeds a live layout area, a ```mermaid fence draws a diagram. This page is about the seam that makes that possible, and about the one thing everybody gets wrong on first contact with it: a fence is never rendered by the code that parses it.

The seam, in one sentence

The platform parses a fence and emits an inert HTML marker; a client replaces that marker with something interactive.

MeshWeaver (platform) MeshWeaver.Plugins (clients) Parser Markdig block parser Renderer emits the marker inert HTML <div class='layout-area' data-address=… > Blazor React React Native live control or plain HTML

The platform never renders an interactive fence. It emits a marker, and whichever client is on the other side decides what that marker becomes.

What lives where

Piece Home
Fence parsing + the marker's HTML src/MeshWeaver.Markdown (this repository)
The controls a marker can resolve to src/MeshWeaver.Layout (this repository)
Blazor hydration MeshWeaver.Blazor/Components/MarkdownHtmlRenderer.cs (MeshWeaver.Plugins)
React hydration clients/react/src/controls/interactiveMarkdown.ts (MeshWeaver.Plugins)
React Native hydration the RN cell renderers (MeshWeaver.Plugins)

Every renderer is in the other repository. That is the fact that decides the shape of any fence work: a new interactive fence is a two-repo change set, platform first, and the platform half is inert on its own by construction.

The existing markers

ExecutableCodeBlockRenderer and LayoutAreaMarkdownRenderer between them define the whole vocabulary. There are only two shapes, and a new fence should reuse one rather than mint a third:

Marker Emitted for What a client makes of it
<div class='layout-area' data-address=… data-area=… data-id=…> ```layout fences, and the result pane of every --render block A live layout area — the general-purpose escape hatch: anything expressible as a UiControl reaches every client through it
md-code-cell / code-content / md-code-cell-toolbar (+ data-submission-id, data-language) an executable block that shows its code The notebook cell: editor, output pane, Run bar on the bottom edge

The layout-area marker is the powerful one. A fence that can be expressed as "render this UiControl here" needs no client change at all — the clients already hydrate that div, and the control travels through the normal layout-area machinery. It comes in two forms, from the same builder (LayoutAreaMarkdownRenderer.GetLayoutAreaDiv / GetLayoutAreaDivOpenTag): empty, and wrapping fallback content for a client that cannot hydrate it — the ```prompt fence uses the second, and the degradation rule below is why.

🚨 The degradation rule

ExecutableCodeBlockRenderer states the contract for the cell markers outright: a client that does not hydrate them "sees an ordinary div and keeps rendering the fence read-only — the attributes are additive." Follow that rule for anything new.

A fence must never render as less than it did before the extension existed. The failure to avoid is a fence whose authored text disappears into a marker that one client turns into a rich widget and the others turn into nothing — the document is then worse on those clients than the plain fenced block it replaced, and nothing in CI can see it, because the platform's own tests only ever look at the marker.

Concretely: emit the authored content as ordinary markup as well as the marker, or wrap the marker so an un-hydrated client still shows the text. The ```prompt fence below is the worked example of the second — the marker carries the read-only fenced block as its children, which a hydrating client drops on its way to mounting the live area.

Worked example: the prompt fence (#2511)

Course pages author suggested AI prompts as ```prompt fences. They used to render as static fenced code: readable, but not editable and not runnable. The request was that such a fence become a composer pre-filled with the authored text, whose Submit starts a real agent thread and opens it full page.

Walking it through the seam gives the whole change set — and shows why the interesting half is not in this repository.

The platform half (this repository)

  1. Parse. ExecutableCodeBlock.Initialize derives PromptDraft from the fence body whenever the info string is prompt, next to the layout block it already parses. A prompt fence never produces a SubmitCodeRequest — it is prose for an agent, not source for the kernel, and saying so in GetSubmitCodeRequest means a stray --render on one cannot turn it into a code cell.
  2. Emit. ExecutableCodeBlockRenderer.WritePromptComposer lowers it to the layout-area marker rather than a new one, pointing at the Prompt area on the page node's own hub. Nothing new has to be taught to any client for the composer to appear.
  3. Carry the draft. The authored text rides as the area's reference id, base64url-encoded (PromptFence.EncodeDraft). Not raw: an area id is concatenated into hrefs, and everything after a ? in one is parsed as reference parameters — and a prompt is prose, full of /, ?, & and newlines. This is the same encoding, for the same reason, as LayoutAreaReference.GetMeshNodeDataContext.
  4. The control. MeshNodeLayoutAreas.PromptComposer returns the composer that already exists — ThreadChatControl, the same control the side panel and the Threads app mount — with the decoded draft on its new InitialDraft property and HideEmptyState on. That flag is load-bearing: ThreadChatView reads it as isCompact and navigates to the created thread full page instead of handing it to the side panel. "Submit starts a full-page thread" is that flag.
  5. Degrade. The marker wraps the ordinary read-only fenced block. A client that hydrates layout areas replaces the div and drops its children; one that does not renders them — the authored prompt, exactly as it read before. With no owning node there is no hub to serve the area, so the fence stays a plain block rather than emitting an ownerless address.

The client half (MeshWeaver.Plugins)

ThreadChatView must seed its composer from ThreadChatControl.InitialDraft — one-shot, into a NEW chat only, so a draft can never clobber text the user is already typing. The machinery is there already: SeedPendingDraftIfAny does exactly this job from a different source (the side panel's one-shot PendingComposerDraft, the "new thread from this cell" hand-off), and the declarative draft is the second source feeding it.

Starting the thread needs nothing new: Hub.StartThread and the full-page navigation are what ThreadChatView's submit already does in compact mode.

The React client hydrates layout-area markers with a regex that matches an empty div (interactiveMarkdown.ts), so a wrapped marker falls through to the fallback there and shows the prompt read-only — correct by the degradation rule, and a one-line widening away from the composer when that client wants it.

Verification

Each half is asserted where its subject lives, which for a fence means two repositories:

Suite Repo What it asserts
MeshWeaver.Markdown.Test/PromptFenceComposerTest MeshWeaver.Plugins the fence produces the expected marker, the draft round-trips through the area id, the marker wraps the read-only fence, no owner ⇒ a plain block, and the fence never reaches the kernel
MeshWeaver.Graph.Test/PromptComposerAreaTest this repo the Prompt area produces a compact ThreadChatControl carrying the authored text

🚨 MeshWeaver.Markdown is compiled here but TESTED in MeshWeaver.Plugins — that repo owns MeshWeaver.Markdown.Test (it project-references this one through $(MeshWeaverRoot)), and this repo has no test project referencing MeshWeaver.Markdown at all. A rendering assertion written here has no home; it goes in the markdown suite, over there. The corollary is that a green platform build proves nothing at all about a fence's HTML — the run that does is the other repo's.

And the half that a learner can actually see is in neither — there is no Blazor in this repository — so the acceptance check is a rendered page: open a course lesson that ships a ```prompt fence, confirm the composer shows the authored text, edit it, submit, and land on the full-page thread.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.