import { z } from "zod";
import type { BlockMdxConfig } from "../types.js";
/**
* Pure (React-free) part of the PLAN-SPECIFIC diff block: its data schema and MDX
* round-trip config. Shared by the server MDX adapter (`plan-mdx.ts` via
* `plan-block-registry.ts`) and the client spec (`planBlocks.tsx`). Keeping this
* React-free means importing it into a server module never pulls React (or the
* `diff` line-differ used only by the renderer) into the Nitro/SSR bundle.
*
* The schema MUST stay data-compatible with the `diff` branch of `planBlockSchema`
* (`plan-content.ts`), and the MDX `tag` + flat attribute shape MUST match the
* `` self-closing encoding so stored
* `.mdx` round-trips. The `before`/`after` source lives in ATTRIBUTES (not MDX
* children) — the shared `prop()` encoder round-trips multiline strings cleanly,
* and keeping them attributes avoids the code being reflowed as prose.
*/
/** Rendering layout for the diff body. */
export type DiffMode = "unified" | "split";
/**
* One line-anchored note attached to a diff, mirroring the `annotated-code`
* annotation shape but adding `side`. The `lines` ref is 1-based against the
* chosen side's source: `side: "after"` (the default) targets the new file's
* line numbers, `side: "before"` the old file's. Optional ⇒ a diff without
* annotations renders exactly as before.
*/
export interface DiffAnnotation {
/** Which side the line ref targets; defaults to "after". */
side?: "before" | "after";
/** 1-based line ref against that side's text: "13" or "13-15" (inclusive). */
lines: string;
/** Optional short label shown before the note (e.g. "Validation"). */
label?: string;
/** The note prose (markdown), rendered through `ctx.renderMarkdown`. */
note: string;
}
export interface DiffData {
/** Optional file path shown in the header (e.g. `src/add.ts`). */
filename?: string;
/** Optional language label rendered as a chip (e.g. `ts`). Purely cosmetic. */
language?: string;
/** Original ("before") source. */
before: string;
/** New ("after") source. */
after: string;
/** Layout: split (default, side-by-side) or unified (one column). */
mode?: DiffMode;
/** Line-anchored notes over the before/after sides. */
annotations?: DiffAnnotation[];
}
/**
* A 1-based line reference: `"3"` or `"3-5"` (inclusive). Whitespace tolerant.
* Matches the `annotated-code` line-ref schema so both blocks validate refs
* identically.
*/
const lineRefSchema = z
.string()
.trim()
.regex(/^\d+(\s*-\s*\d+)?$/, {
message: 'lines must be a 1-based line ref like "3" or "3-5"',
})
.max(40);
const diffAnnotationSchema = z.object({
side: z.enum(["before", "after"]).optional(),
lines: lineRefSchema,
label: z.string().trim().max(160).optional(),
note: z.string().trim().min(1).max(4_000),
}) as z.ZodType;
export const diffSchema = z.object({
filename: z.string().trim().max(400).optional(),
language: z.string().trim().max(40).optional(),
before: z.string().max(100_000),
after: z.string().max(100_000),
mode: z.enum(["unified", "split"]).optional(),
annotations: z.array(diffAnnotationSchema).max(80).optional(),
}) as unknown as z.ZodType;
/**
* MDX config: `filename`, `language`, `mode`, `before`, `after`, and
* `annotations` are flat attributes — the
* `` self-closing
* form. Insertion order of `toAttrs` is the on-disk attribute order. `before`/
* `after` are multiline string attributes (round-trip through the shared `prop()`
* encoder); `annotations` is a JSON array attribute, encoded the same way as the
* `annotated-code` block. `fromAttrs` mirrors a forgiving parse (`before ?? ""`,
* `after ?? ""`, optional `filename`/`language`/`mode`/`annotations` undefined
* when absent) so a plan missing an attribute still parses without re-emitting
* unauthored empty arrays.
*/
export const diffMdx: BlockMdxConfig = {
tag: "Diff",
toAttrs: (data) => ({
filename: data.filename,
language: data.language,
mode: data.mode,
before: data.before,
after: data.after,
annotations: data.annotations,
}),
fromAttrs: (attrs) => ({
filename: attrs.string("filename"),
language: attrs.string("language"),
mode: attrs.string("mode") as DiffMode | undefined,
before: attrs.string("before") ?? "",
after: attrs.string("after") ?? "",
annotations: attrs.array("annotations"),
}),
};