Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -3540,6 +3540,7 @@ Status is measured against main.
| document validation | validates one supplied root projection and the recursive Markdown source closure normal component selection discovers, returning deterministic version-1 document diagnostics and `valid`, `invalid` or `not-statically-checkable` invocation outcomes without evaluating document code or installing operational host behavior | built on the #654 stack |
| `<PrintErrors>` / `printErrors(fn)` | prints failures | built on main |
| `<Let as="name">` | binds one name in the current environment from exactly one source: the content it renders, or the exact value `value` names, bound by reference and never through the JSON boundary component props cross — the scanner resolves no JSON for that one prop, and expansion projects none. Which source it has is read from what the author wrote, before either one runs, so a construct naming both expands no child and evaluates no expression. It opens no scope, owns no resource, adds no middleware boundary and writes no journal record — replay reconstructs both sources through ordinary expansion | built on the #527 stack |
| `<CodeBlock value={…} />` | shows any supplied string inside one fenced Markdown code block, choosing a fence one backtick longer than the value's longest backtick run and never shorter than three, so the value cannot close it. An ordinary overridable text default: `value` and the optional single-token `language` are ordinary props on the closed schema, so a repository `CodeBlock.md` receives them the way it receives any other prop. Self-closing only, declared to canonical dispatch rather than decided in the body, so a paired spelling is refused before the body runs and its content never expands; prop validation refuses a missing or non-string `value`, a refused `language` and any unknown prop ahead of both. The value is returned unchanged — nothing trimmed, normalized, escaped or removed — and the framing line feeds belong to the envelope, with no line feed after the closing fence. Exactness is a promise at the function-component return and the `as` capture boundary; the `DocumentOutput` middleware contract governs emitted output after it. It owns no scope, resource, authority or durable effect of its own, so partial and completed replay are the ordinary ones | built on the #658 stack |
| `<Json value={…} />` | renders one supplied value as JSON text where the element was written, from one native two-space `JSON.stringify` call. An ordinary overridable core default whose operand is a capture: the exact evaluation result arrives by reference and is never mutated, cloned, replaced or frozen. It binds nothing, and `as`, content and a missing `value` are all refused before the operand evaluates. A value with no JSON text and a serialization that threw are distinct failures, each positioned at the invocation, emitting no partial output and preserving the original error as its cause. No scope, resource, authority or JSON-specific durable effect: replay reaches it through ordinary expansion, and a surrounding `<Prompt>` or `<File>` keeps its own record of the text it consumed | built on the #452 stack |
| root failure settlement | a text root installs the fail-capable `output` mode for its whole body, so an uncaught, undecided failure is the document execution's own outcome, whether or not the root declares `<Output>` | built on the #453 stack |
| `<Output>` region `output` mode | an undecided error fails the document execution | built on main |
Expand Down
1 change: 1 addition & 0 deletions packages/cli/tests/document-suites/syntax/Syntax.test.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,5 @@ rows read.
<AssertEquals actual={builtInNames.includes("AssertEquals")} expected={true} />
<AssertEquals actual={builtInNames.includes("WebForm")} expected={true} />
<AssertEquals actual={builtInNames.includes("TempDir")} expected={true} />
<AssertEquals actual={builtInNames.includes("CodeBlock")} expected={true} />
</Test>
120 changes: 120 additions & 0 deletions packages/core/src/components/CodeBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* `<CodeBlock>` — place any text inside one fenced Markdown code block
* (specs/executable-mdx-spec.md §6.19).
*
* A document that shows generated source, a diagnostic, or anything else it did
* not write needs the text to arrive as text. The hazard is the fence itself: a
* value that happens to contain three backticks closes a three-backtick block
* early, and the rest of it lands in the document as Markdown — as headings, as
* component invocations, as another fence. Writing that arithmetic in an eval
* block puts the safety of the surrounding document in the hands of every
* author who needs to quote something.
*
* So the fence is chosen here, from the value, and it is the only thing this
* component decides. It scans for the longest run of backticks and opens with
* one more than that, never fewer than three, which is a fence the value cannot
* close no matter what it holds.
*
* ## The value is not read for anything else
*
* Nothing is trimmed, normalized, escaped, re-encoded or removed. Text that
* looks like a fence, an element, an interpolation, HTML or an executable code
* block stays exactly as it arrived, because a function component's return is
* rendered rather than rescanned as document source (§6.8). The two line feeds
* that frame the value belong to the envelope, not to the value, and there is
* no line feed after the closing fence: a document that wants one writes it.
*
* ## `value` is an ordinary prop
*
* Unlike `<Json>` (§6.12), whose operand must arrive by reference or lose the
* very failures it exists to report, a string crosses the component JSON
* boundary as itself. Keeping `value` on the ordinary boundary is what lets a
* repository `CodeBlock.md` receive it the way it receives every other prop,
* instead of inheriting a capture from core's registration that it never
* declared.
*
* `language` is closed to one token — the info string a Markdown reader expects
* — so that a value cannot reach the opening fence line, where it would be read
* as syntax rather than shown as text.
*/

import type { Operation } from "effection";
import { printErrors } from "../component-failures.ts";
import type { FormDeclaration, InvocationForm } from "../invocation-identity.ts";
import type { Json } from "../types.ts";

export const props = {
type: "object",
properties: {
value: { type: "string" },
language: {
type: "string",
pattern: "^[A-Za-z0-9][A-Za-z0-9._+#-]*$",
},
},
required: ["value"],
additionalProperties: false,
};

/** An invocation `<CodeBlock>` cannot render. */
export class CodeBlockError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "CodeBlockError";
}
}

const PAIRED =
"<CodeBlock> shows the text it is given, not content: write <CodeBlock value={…} /> instead.";

const UNESTABLISHED =
"<CodeBlock value={…} /> was called without the invocation the engine issued, so which " +
"form it was written as cannot be established.";

/**
* The fence this value cannot close.
*
* Scanned rather than matched: a regular expression over an arbitrary value is
* one more thing between the text and the count, and the count is the whole
* contract. Every character other than U+0060 ends a run and is otherwise not
* looked at.
*/
function fenceFor(value: string): string {
let longest = 0;
let run = 0;
for (let index = 0; index < value.length; index += 1) {
if (value[index] === "`") {
run += 1;
if (run > longest) {
longest = run;
}
} else {
run = 0;
}
}
return "`".repeat(Math.max(3, longest + 1));
}

const show = printErrors(
// deno-lint-ignore require-yield
function* CodeBlock(props: Record<string, Json>): Operation<string> {
const value = String(props.value);
const language = props.language === undefined ? "" : String(props.language);
const fence = fenceFor(value);
return `${fence}${language}\n${value}\n${fence}`;
},
);

/**
* The one form this component runs, and what it says about the other.
*
* Declared rather than decided in the body: canonical dispatch reads the shape
* the author wrote (§5.6), so what runs, what the catalog advertises and what a
* refusal says all come from this one value.
*/
export const form: FormDeclaration = {
forms: "self-closing",
fn: show,
refuse: (_props: Record<string, Json>, written: InvocationForm | undefined) =>
new CodeBlockError(written === "paired" ? PAIRED : UNESTABLISHED),
};
9 changes: 9 additions & 0 deletions packages/core/src/components/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import type { ComponentRegistry, RegistryEntry } from "../types.ts";
import { form as codeBlockForm, props as codeBlockProps } from "./CodeBlock.ts";
import Elicit, { props as elicitProps, returns as elicitReturns } from "./Elicit.ts";
import TempDir, { props as tempDirProps } from "./TempDir.ts";
import Fetch, { props as fetchProps } from "./Fetch.ts";
Expand Down Expand Up @@ -169,6 +170,14 @@ export const CORE_REGISTRY: ComponentRegistry = new Map<string, RegistryEntry>([
},
{ returns: parseJsonObject(globReturns) },
),
core("CodeBlock", codeBlockForm, parseJsonObject(codeBlockProps), {
description:
"Show arbitrary text as a fenced Markdown code block. " +
'`<CodeBlock value={source} language="markdown" />` chooses a fence the value cannot ' +
"close.",
as: "Optional. Captures the exact fenced Markdown instead of emitting it.",
context: null,
}),
core(
"Json",
Json,
Expand Down
Loading
Loading