From d05607188cf6f76f4bb5f9906215405305cedfa0 Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:19:10 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=92=A5=20Select=20the=20=20branch?= =?UTF-8?q?=20by=20JavaScript=20truthiness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolved condition selects a branch with `!!value` instead of being rejected unless it is a boolean, so a document branches on the value it already has — `` — without converting an optional string first. An expression condition is evaluated directly rather than through `resolveExpressionProps()`, which rejects `undefined` and rewrites `NaN` as `null`; the value selects a branch and is never journaled or forwarded as a prop. An absent member is therefore silently falsy, while an undeclared identifier still fails evaluation. Closes #258 --- README.md | 2 +- packages/core/src/expand.ts | 43 +++++++--------- packages/core/tests/if.test.ts | 75 ++++++++++++++++++---------- packages/core/tests/loop.test.ts | 27 ++++++---- packages/testing/tests/smoke.test.ts | 1 + site/routes/docs/control-flow.tsx | 20 ++++++-- smoke-test/Guide/If.md | 15 ++++-- specs/executable-mdx-spec.md | 43 ++++++++++------ 8 files changed, 143 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 2ceeec84..61fe1377 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ executable.md treats the root document like a component: ## Control flow -`` expands one branch and only one. `condition` must be a boolean — there is no truthy or falsy coercion — and the branch that is not selected never expands, so nothing in it imports a component, runs a block, or creates a binding. +`` expands one branch and only one. `condition` selects by ordinary JavaScript truthiness — `false`, `0`, `NaN`, `""`, `null`, and `undefined` take the false branch, and everything else takes the true one, including `"false"`, `[]`, and `{}` — and the branch that is not selected never expands, so nothing in it imports a component, runs a block, or creates a binding. ```md diff --git a/packages/core/src/expand.ts b/packages/core/src/expand.ts index 7fa6ed9d..aac172e7 100644 --- a/packages/core/src/expand.ts +++ b/packages/core/src/expand.ts @@ -1360,11 +1360,12 @@ const IF_PROPS = new Set(["condition"]); * environment, so a `` it creates behaves like inline content and * stays available after ``. * - * It is not an observation boundary either. Errors it creates itself — an - * invalid condition, an unknown prop, a malformed `` — are reported here, - * exactly once. Everything the selected branch returns was already reported - * where it was produced and is handed back untouched, so a `` inside - * a selected branch settles once, exactly as it would inline. + * It is not an observation boundary either. Errors it creates itself — a + * missing condition, a condition expression that fails to evaluate, an unknown + * prop, a malformed `` — are reported here, exactly once. Everything the + * selected branch returns was already reported where it was produced and is + * handed back untouched, so a `` inside a selected branch settles + * once, exactly as it would inline. */ function* expandIf( segment: ComponentElement, @@ -1396,18 +1397,21 @@ function* expandIf( return; } - let condition: Json; + let condition: unknown; if ("condition" in segment.props) { condition = segment.props.condition; } else if ("condition" in segment.expressions) { try { - const resolved = yield* resolveExpressionProps( - {}, - { condition: segment.expressions.condition }, + // Evaluated directly rather than through resolveExpressionProps: that + // helper rejects `undefined` and rewrites `NaN` as `null`, and both are + // conditions truthiness decides. The value selects a branch and is never + // journaled or forwarded as a prop, so it crosses no JSON boundary. + condition = yield* evaluateExpression( + segment.expressions.condition, "If", + "condition", segment.projectedEnv, ); - condition = resolved.condition; } catch (error) { owner.push( yield* raise(ifError(segment, error instanceof Error ? error.message : String(error))), @@ -1415,27 +1419,16 @@ function* expandIf( return; } } else { - owner.push(yield* raise(ifError(segment, ' requires a "condition" prop (a boolean).'))); + owner.push(yield* raise(ifError(segment, ' requires a "condition" prop.'))); return; } - if (typeof condition !== "boolean") { - owner.push( - yield* raise( - ifError( - segment, - `Prop "condition" on must be a boolean, not ${jsonKind(condition)}. ` + - " does not coerce truthy or falsy values.", - ), - ), - ); - return; - } + const selected = !!condition; // The false arm belongs to ``, which is consumed above, so its frame is // added here — otherwise both arms of one `` expand under one path. const branchPath = - condition || structure.elseElement === undefined + selected || structure.elseElement === undefined ? path : extendPath( path, @@ -1446,7 +1439,7 @@ function* expandIf( ); yield* expandSegments( - condition ? structure.whenTrue : structure.whenFalse, + selected ? structure.whenTrue : structure.whenFalse, parentMeta, parentProps, hideSet, diff --git a/packages/core/tests/if.test.ts b/packages/core/tests/if.test.ts index e69c0cb1..1a79d2f1 100644 --- a/packages/core/tests/if.test.ts +++ b/packages/core/tests/if.test.ts @@ -175,32 +175,57 @@ describe("Tier IF — condition validation", () => { expect(run.output).not.toContain("body"); }); - it("IF14: a non-boolean condition is rejected without coercion", function* () { - const cases: Array<[string, string]> = [ - ['x', "a string"], - ["x", "a number"], - ["x", "a number"], - ["x", "null"], - ["x", "an array"], - ["x", "an object"], + it("IF14: every falsy condition selects the branch", function* () { + const cases: Array<[string, unknown]> = [ + ["false", false], + ["0", 0], + ["NaN", NaN], + ['""', ""], + ["null", null], + ["undefined", undefined], ]; - for (const [source, kind] of cases) { - const run = yield* runIf(source); - const message = errorMessages(run.segments)[0] ?? ""; - expect(message).toContain("must be a boolean"); - expect(message).toContain(kind); - expect(run.output).not.toContain("x"); + const selected: string[] = []; + for (const [label, value] of cases) { + const run = yield* runIf("thenelse", { + env: { condition: value }, + }); + selected.push(`${label}: ${run.output}`); + expect(errorMessages(run.segments)).toHaveLength(0); } + expect(selected).toEqual(cases.map(([label]) => `${label}: else`)); }); - it("IF15: a non-boolean expression result is rejected", function* () { - const run = yield* runIf("x", { env: { count: 3 } }); - expect(errorMessages(run.segments)[0]).toContain("must be a boolean, not a number"); + it("IF15: every truthy condition selects the leading branch", function* () { + const cases: Array<[string, unknown]> = [ + ["true", true], + ["1", 1], + ['"false"', "false"], + ['"text"', "text"], + ["[]", []], + ["{}", {}], + ]; + const selected: string[] = []; + for (const [label, value] of cases) { + const run = yield* runIf("thenelse", { + env: { condition: value }, + }); + selected.push(`${label}: ${run.output}`); + expect(errorMessages(run.segments)).toHaveLength(0); + } + expect(selected).toEqual(cases.map(([label]) => `${label}: then`)); }); - it("IF16: an unresolvable condition expression is rejected", function* () { - const run = yield* runIf("x"); - expect(errorMessages(run.segments)[0]).toContain("condition={missing}"); + it("IF16: an absent member is falsy, an undeclared identifier is rejected", function* () { + const misspelled = yield* runIf("yesno", { + env: { review: { approved: true } }, + }); + expect(misspelled.output).toBe("no"); + expect(errorMessages(misspelled.segments)).toHaveLength(0); + + const undeclared = yield* runIf("thenelse"); + expect(errorMessages(undeclared.segments)[0]).toContain("condition={missing}"); + expect(undeclared.output).not.toContain("then"); + expect(undeclared.output).not.toContain("else"); }); it("IF17: unknown props are rejected", function* () { @@ -383,7 +408,7 @@ describe("Tier IF — printed errors carry source positions", () => { }); it("IF36: an origin adds the file path", function* () { - const run = yield* runIf("\nbody", { + const run = yield* runIf("\nbody", { origin: { path: "Doc.md", baseOffset: 40, baseLine: 5 }, }); expect(errorMessages(run.segments)[0]).toContain("(Doc.md:6:1)"); @@ -410,7 +435,7 @@ describe("Tier IF — printed errors carry source positions", () => { return yield* expandSegments([element], {}, {}, new Set()); }); const message = errorMessages(segments)[0] ?? ""; - expect(message).toBe(' requires a "condition" prop (a boolean).'); + expect(message).toBe(' requires a "condition" prop.'); }); }); @@ -773,9 +798,9 @@ describe("Tier IF — error observation", () => { expect(missing.observed).toHaveLength(1); expect(missing.observed[0]).toContain('requires a "condition" prop'); - const nonBoolean = yield* runRaiseProbe("body"); - expect(nonBoolean.observed).toHaveLength(1); - expect(nonBoolean.observed[0]).toContain("must be a boolean"); + const unresolvable = yield* runRaiseProbe("body"); + expect(unresolvable.observed).toHaveLength(1); + expect(unresolvable.observed[0]).toContain("condition={absent}"); const structure = yield* runRaiseProbe('ab'); expect(structure.observed).toHaveLength(1); diff --git a/packages/core/tests/loop.test.ts b/packages/core/tests/loop.test.ts index 9dfaf355..845e06f7 100644 --- a/packages/core/tests/loop.test.ts +++ b/packages/core/tests/loop.test.ts @@ -263,10 +263,11 @@ describe("Tier LOOP — bindings", () => { 'againfirstx', { env: { seen: false } }, ); - // The capture rebinds `seen` to a non-boolean, so the second iteration's - // condition is rejected rather than coerced — bindings really do carry. - expect(run.output).toContain("first"); - expect(errorMessages(run.segments)[0]).toContain("must be a boolean, not a string"); + // The capture rebinds `seen` to a non-empty string, which the second + // iteration reads as truthy — bindings really do carry. + expect(run.output).toBe("firstagain"); + expect(errorMessages(run.segments)).toHaveLength(0); + expect(run.env?.seen).toBe("x"); }); }); @@ -1255,13 +1256,14 @@ describe("Tier LOOP — replay validates the terminal record", () => { "props:", " type: object", " properties:", - " condition: {}", - " required: [condition]", + " fail:", + " type: boolean", + " required: [fail]", " additionalProperties: false", "---", "", "", - "step", + '{item}', "", "", "", @@ -1269,12 +1271,15 @@ describe("Tier LOOP — replay validates the terminal record", () => { "", ].join("\n"); - const cut = yield* completeThenCut(FAILING, { condition: true }); + const cut = yield* completeThenCut(FAILING, { fail: false }); expect(outcomeRecords(cut).map((entry) => entry.outcome)).toEqual(["exhausted"]); - // A non-boolean condition makes the body fail under the documentation - // error mode, so this run derives `error` where the journal holds `exhausted`. - const replayed = yield* resume(FAILING, cut, { condition: 1 }); + // The branch this run selects holds an whose `in` is not an array, + // so the body fails under the documentation error mode and this run derives + // `error` where the journal holds `exhausted`. The failing content performs + // no journaled operation, so the two runs disagree about the terminal + // record and about nothing before it. + const replayed = yield* resume(FAILING, cut, { fail: true }); expect(replayed.failure).toBeInstanceOf(StaleInputError); expect(outcomeRecords(replayed.events).map((entry) => entry.outcome)).toEqual(["exhausted"]); diff --git a/packages/testing/tests/smoke.test.ts b/packages/testing/tests/smoke.test.ts index fa73f971..6b403805 100644 --- a/packages/testing/tests/smoke.test.ts +++ b/packages/testing/tests/smoke.test.ts @@ -73,6 +73,7 @@ const EMBEDDED_TESTS = [ "If selects the leading branch when the condition is true", "If resolves its condition from an existing binding", "If resolves a computed boolean expression", + "If branches on a captured string without converting it first", "Content around the selected branch keeps its order", "A capture from the selected branch stays available afterward", "The unselected branch creates no binding", diff --git a/site/routes/docs/control-flow.tsx b/site/routes/docs/control-flow.tsx index 5b23d24f..66d218be 100644 --- a/site/routes/docs/control-flow.tsx +++ b/site/routes/docs/control-flow.tsx @@ -112,12 +112,26 @@ export default define.page(function ControlFlow() {

Choosing a branch

condition{" "} - is the only prop, and it must be a boolean — there is no truthy or falsy - coercion, so a string, number, array, or null{" "} - is an error rather than a branch. The optional <Else> + is the only prop, and the value it resolves to selects a branch by + ordinary JavaScript truthiness. false, 0,{" "} + NaN, "", null, and{" "} + undefined{" "} + take the false branch; everything else takes the true one — including {" "} + "false", "0", [], and{" "} + {"{}"}, which are JavaScript's familiar edges rather than a + rule <If>{" "} + invents. So a document can branch on the value it already has, as in + {" "} + {""}. The optional{" "} + <Else>{" "} block holds the alternative and is written once, as a direct child.

+

+ A misspelled member resolves to undefined{" "} + and quietly takes the false branch; an undeclared identifier is still an + error. +

{SIMPLE}

diff --git a/smoke-test/Guide/If.md b/smoke-test/Guide/If.md index 69e0524b..f138725a 100644 --- a/smoke-test/Guide/If.md +++ b/smoke-test/Guide/If.md @@ -1,9 +1,12 @@

`` chooses one branch of a document and expands only that branch. The -`condition` prop is a boolean — there is no truthy or falsy coercion — and an -optional `` block, written once as a direct child, holds the alternative. -Without `` a false condition renders nothing. +`condition` prop selects by ordinary JavaScript truthiness — `false`, `0`, +`NaN`, `""`, `null`, and `undefined` take the false branch, everything else +takes the true one, including `"false"`, `[]`, and `{}` — so a document branches +on the value it already has. An optional `` block, written once as a +direct child, holds the alternative; without `` a falsy condition renders +nothing. The unselected branch is not hidden output: it never expands, so nothing in it imports a component, runs a code block, reaches a provider, or creates a @@ -44,6 +47,12 @@ selected branch behaves like inline content and stays available after ``. + +needs a second look +Note: {note}No note + + + before|midalt|after diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index b756d111..8b3ee818 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -3494,9 +3494,22 @@ All checks passed. `condition` is the only accepted prop; any other prop is an error. It resolves in the invocation's evaluation environment — an eval expression (`condition={verdict.passed}`) against the caller/projected env at expansion -time, a JSON literal (`condition={true}`) at scan time — and **must be a -boolean**. A string, number, `null`, array, or object is an error rather than a -truthy or falsy value: `` performs no coercion. +time, a JSON literal (`condition={true}`) at scan time — and the resolved value +then selects a branch by ordinary JavaScript truthiness, `!!value`. A document +branches on the value it already has: `` asks +whether the reviewer wrote anything, with no conversion to a boolean first. + +**Falsy** — `false`, `0`, `NaN`, `""`, `null`, `undefined` — selects the false +branch. Every other value is **truthy**, including `"false"`, `"0"`, `[]`, and +`{}`. Those are JavaScript's familiar edges rather than a rule `` invents: a +non-empty string is truthy whatever it spells, and an empty array or object is a +value rather than an absence. A document that means "no findings" writes +`condition={findings.length === 0}`, not `condition={findings}`. + +An absent member of a declared object resolves to `undefined` and selects the +false branch without an error, so a misspelled `review.aproved` reads as false. +An undeclared root identifier still fails evaluation and is reported as an +``-owned printed error. `` holds the alternative branch. It is optional, accepts no props, takes content, and may appear once as a **direct child** of its ``. A nested @@ -3514,7 +3527,7 @@ region silently folded into the true branch. Like every other structural violation this is decided before the condition is evaluated, so neither branch runs. -A true condition expands the children before ``; a false one expands the +A truthy condition expands the children before ``; a falsy one expands the `` children, or nothing when there is no ``. **Only the selected branch does work.** The other branch is not hidden output: @@ -3535,12 +3548,12 @@ segments that are spliced into the surrounding output, so `ErrorSegment` and exactly as elsewhere. `` is **not an observation boundary**. Under the one-observation rule -(§6.9), it reports only the errors it creates itself — an invalid condition, an -unknown prop, a malformed `` — and hands back the selected branch's -segments untouched, because they were already reported where they were -produced. A failing element inside a selected branch therefore settles exactly -once, as it would inline, and an ambient `throw` error mode still aborts at the -first error. +(§6.9), it reports only the errors it creates itself — a missing `condition`, a +condition expression that fails to evaluate, an unknown prop, a malformed +`` — and hands back the selected branch's segments untouched, because they +were already reported where they were produced. A failing element inside a +selected branch therefore settles exactly once, as it would inline, and an +ambient `throw` error mode still aborts at the first error. Printed errors from `` and `` carry the source location of the element that caused them, as `path:line:column` when the element came from a file and @@ -6904,9 +6917,9 @@ Identifiers match `packages/core/tests/if.test.ts` one to one. | IF11 | Nested `` in the unselected branch | It never runs | | IF12 | Self-closing `` | Renders nothing, with no error | | IF13 | Missing `condition` | Rejected; the body does not render | -| IF14 | No coercion | String, number (including `0`/`1`), `null`, array, and object are rejected | -| IF15 | Non-boolean expression result | A numeric binding is rejected with its kind named | -| IF16 | Unresolvable expression | The failing expression is quoted in the printed error | +| IF14 | Falsy conditions | `false`, `0`, `NaN`, `""`, `null`, and `undefined` each select ``, with no error | +| IF15 | Truthy conditions | `true`, `1`, `"false"`, `"text"`, `[]`, and `{}` each select the leading branch, with no error | +| IF16 | Absent member versus undeclared identifier | A misspelled member is falsy and silent; an undeclared identifier is quoted in the printed error | | IF17 | Unknown props | Literal and expression props other than `condition` are rejected | | IF18 | `` outside `` | Diagnosed; no component named `Else` is imported | | IF19 | Duplicate `` | A second `` is rejected | @@ -6942,7 +6955,7 @@ Identifiers match `packages/core/tests/if.test.ts` one to one. | IF49 | Inline observation baseline | An `ErrorSegment` outside any `` passes through `Component.raise` once | | IF50 | Selected branch observed once | The same error inside a selected branch is observed once, not twice | | IF51 | Unselected branch unobserved | An error in the unselected branch is observed zero times | -| IF52 | ``-owned errors observed once | Missing/non-boolean `condition` and a malformed `` each report once | +| IF52 | ``-owned errors observed once | A missing `condition`, an unresolvable condition expression, and a malformed `` each report once | | IF53 | Throwing error mode | An ambient `throw` error mode still aborts on a selected-branch error | | IF54 | Provider boundary | An unselected branch makes zero Sample Api calls; the same probe records one when selected | @@ -6997,7 +7010,7 @@ Identifiers match `packages/core/tests/loop.test.ts` one to one. | LOOP10 | Bindings carry forward | An iteration reads what an earlier one bound | | LOOP11 | Bindings survive the loop | The final value is readable after `` | | LOOP12 | Last-iteration binding | A binding made in the final iteration survives | -| LOOP13 | Body reads the shared env | An `` in the body sees a binding an earlier iteration changed | +| LOOP13 | Body reads the shared env | An `` in the body reads the string an earlier iteration bound as truthy | | LOOP14 | Missing `max` | Rejected; the body does not render | | LOOP15 | Non-positive and fractional bounds | `0`, `-1`, and `1.5` are rejected | | LOOP16 | No coercion | String, boolean, `null`, array, and object bounds are rejected with their kind named | From 13903fe9352cd8ce7320fe5920c849b36dc3645f Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:32:29 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20State=20the=20whole=20falsy?= =?UTF-8?q?=20set=20and=20the=20condition's=20freedom=20from=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `-0` and `0n` are falsy too, and IF14 now binds both: `0n` is what discriminates raw evaluation from the prop path, because JSON.stringify throws on a BigInt rather than rewriting it. The reason for evaluating a condition directly is general, not a list of two exceptions. A condition is decided and discarded rather than passed on or recorded, so it takes any JavaScript value. --- README.md | 2 +- packages/core/src/expand.ts | 7 ++++--- packages/core/tests/if.test.ts | 2 ++ site/routes/docs/control-flow.tsx | 5 +++-- smoke-test/Guide/If.md | 12 ++++++------ specs/executable-mdx-spec.md | 22 +++++++++++++++------- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 61fe1377..dee889f9 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ executable.md treats the root document like a component: ## Control flow -`` expands one branch and only one. `condition` selects by ordinary JavaScript truthiness — `false`, `0`, `NaN`, `""`, `null`, and `undefined` take the false branch, and everything else takes the true one, including `"false"`, `[]`, and `{}` — and the branch that is not selected never expands, so nothing in it imports a component, runs a block, or creates a binding. +`` expands one branch and only one. `condition` selects by ordinary JavaScript truthiness — `false`, `0`, `-0`, `0n`, `NaN`, `""`, `null`, and `undefined` take the false branch, and everything else takes the true one, including `"false"`, `[]`, and `{}` — and the branch that is not selected never expands, so nothing in it imports a component, runs a block, or creates a binding. ```md diff --git a/packages/core/src/expand.ts b/packages/core/src/expand.ts index aac172e7..fe88dc80 100644 --- a/packages/core/src/expand.ts +++ b/packages/core/src/expand.ts @@ -1403,9 +1403,10 @@ function* expandIf( } else if ("condition" in segment.expressions) { try { // Evaluated directly rather than through resolveExpressionProps: that - // helper rejects `undefined` and rewrites `NaN` as `null`, and both are - // conditions truthiness decides. The value selects a branch and is never - // journaled or forwarded as a prop, so it crosses no JSON boundary. + // helper normalizes its result through JSON, which rejects `undefined`, + // rewrites `NaN` as `null`, and throws on a BigInt. A condition is + // decided and discarded rather than passed on or recorded, so it takes + // any JavaScript value and crosses no serialization boundary. condition = yield* evaluateExpression( segment.expressions.condition, "If", diff --git a/packages/core/tests/if.test.ts b/packages/core/tests/if.test.ts index 1a79d2f1..2c6130a2 100644 --- a/packages/core/tests/if.test.ts +++ b/packages/core/tests/if.test.ts @@ -179,6 +179,8 @@ describe("Tier IF — condition validation", () => { const cases: Array<[string, unknown]> = [ ["false", false], ["0", 0], + ["-0", -0], + ["0n", 0n], ["NaN", NaN], ['""', ""], ["null", null], diff --git a/site/routes/docs/control-flow.tsx b/site/routes/docs/control-flow.tsx index 66d218be..5b7c9eb1 100644 --- a/site/routes/docs/control-flow.tsx +++ b/site/routes/docs/control-flow.tsx @@ -114,8 +114,9 @@ export default define.page(function ControlFlow() { condition{" "} is the only prop, and the value it resolves to selects a branch by ordinary JavaScript truthiness. false, 0,{" "} - NaN, "", null, and{" "} - undefined{" "} + -0, 0n, NaN, "", + {" "} + null, and undefined{" "} take the false branch; everything else takes the true one — including {" "} "false", "0", [], and{" "} diff --git a/smoke-test/Guide/If.md b/smoke-test/Guide/If.md index f138725a..11133264 100644 --- a/smoke-test/Guide/If.md +++ b/smoke-test/Guide/If.md @@ -1,12 +1,12 @@
`` chooses one branch of a document and expands only that branch. The -`condition` prop selects by ordinary JavaScript truthiness — `false`, `0`, -`NaN`, `""`, `null`, and `undefined` take the false branch, everything else -takes the true one, including `"false"`, `[]`, and `{}` — so a document branches -on the value it already has. An optional `` block, written once as a -direct child, holds the alternative; without `` a falsy condition renders -nothing. +`condition` prop selects by ordinary JavaScript truthiness — `false`, `0`, `-0`, +`0n`, `NaN`, `""`, `null`, and `undefined` take the false branch, everything +else takes the true one, including `"false"`, `[]`, and `{}` — so a document +branches on the value it already has. An optional `` block, written once +as a direct child, holds the alternative; without `` a falsy condition +renders nothing. The unselected branch is not hidden output: it never expands, so nothing in it imports a component, runs a code block, reaches a provider, or creates a diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 8b3ee818..69eeec40 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -3499,18 +3499,26 @@ then selects a branch by ordinary JavaScript truthiness, `!!value`. A document branches on the value it already has: `` asks whether the reviewer wrote anything, with no conversion to a boolean first. -**Falsy** — `false`, `0`, `NaN`, `""`, `null`, `undefined` — selects the false -branch. Every other value is **truthy**, including `"false"`, `"0"`, `[]`, and -`{}`. Those are JavaScript's familiar edges rather than a rule `` invents: a -non-empty string is truthy whatever it spells, and an empty array or object is a -value rather than an absence. A document that means "no findings" writes -`condition={findings.length === 0}`, not `condition={findings}`. +**Falsy** — `false`, `0`, `-0`, `0n`, `NaN`, `""`, `null`, `undefined` — selects +the false branch. Every other value is **truthy**, including `"false"`, `"0"`, +`[]`, and `{}`. Those are JavaScript's familiar edges rather than a rule `` +invents: a non-empty string is truthy whatever it spells, and an empty array or +object is a value rather than an absence. A document that means "no findings" +writes `condition={findings.length === 0}`, not `condition={findings}`. An absent member of a declared object resolves to `undefined` and selects the false branch without an error, so a misspelled `review.aproved` reads as false. An undeclared root identifier still fails evaluation and is reported as an ``-owned printed error. +**The condition is not restricted to JSON.** An expression prop must be +JSON-serializable because its value is passed to a component and recorded; a +condition is neither. `` takes whatever the expression evaluates to — a +`BigInt`, a `Symbol`, a function, a class instance, `undefined`, `NaN`, `-0` — +decides one branch with it, and discards it. The value is never interpolated, +journaled, or forwarded, so it crosses no serialization boundary and no +serialization rule constrains it. + `` holds the alternative branch. It is optional, accepts no props, takes content, and may appear once as a **direct child** of its ``. A nested `` owns the `` elements beneath it. An `` written anywhere else @@ -6917,7 +6925,7 @@ Identifiers match `packages/core/tests/if.test.ts` one to one. | IF11 | Nested `` in the unselected branch | It never runs | | IF12 | Self-closing `` | Renders nothing, with no error | | IF13 | Missing `condition` | Rejected; the body does not render | -| IF14 | Falsy conditions | `false`, `0`, `NaN`, `""`, `null`, and `undefined` each select ``, with no error | +| IF14 | Falsy conditions | `false`, `0`, `-0`, `0n`, `NaN`, `""`, `null`, and `undefined` each select ``, with no error | | IF15 | Truthy conditions | `true`, `1`, `"false"`, `"text"`, `[]`, and `{}` each select the leading branch, with no error | | IF16 | Absent member versus undeclared identifier | A misspelled member is falsy and silent; an undeclared identifier is quoted in the printed error | | IF17 | Unknown props | Literal and expression props other than `condition` are rejected |