Skip to content

✨ Show arbitrary text as a fenced Markdown code block (#658) - #661

Merged
taras merged 1 commit into
mainfrom
agent/issue-658-code-block
Aug 30, 2026
Merged

✨ Show arbitrary text as a fenced Markdown code block (#658)#661
taras merged 1 commit into
mainfrom
agent/issue-658-code-block

Conversation

@taras

@taras taras commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Closes #658. Parallel prerequisite of #260.

Why

A document that renders text it did not write — generated candidate source, a
validator's diagnostics, a command transcript — needs that text to arrive as
text. The hazard is the fence: a value holding three backticks closes a
three-backtick block early, and everything after it lands in the document as
Markdown, as headings, as element invocations, as another fence. Today every
author who has something to quote writes that delimiter arithmetic themselves in
an eval block.

What changes

Before: showing arbitrary text safely meant computing a fence in an eval block
at every site that needed one.

After: <CodeBlock value={source} language="markdown" /> is a core default that
does it, and does nothing else to the value.

How it works

<CodeBlock value={…} /> → canonical self-closing dispatch → scan for the longest
backtick run → max(3, longest + 1) backticks → fence + language + "\n" + value +
"\n" + fence

The whole value is scanned for the longest contiguous run of U+0060, and the
fence is one backtick longer than that, never shorter than three — a fence the
value cannot close, whatever it holds. Both framing characters are U+000A and
belong to the envelope rather than to the value, and there is no line feed after
the closing fence.

Nothing else about the value is read. It is not trimmed, line-ending-normalized,
escaped, re-encoded, or searched for an apparent closing fence.

Review guide

Start with: packages/core/src/components/CodeBlock.ts

Then review:

  1. The closed props schema and the self-closing FormDeclaration
  2. fenceFor() and the returned string
  3. The registration in packages/core/src/components/registry.ts
  4. packages/core/tests/code-block-component.test.ts, then the registration,
    catalog and CLI-syntax additions

Look carefully at:

  • The language character class. The hyphen is last so it stays literal; a
    hyphen that became a range would admit /, :, , and ; into the opening
    fence line.

What must stay true

  • The fence is one longer than the longest backtick run, never under three
    enforced by fenceFor() and checked by CB2's table of complete fence lengths.
  • The value crosses unchanged — enforced by returning it without any
    rewriting, and checked by CB3, which slices the known envelope off and
    compares code unit by code unit.
  • Only the self-closing form runs — enforced by declaring forms: "self-closing" to canonical dispatch rather than reading
    Component.hasContent(), and checked by CB5's paired cases, which count a
    <Tripwire /> written inside the refused content.
  • value is an ordinary prop, not a capture — enforced by leaving it on the
    schema, and checked by CR22d, where a repository CodeBlock.md receives the
    string through the ordinary boundary.
  • Exactness is claimed at the component return and the as capture, not
    after output middleware
    — every byte assertion in Tier CB reads one of those
    two boundaries, and no case installs useNormalizedOutput().

How to verify it

  • CB1 proves the empty value and ordinary Markdown render with a
    three-backtick fence, the two framing line feeds and no final one; fails on an
    extra newline, a trim, a default language or a wrong minimum.
  • CB2 proves the fence length across no run, sub-three runs, a three-run,
    several unequal runs and a long run; fails on off-by-one, first-run-only or a
    fixed fence.
  • CB3 proves a round trip through leading and trailing spaces, blank lines,
    tabs, \r and embedded runs; fails on any normalization.
  • CB4 proves the accepted token table renders bare on the fence line and
    every refused token fails validation with no fence; fails on a widened or
    narrowed pattern and on partial output after a refusal.
  • CB5 proves a missing or non-string value, an unknown prop and both
    paired spellings are refused with no fence and zero tripwire calls.
  • CB6 proves candidate-like content stays inert inside a safe fence and that
    as binds the byte-identical string while emitting nothing at the site.
  • CB7 proves no CodeBlock-specific durable event exists, that a partial
    replay re-renders from the newly reconstructed string, and that a completed
    root reuses its result without consulting the live source.
  • CR16b/CR17/CR22d prove the name is an ordinary overridable default.
  • SY24b proves the catalog reports the whole contract without running it,
    and the CLI Syntax.test.md suite proves the rendered production catalog
    names it across the subprocess boundary.

Four mutations were run against the committed tests and each was caught: max(3, longest), a .trim() on the value, forms: "either", and a language class
whose hyphen becomes a range.

Focused commands, all green at 1c6fbb2a:

deno task test packages/core/tests/code-block-component.test.ts
deno task test packages/core/tests/component-registration.test.ts
deno task test packages/core/tests/syntax-catalog.test.ts
deno task test packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts

Delivery battery, all green:

deno task lint       # 0 errors
deno task check      # no errors
deno task check:jsr  # Success Dry run complete

Scope

Included

  • <CodeBlock>: the pure renderer, the closed props schema, the self-closing
    form declaration and its paired refusal.
  • Core registration with the accepted description and explicit as/context
    metadata.
  • Tier CB, the resolution and catalog regressions, and the CLI syntax suite.
  • Specification §6.19, the core-supplies list and one architecture inventory
    row.

Intentionally unchanged

Risks and limitations

  • Nothing promises that normalized or ANSI terminal output preserves the
    component's bytes. Exactness is a promise at the component return and the as
    capture boundary, and the specification says so.
  • Recovery or rollback: revert the single commit. The component is additive and
    nothing else resolves the name.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Add `<CodeBlock>`, a core default that places any supplied string inside one
fenced Markdown code block and chooses a fence the value cannot close: one
backtick longer than the value's longest backtick run, never shorter than
three. The value is returned unchanged — nothing trimmed, normalized, escaped
or removed — so generated source and diagnostics can be shown as inert text
without writing delimiter arithmetic in an eval block.

`value` and the optional single-token `language` are ordinary props on a closed
schema, so a repository `CodeBlock.md` receives them the way it receives any
other prop. The self-closing form is 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.
@github-actions

Copy link
Copy Markdown

PR #661: ✨ Show arbitrary text as a fenced Markdown code block (#658)

8 files, +732 / -3

Scope

🟡 735 lines changed. PRs under 400 receive more thorough review.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 3 diagnostics across 1 file (2 rules)
Density: 0.004 violations/added-line

no-base-to-string (2): packages/core/src/components/CodeBlock.ts
no-shadow (1): packages/core/src/components/CodeBlock.ts

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 30, 2026 14:57
@taras
taras merged commit 879c4ff into main Aug 30, 2026
30 checks passed
@taras
taras deleted the agent/issue-658-code-block branch August 30, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Render arbitrary text safely with <CodeBlock>

1 participant