Skip to content

<Content /> is not substituted when nested inside another element in a component body #328

Description

@taras

Motivation

§6.3 says <Content /> "inside a component body" is replaced by the caller's children. The engine substitutes it only at a body's top level, or directly inside <Output>. Nested anywhere else it survives substitution, reaches component resolution, and fails — Content is reserved, so nothing can supply it.

The failure message describes a real rule ("it means something only inside a component's body") but the element is inside a component's body, which makes the diagnosis misleading as well as the behavior surprising.

Reproduction

All three fail identically on 511776e. The wrapper's kind does not matter — a Markdown component, a core function component, and a structural construct all reproduce it.

<!-- components/Echo.md -->
<Output>ECHO[<Content />]</Output>
<!-- components/Host.md — nested inside another invocation -->
<Echo as="e">prefix <Content /> suffix</Echo>
<Output>host got: {e}</Output>
<!-- components/HostTemp.md — nested inside a core function component -->
<Capture as="c"><TempDir>dir wrapped: <Content /></TempDir></Capture>
<Output>host got: {c}</Output>
<!-- components/HostCap.md — nested inside a structural construct -->
<Capture as="c">captured: <Content /></Capture>
<Output>host got: {c}</Output>

Invoked as <Host>MATERIAL</Host>, each exits 1 with:

<Content /> renders the content its invocation was given, so it means something
only inside a component's body. <Content> is reserved: it never resolves a
component, so a repository file named Content cannot supply it.

Working control: <Output><Content /></Output> as the whole body, and <Content /> as a top-level body segment, both substitute correctly.

Cause

substituteSegmentList (packages/core/src/expand.ts:2668) maps over a body's segments, handles Content and interpolates text, and for any other component segment returns [segment] — it never recurses into segment.children. buildBody calls it once per top-level segment and once for each <Output> region's direct children, which is exactly the set of positions that work.

Why it matters

Passing caller-supplied material into a prompt is the natural use, and it is unavailable:

<Prompt as="answer">
  Material to assess:

  <Content />
</Prompt>

The workaround is to take the material as a prop and interpolate {props.material}, which works anywhere. That is a fine interface, but it is a different one, and a component author has no way to learn the restriction except by hitting it — the spec does not state it and the error does not describe it.

Acceptance criteria

  • <Content /> and <Content slot="…" /> substitute wherever they appear in a component body, including inside another component's invocation, inside a structural construct, and nested several levels deep.
  • Slot partitioning, the once-only slot errors of §6.3.3, and the content-scope rule of §6.3.4 are unchanged — a nested projection still runs in the invocation's content scope and still stops before the component releases its own.
  • Two <Content /> elements at different depths behave the same as two at top level.
  • §6.3 states the resolved rule, and a conformance row covers a nested position.
  • If the restriction is instead deliberate, the spec says so and the error message names the actual rule, so the failure is diagnosable.

Impact

Found while synchronizing the #181 living end-goal target (#292). Two authored components used the pattern and could not expand; both now take the material as a prop and the target classifies this as missing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions