
























@@ -1,15 +1,10 @@
11/**
2- * Mutate a `MdAst` at an OcPath. Returns a new AST with the
3- * value replaced; the original is unchanged.
2+ * Mutate `MdAst` at an OcPath. Returns a new AST; original unchanged.
43 *
5- * Writable surface:
4+ * oc://FILE/[frontmatter]/key → frontmatter value
5+ * oc://FILE/section/item/field → item.kv.value
66 *
7- * oc://FILE/[frontmatter]/key → frontmatter entry value
8- * oc://FILE/section/item/field → item.kv.value (when item has kv shape)
9- *
10- * Section bodies, tables, and code blocks are NOT writable through
11- * this primitive — they're prose, and a generic "set" doesn't compose
12- * cleanly. Doctor fixers handle structural edits via dedicated verbs.
7+ * Section bodies aren't writable through this primitive.
138 *
149 * @module @openclaw/oc-path/edit
1510 */
@@ -25,74 +20,44 @@ export type MdEditResult =
2520readonly reason: "unresolved" | "not-writable" | "no-item-kv";
2621};
272228-/**
29- * Replace the value at `path` with `newValue`. The new AST has fresh
30- * `raw` re-rendered from the structural fields.
31- *
32- * Sentinel guard at the substrate boundary — `setJsoncOcPath` and the
33- * jsonl `finalize`-via-render path both reject sentinel-bearing values
34- * before they reach the AST. The md path was deferring entirely to
35- * round-trip echo through `emitMd`, which `acceptPreExistingSentinel`
36- * by default skips. Closing the gap here keeps F9 (formatter sentinel
37- * guard) symmetric across all three kinds.
38- */
23+// Sentinel guard at the boundary keeps md symmetric with jsonc/jsonl,
24+// which both reject sentinel values before they reach the AST.
3925export function setMdOcPath(ast: MdAst, path: OcPath, newValue: string): MdEditResult {
4026guardSentinel(newValue, formatOcPath(path));
41-// Frontmatter address: oc://FILE/[frontmatter]/<key>
4227if (path.section === "[frontmatter]") {
4328const key = path.item ?? path.field;
44-if (key === undefined) {
45-return { ok: false, reason: "unresolved" };
46-}
29+if (key === undefined) return { ok: false, reason: "unresolved" };
4730const idx = ast.frontmatter.findIndex((e) => e.key === key);
48-if (idx === -1) {
49-return { ok: false, reason: "unresolved" };
50-}
31+if (idx === -1) return { ok: false, reason: "unresolved" };
5132const existing = ast.frontmatter[idx];
52-if (existing === undefined) {
53-return { ok: false, reason: "unresolved" };
54-}
33+if (existing === undefined) return { ok: false, reason: "unresolved" };
5534const newEntry: FrontmatterEntry = { ...existing, value: newValue };
5635const newFm = ast.frontmatter.slice();
5736newFm[idx] = newEntry;
5837return finalize({ ...ast, frontmatter: newFm });
5938}
603961-// Item-field address: oc://FILE/section/item/field
6240if (path.section === undefined || path.item === undefined || path.field === undefined) {
6341return { ok: false, reason: "not-writable" };
6442}
65436644const sectionSlug = path.section.toLowerCase();
6745const blockIdx = ast.blocks.findIndex((b) => b.slug === sectionSlug);
68-if (blockIdx === -1) {
69-return { ok: false, reason: "unresolved" };
70-}
46+if (blockIdx === -1) return { ok: false, reason: "unresolved" };
7147const block = ast.blocks[blockIdx];
72-if (block === undefined) {
73-return { ok: false, reason: "unresolved" };
74-}
48+if (block === undefined) return { ok: false, reason: "unresolved" };
75497650const itemSlug = path.item.toLowerCase();
7751const itemIdx = block.items.findIndex((i) => i.slug === itemSlug);
78-if (itemIdx === -1) {
79-return { ok: false, reason: "unresolved" };
80-}
52+if (itemIdx === -1) return { ok: false, reason: "unresolved" };
8153const item = block.items[itemIdx];
82-if (item === undefined) {
83-return { ok: false, reason: "unresolved" };
84-}
85-if (item.kv === undefined) {
86-return { ok: false, reason: "no-item-kv" };
87-}
54+if (item === undefined) return { ok: false, reason: "unresolved" };
55+if (item.kv === undefined) return { ok: false, reason: "no-item-kv" };
8856if (item.kv.key.toLowerCase() !== path.field.toLowerCase()) {
8957return { ok: false, reason: "unresolved" };
9058}
915992-const newItem: AstItem = {
93- ...item,
94-kv: { key: item.kv.key, value: newValue },
95-};
60+const newItem: AstItem = { ...item, kv: { key: item.kv.key, value: newValue } };
9661const newItems = block.items.slice();
9762newItems[itemIdx] = newItem;
9863const newBlock: AstBlock = {
@@ -105,28 +70,17 @@ export function setMdOcPath(ast: MdAst, path: OcPath, newValue: string): MdEditR
10570return finalize({ ...ast, blocks: newBlocks });
10671}
10772108-/**
109- * Rebuild block.bodyText so emit-roundtrip mode reflects the edit. We
110- * do a minimal in-place substitution on the existing bodyText: find
111- * each `- key: value` line for a touched item and rewrite the value.
112- *
113- * For items without a matching bullet line, we leave bodyText alone
114- * (the structural fields take precedence in render mode anyway).
115- */
73+// In-place substitution on `bodyText` so round-trip emit reflects the
74+// edit. Items without a matching bullet line are skipped (render mode
75+// uses structural fields anyway).
11676function rebuildBlockBody(block: AstBlock, newItems: readonly AstItem[]): string {
11777let body = block.bodyText;
11878for (let i = 0; i < newItems.length; i++) {
11979const newItem = newItems[i];
12080const oldItem = block.items[i];
121-if (newItem === undefined || oldItem === undefined) {
122-continue;
123-}
124-if (newItem.kv === undefined || oldItem.kv === undefined) {
125-continue;
126-}
127-if (newItem.kv.value === oldItem.kv.value) {
128-continue;
129-}
81+if (newItem === undefined || oldItem === undefined) continue;
82+if (newItem.kv === undefined || oldItem.kv === undefined) continue;
83+if (newItem.kv.value === oldItem.kv.value) continue;
13084const re = new RegExp(`^(\\s*-\\s*${escapeRegex(oldItem.kv.key)}\\s*:\\s*).*$`, "m");
13185body = body.replace(re, `$1${newItem.kv.value}`);
13286}
@@ -137,10 +91,6 @@ function escapeRegex(s: string): string {
13791return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13892}
13993140-/**
141- * Re-render `ast.raw` from the (possibly mutated) tree using the same
142- * shape the round-trip emitter expects.
143- */
14494function finalize(ast: MdAst): MdEditResult {
14595const parts: string[] = [];
14696if (ast.frontmatter.length > 0) {
@@ -151,30 +101,19 @@ function finalize(ast: MdAst): MdEditResult {
151101parts.push("---");
152102}
153103if (ast.preamble.length > 0) {
154-if (parts.length > 0) {
155-parts.push("");
156-}
104+if (parts.length > 0) parts.push("");
157105parts.push(ast.preamble);
158106}
159107for (const block of ast.blocks) {
160-if (parts.length > 0) {
161-parts.push("");
162-}
108+if (parts.length > 0) parts.push("");
163109parts.push(`## ${block.heading}`);
164-if (block.bodyText.length > 0) {
165-parts.push(block.bodyText);
166-}
110+if (block.bodyText.length > 0) parts.push(block.bodyText);
167111}
168-const raw = parts.join("\n");
169-return { ok: true, ast: { ...ast, raw } };
112+return { ok: true, ast: { ...ast, raw: parts.join("\n") } };
170113}
171114172115function formatFrontmatterValue(value: string): string {
173-if (value.length === 0) {
174-return '""';
175-}
176-if (/[:#&*?|<>=!%@`,[\]{}\r\n]/.test(value)) {
177-return JSON.stringify(value);
178-}
116+if (value.length === 0) return '""';
117+if (/[:#&*?|<>=!%@`,[\]{}\r\n]/.test(value)) return JSON.stringify(value);
179118return value;
180119}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。