@ryhrm-gz/xincodo-lib
Version:
Utilities for working with Xincodo body documents.
252 lines (189 loc) • 6.25 kB
Markdown
---
name: editing-preview-workflow
description: >
Load when implementing management-screen editing and preview updates with
walkBody, findBlockPathById, TraversalPath, getBlockAtPath,
insertBlockAtRoot, insertBlockAtPath, updateBlockAtPath,
replaceBlockAtPath, removeBlockAtPath, moveBlock, updateRichTextAtPath,
mapBody, or BodyEditError.
type: lifecycle
library: "@ryhrm-gz/xincodo-lib"
library_version: "0.1.0"
requires:
- building-and-parsing-body
- validating-body
sources:
- "ryhrm-gz/xincodo-lib:README.md"
- "ryhrm-gz/xincodo-lib:src/editing.ts"
- "ryhrm-gz/xincodo-lib:src/traversal.ts"
- "ryhrm-gz/xincodo-lib:tests/editing.test.ts"
- "ryhrm-gz/xincodo-lib:tests/traversal.test.ts"
---
# Editing Preview Workflow
This skill builds on `building-and-parsing-body` and `validating-body`. Use
stable ids to locate blocks, resolve a fresh `TraversalPath` immediately
before editing, and use immutable helpers to produce the next `Body`.
## Setup
```ts
import { findBlockPathById, insertBlockAtPath, paragraph, type Body } from "@ryhrm-gz/xincodo-lib";
export function insertNoteAfterIntro(body: Body): Body {
const introPath = findBlockPathById(body, "intro");
if (!introPath) {
return body;
}
return insertBlockAtPath(body, introPath, paragraph("Preview note"), {
position: "after",
});
}
```
## Core Patterns
### Resolve a fresh path from a stable id
```ts
import { findBlockPathById, getBlockAtPath } from "@ryhrm-gz/xincodo-lib";
const path = findBlockPathById(body, "summary");
const block = path ? getBlockAtPath(body, path) : undefined;
```
Paths are position-based. Recompute them from stable ids after edits that may
move or insert blocks.
### Replace or update a block immutably
```ts
import { heading, replaceBlockAtPath, type Body, type TraversalPath } from "@ryhrm-gz/xincodo-lib";
export function promoteIntro(body: Body, path: TraversalPath) {
return replaceBlockAtPath(body, path, heading(2, "Introduction", { id: "intro" }));
}
```
The returned `Body` is the value to store in editor state.
### Update rich text from traversal context
```ts
import {
richText,
updateRichTextAtPath,
walkBody,
type Body,
type TraversalPath,
} from "@ryhrm-gz/xincodo-lib";
export function renameIntro(body: Body): Body {
let richTextPath: TraversalPath | undefined;
walkBody(body, {
richText(_, context) {
if (
"type" in context.parent &&
context.parent.type === "text" &&
context.parent.id === "intro"
) {
richTextPath = context.path;
}
},
});
return richTextPath
? updateRichTextAtPath(body, richTextPath, () => richText("Updated intro"))
: body;
}
```
`updateRichTextAtPath` needs a path ending at `richText`, `caption`, or
`title`, not just the block path.
### Move blocks with descendant safety
```ts
import { findBlockPathById, moveBlock, type Body } from "@ryhrm-gz/xincodo-lib";
export function moveIntroAfterSummary(body: Body): Body {
const introPath = findBlockPathById(body, "intro");
const summaryPath = findBlockPathById(body, "summary");
if (!introPath || !summaryPath) {
return body;
}
return moveBlock(body, introPath, summaryPath, { position: "after" });
}
```
`moveBlock` throws `BodyEditError` if the target is inside the moved block.
## Common Mistakes
### HIGH Mutating nested content directly
Wrong:
```ts
import { paragraph, type Body } from "@ryhrm-gz/xincodo-lib";
function addNote(body: Body): Body {
const first = body.content[0];
if (first?.type === "text") {
first.children?.push(paragraph("Inserted"));
}
return body;
}
```
Correct:
```ts
import { findBlockPathById, insertBlockAtPath, paragraph, type Body } from "@ryhrm-gz/xincodo-lib";
function addNote(body: Body): Body {
const path = findBlockPathById(body, "intro");
return path ? insertBlockAtPath(body, path, paragraph("Inserted")) : body;
}
```
Editing helpers return new `Body` values so preview state can update
predictably.
Source: `README.md`; `tests/editing.test.ts`; `tests/traversal.test.ts`
### HIGH Passing a block path to rich text updater
Wrong:
```ts
import {
findBlockPathById,
richText,
updateRichTextAtPath,
type Body,
} from "@ryhrm-gz/xincodo-lib";
function updateIntro(body: Body): Body {
const path = findBlockPathById(body, "intro");
return path ? updateRichTextAtPath(body, path, () => richText("Updated")) : body;
}
```
Correct:
```ts
import {
richText,
updateRichTextAtPath,
walkBody,
type Body,
type TraversalPath,
} from "@ryhrm-gz/xincodo-lib";
function updateIntro(body: Body): Body {
let richTextPath: TraversalPath | undefined;
walkBody(body, {
richText(_, context) {
if (
"type" in context.parent &&
context.parent.type === "text" &&
context.parent.id === "intro"
) {
richTextPath = context.path;
}
},
});
return richTextPath ? updateRichTextAtPath(body, richTextPath, () => richText("Updated")) : body;
}
```
Rich text updates require a field path, usually captured from `walkBody`.
Source: `src/editing.ts`; `tests/editing.test.ts`
### MEDIUM Moving a block into its descendant
Wrong:
```ts
import { findBlockPathById, moveBlock, type Body } from "@ryhrm-gz/xincodo-lib";
function moveParentIntoChild(body: Body): Body {
return moveBlock(body, findBlockPathById(body, "parent")!, findBlockPathById(body, "child")!);
}
```
Correct:
```ts
import { findBlockPathById, moveBlock, type Body } from "@ryhrm-gz/xincodo-lib";
function moveParentAfterSibling(body: Body): Body {
const parentPath = findBlockPathById(body, "parent");
const siblingPath = findBlockPathById(body, "sibling");
return parentPath && siblingPath
? moveBlock(body, parentPath, siblingPath, { position: "after" })
: body;
}
```
`moveBlock` prevents moving a block relative to its own descendant because
that would create an invalid nesting operation.
Source: `src/editing.ts`; `tests/editing.test.ts`
## References
- [Traversal paths](references/traversal-paths.md)
- [Rich text field paths](references/rich-text-field-paths.md)
See also: `validating-body/SKILL.md` — edited preview state should be linted
before save or publish.