mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 15:50:42 +00:00
lint(oc-path): satisfy curly + unused-import rules
This commit is contained in:
committed by
Peter Steinberger
parent
ac9418d206
commit
080e34a2c2
@@ -63,13 +63,13 @@ const defaultRuntime: OutputRuntimeEnv = {
|
||||
// Defense-in-depth: replace the redaction sentinel with `[REDACTED]`
|
||||
// before writing, even if upstream emits it.
|
||||
export function scrubSentinel(s: string): string {
|
||||
if (!s.includes(REDACTED_SENTINEL)) return s;
|
||||
if (!s.includes(REDACTED_SENTINEL)) {return s;}
|
||||
return s.split(REDACTED_SENTINEL).join(SCRUB_PLACEHOLDER);
|
||||
}
|
||||
|
||||
function detectMode(options: PathCommandOptions): OutputMode {
|
||||
if (options.json === true) return "json";
|
||||
if (options.human === true) return "human";
|
||||
if (options.json === true) {return "json";}
|
||||
if (options.human === true) {return "human";}
|
||||
return process.stdout.isTTY ? "human" : "json";
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ function catchSentinel<T>(
|
||||
async function loadAst(absPath: string, fileName: string): Promise<OcAst> {
|
||||
const raw = await fs.readFile(absPath, "utf-8");
|
||||
const kind = inferKind(fileName);
|
||||
if (kind === "jsonc") return parseJsonc(raw).ast;
|
||||
if (kind === "jsonl") return parseJsonl(raw).ast;
|
||||
if (kind === "jsonc") {return parseJsonc(raw).ast;}
|
||||
if (kind === "jsonl") {return parseJsonl(raw).ast;}
|
||||
return parseMd(raw).ast;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ function emitForKind(ast: OcAst, fileName?: string): string {
|
||||
}
|
||||
|
||||
function resolveFsPath(path: OcPath, options: PathCommandOptions): string {
|
||||
if (options.file !== undefined) return resolvePath(options.file);
|
||||
if (options.file !== undefined) {return resolvePath(options.file);}
|
||||
return resolvePath(options.cwd ?? process.cwd(), path.file);
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ function formatMatchHuman(match: OcMatch): string {
|
||||
if (match.kind === "leaf") {
|
||||
return `leaf @ L${match.line}: ${JSON.stringify(match.valueText)} (${match.leafType})`;
|
||||
}
|
||||
if (match.kind === "node") return `node @ L${match.line} [${match.descriptor}]`;
|
||||
if (match.kind === "node") {return `node @ L${match.line} [${match.descriptor}]`;}
|
||||
if (match.kind === "insertion-point") {
|
||||
return `insertion-point @ L${match.line} [${match.container}]`;
|
||||
}
|
||||
@@ -199,9 +199,9 @@ export async function pathResolveCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): Promise<void> {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(pathStr, "resolve: missing <oc-path> argument", runtime, mode)) return;
|
||||
if (!requireArg(pathStr, "resolve: missing <oc-path> argument", runtime, mode)) {return;}
|
||||
const ocPath = tryParse(pathStr, runtime, mode);
|
||||
if (ocPath === null) return;
|
||||
if (ocPath === null) {return;}
|
||||
const ast = await loadAst(resolveFsPath(ocPath, options), ocPath.file);
|
||||
let match: OcMatch | null;
|
||||
try {
|
||||
@@ -230,15 +230,15 @@ export async function pathSetCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): Promise<void> {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(pathStr, "set: requires <oc-path> <value>", runtime, mode)) return;
|
||||
if (!requireArg(value, "set: requires <oc-path> <value>", runtime, mode)) return;
|
||||
if (!requireArg(pathStr, "set: requires <oc-path> <value>", runtime, mode)) {return;}
|
||||
if (!requireArg(value, "set: requires <oc-path> <value>", runtime, mode)) {return;}
|
||||
const ocPath = tryParse(pathStr, runtime, mode);
|
||||
if (ocPath === null) return;
|
||||
if (ocPath === null) {return;}
|
||||
const fsPath = resolveFsPath(ocPath, options);
|
||||
const ast = await loadAst(fsPath, ocPath.file);
|
||||
|
||||
const result = catchSentinel("set", runtime, mode, () => setOcPath(ast, ocPath, value));
|
||||
if (result === null) return;
|
||||
if (result === null) {return;}
|
||||
if (!result.ok) {
|
||||
const detail = "detail" in result ? result.detail : undefined;
|
||||
emit(
|
||||
@@ -254,7 +254,7 @@ export async function pathSetCommand(
|
||||
const newBytes = catchSentinel("emit", runtime, mode, () =>
|
||||
emitForKind(result.ast, ocPath.file),
|
||||
);
|
||||
if (newBytes === null) return;
|
||||
if (newBytes === null) {return;}
|
||||
|
||||
if (options.dryRun === true) {
|
||||
emit(
|
||||
@@ -280,9 +280,9 @@ export async function pathFindCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): Promise<void> {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(patternStr, "find: missing <pattern> argument", runtime, mode)) return;
|
||||
if (!requireArg(patternStr, "find: missing <pattern> argument", runtime, mode)) {return;}
|
||||
const pattern = tryParse(patternStr, runtime, mode);
|
||||
if (pattern === null) return;
|
||||
if (pattern === null) {return;}
|
||||
// File-slot wildcards would silently ENOENT during readFile; reject.
|
||||
if (/[*?]/.test(pattern.file)) {
|
||||
emitError(
|
||||
@@ -306,7 +306,7 @@ export async function pathFindCommand(
|
||||
matches: matches.map((m) => ({ path: formatOcPath(m.path), match: m.match })),
|
||||
},
|
||||
() => {
|
||||
if (matches.length === 0) return `0 matches for ${patternStr}`;
|
||||
if (matches.length === 0) {return `0 matches for ${patternStr}`;}
|
||||
const plural = matches.length === 1 ? "" : "es";
|
||||
const lines = [`${matches.length} match${plural} for ${patternStr}:`];
|
||||
for (const m of matches) {
|
||||
@@ -315,7 +315,7 @@ export async function pathFindCommand(
|
||||
return lines.join("\n");
|
||||
},
|
||||
);
|
||||
if (matches.length === 0) runtime.exit(1);
|
||||
if (matches.length === 0) {runtime.exit(1);}
|
||||
}
|
||||
|
||||
export function pathValidateCommand(
|
||||
@@ -324,7 +324,7 @@ export function pathValidateCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): void {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(pathStr, "validate: missing <oc-path> argument", runtime, mode)) return;
|
||||
if (!requireArg(pathStr, "validate: missing <oc-path> argument", runtime, mode)) {return;}
|
||||
try {
|
||||
const ocPath = parseOcPath(pathStr);
|
||||
emit(
|
||||
@@ -344,10 +344,10 @@ export function pathValidateCommand(
|
||||
},
|
||||
() => {
|
||||
const lines = [`valid: ${pathStr}`, ` file: ${ocPath.file}`];
|
||||
if (ocPath.section !== undefined) lines.push(` section: ${ocPath.section}`);
|
||||
if (ocPath.item !== undefined) lines.push(` item: ${ocPath.item}`);
|
||||
if (ocPath.field !== undefined) lines.push(` field: ${ocPath.field}`);
|
||||
if (ocPath.session !== undefined) lines.push(` session: ${ocPath.session}`);
|
||||
if (ocPath.section !== undefined) {lines.push(` section: ${ocPath.section}`);}
|
||||
if (ocPath.item !== undefined) {lines.push(` item: ${ocPath.item}`);}
|
||||
if (ocPath.field !== undefined) {lines.push(` field: ${ocPath.field}`);}
|
||||
if (ocPath.session !== undefined) {lines.push(` session: ${ocPath.session}`);}
|
||||
return lines.join("\n");
|
||||
},
|
||||
);
|
||||
@@ -372,7 +372,7 @@ export async function pathEmitCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): Promise<void> {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(fileArg, "emit: missing <file> argument", runtime, mode)) return;
|
||||
if (!requireArg(fileArg, "emit: missing <file> argument", runtime, mode)) {return;}
|
||||
const fsPath =
|
||||
options.file !== undefined
|
||||
? resolvePath(options.file)
|
||||
@@ -380,7 +380,7 @@ export async function pathEmitCommand(
|
||||
const fileName = fsPath.split(/[\\/]/).pop() ?? fileArg;
|
||||
const ast = await loadAst(fsPath, fileName);
|
||||
const bytes = catchSentinel("emit", runtime, mode, () => emitForKind(ast, fileName));
|
||||
if (bytes === null) return;
|
||||
if (bytes === null) {return;}
|
||||
if (mode === "json") {
|
||||
runtime.writeStdout(scrubSentinel(JSON.stringify({ ok: true, kind: ast.kind, bytes })));
|
||||
return;
|
||||
|
||||
@@ -26,11 +26,11 @@ export function setMdOcPath(ast: MdAst, path: OcPath, newValue: string): MdEditR
|
||||
guardSentinel(newValue, formatOcPath(path));
|
||||
if (path.section === "[frontmatter]") {
|
||||
const key = path.item ?? path.field;
|
||||
if (key === undefined) return { ok: false, reason: "unresolved" };
|
||||
if (key === undefined) {return { ok: false, reason: "unresolved" };}
|
||||
const idx = ast.frontmatter.findIndex((e) => e.key === key);
|
||||
if (idx === -1) return { ok: false, reason: "unresolved" };
|
||||
if (idx === -1) {return { ok: false, reason: "unresolved" };}
|
||||
const existing = ast.frontmatter[idx];
|
||||
if (existing === undefined) return { ok: false, reason: "unresolved" };
|
||||
if (existing === undefined) {return { ok: false, reason: "unresolved" };}
|
||||
const newEntry: FrontmatterEntry = { ...existing, value: newValue };
|
||||
const newFm = ast.frontmatter.slice();
|
||||
newFm[idx] = newEntry;
|
||||
@@ -43,16 +43,16 @@ export function setMdOcPath(ast: MdAst, path: OcPath, newValue: string): MdEditR
|
||||
|
||||
const sectionSlug = path.section.toLowerCase();
|
||||
const blockIdx = ast.blocks.findIndex((b) => b.slug === sectionSlug);
|
||||
if (blockIdx === -1) return { ok: false, reason: "unresolved" };
|
||||
if (blockIdx === -1) {return { ok: false, reason: "unresolved" };}
|
||||
const block = ast.blocks[blockIdx];
|
||||
if (block === undefined) return { ok: false, reason: "unresolved" };
|
||||
if (block === undefined) {return { ok: false, reason: "unresolved" };}
|
||||
|
||||
const itemSlug = path.item.toLowerCase();
|
||||
const itemIdx = block.items.findIndex((i) => i.slug === itemSlug);
|
||||
if (itemIdx === -1) return { ok: false, reason: "unresolved" };
|
||||
if (itemIdx === -1) {return { ok: false, reason: "unresolved" };}
|
||||
const item = block.items[itemIdx];
|
||||
if (item === undefined) return { ok: false, reason: "unresolved" };
|
||||
if (item.kv === undefined) return { ok: false, reason: "no-item-kv" };
|
||||
if (item === undefined) {return { ok: false, reason: "unresolved" };}
|
||||
if (item.kv === undefined) {return { ok: false, reason: "no-item-kv" };}
|
||||
if (item.kv.key.toLowerCase() !== path.field.toLowerCase()) {
|
||||
return { ok: false, reason: "unresolved" };
|
||||
}
|
||||
@@ -78,9 +78,9 @@ function rebuildBlockBody(block: AstBlock, newItems: readonly AstItem[]): string
|
||||
for (let i = 0; i < newItems.length; i++) {
|
||||
const newItem = newItems[i];
|
||||
const oldItem = block.items[i];
|
||||
if (newItem === undefined || oldItem === undefined) continue;
|
||||
if (newItem.kv === undefined || oldItem.kv === undefined) continue;
|
||||
if (newItem.kv.value === oldItem.kv.value) continue;
|
||||
if (newItem === undefined || oldItem === undefined) {continue;}
|
||||
if (newItem.kv === undefined || oldItem.kv === undefined) {continue;}
|
||||
if (newItem.kv.value === oldItem.kv.value) {continue;}
|
||||
const re = new RegExp(`^(\\s*-\\s*${escapeRegex(oldItem.kv.key)}\\s*:\\s*).*$`, "m");
|
||||
body = body.replace(re, `$1${newItem.kv.value}`);
|
||||
}
|
||||
@@ -101,19 +101,19 @@ function finalize(ast: MdAst): MdEditResult {
|
||||
parts.push("---");
|
||||
}
|
||||
if (ast.preamble.length > 0) {
|
||||
if (parts.length > 0) parts.push("");
|
||||
if (parts.length > 0) {parts.push("");}
|
||||
parts.push(ast.preamble);
|
||||
}
|
||||
for (const block of ast.blocks) {
|
||||
if (parts.length > 0) parts.push("");
|
||||
if (parts.length > 0) {parts.push("");}
|
||||
parts.push(`## ${block.heading}`);
|
||||
if (block.bodyText.length > 0) parts.push(block.bodyText);
|
||||
if (block.bodyText.length > 0) {parts.push(block.bodyText);}
|
||||
}
|
||||
return { ok: true, ast: { ...ast, raw: parts.join("\n") } };
|
||||
}
|
||||
|
||||
function formatFrontmatterValue(value: string): string {
|
||||
if (value.length === 0) return '""';
|
||||
if (/[:#&*?|<>=!%@`,[\]{}\r\n]/.test(value)) return JSON.stringify(value);
|
||||
if (value.length === 0) {return '""';}
|
||||
if (/[:#&*?|<>=!%@`,[\]{}\r\n]/.test(value)) {return JSON.stringify(value);}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -129,9 +129,9 @@ function repackSlotSubs(pattern: OcPath, slotSubs: readonly SlotSub[]): OcPath {
|
||||
const itemSubs: string[] = [];
|
||||
const fieldSubs: string[] = [];
|
||||
for (const s of slotSubs) {
|
||||
if (s.slot === "section") sectionSubs.push(s.value);
|
||||
else if (s.slot === "item") itemSubs.push(s.value);
|
||||
else fieldSubs.push(s.value);
|
||||
if (s.slot === "section") {sectionSubs.push(s.value);}
|
||||
else if (s.slot === "item") {itemSubs.push(s.value);}
|
||||
else {fieldSubs.push(s.value);}
|
||||
}
|
||||
return {
|
||||
file: pattern.file,
|
||||
@@ -176,7 +176,7 @@ function dispatchSeg<T>(
|
||||
|
||||
if (isUnionSeg(cur.value)) {
|
||||
const alts = parseUnionSeg(cur.value);
|
||||
if (alts === null) return;
|
||||
if (alts === null) {return;}
|
||||
for (const alt of alts) {
|
||||
const altSubs = subs.slice();
|
||||
altSubs[i] = { slot: cur.slot, value: alt };
|
||||
@@ -187,7 +187,7 @@ function dispatchSeg<T>(
|
||||
|
||||
if (isPredicateSeg(cur.value)) {
|
||||
const pred = parsePredicateSeg(cur.value);
|
||||
if (pred === null) return;
|
||||
if (pred === null) {return;}
|
||||
for (const m of ops.predicate(node, pred)) {
|
||||
ops.walk(m.child, subs, i + 1, [...walked, { slot: cur.slot, value: m.keySub }], onMatch);
|
||||
}
|
||||
@@ -197,7 +197,7 @@ function dispatchSeg<T>(
|
||||
if (cur.value === WILDCARD_RECURSIVE) {
|
||||
// `**` — descend with `**` consumed (i+1) AND retained (i) so
|
||||
// deeper structures still match. Emit if no subs remain.
|
||||
if (i + 1 >= subs.length) onMatch(walked);
|
||||
if (i + 1 >= subs.length) {onMatch(walked);}
|
||||
for (const m of ops.enumerate(node)) {
|
||||
const nextWalked: readonly SlotSub[] = [...walked, { slot: cur.slot, value: m.keySub }];
|
||||
ops.walk(m.child, subs, i + 1, nextWalked, onMatch);
|
||||
@@ -215,13 +215,13 @@ function dispatchSeg<T>(
|
||||
|
||||
if (isPositionalSeg(cur.value)) {
|
||||
const m = ops.positional(node, cur.value);
|
||||
if (m === null) return;
|
||||
if (m === null) {return;}
|
||||
ops.walk(m.child, subs, i + 1, [...walked, { slot: cur.slot, value: m.keySub }], onMatch);
|
||||
return;
|
||||
}
|
||||
|
||||
const m = ops.lookup(node, cur.value);
|
||||
if (m === null) return;
|
||||
if (m === null) {return;}
|
||||
ops.walk(m.child, subs, i + 1, [...walked, { slot: cur.slot, value: m.keySub }], onMatch);
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ function walkJsonc(
|
||||
const jsoncOps: WalkOps<JsoncValue> = {
|
||||
*enumerate(node) {
|
||||
if (node.kind === "object") {
|
||||
for (const e of node.entries) yield { keySub: quoteSeg(e.key), child: e.value };
|
||||
for (const e of node.entries) {yield { keySub: quoteSeg(e.key), child: e.value };}
|
||||
} else if (node.kind === "array") {
|
||||
for (let idx = 0; idx < node.items.length; idx++) {
|
||||
yield { keySub: String(idx), child: node.items[idx] };
|
||||
@@ -262,14 +262,14 @@ const jsoncOps: WalkOps<JsoncValue> = {
|
||||
}
|
||||
if (node.kind === "array") {
|
||||
const idx = Number(key);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= node.items.length) return null;
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= node.items.length) {return null;}
|
||||
return { keySub: key, child: node.items[idx] };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
positional(node, seg) {
|
||||
const concrete = positionalForJsoncNode(node, seg);
|
||||
if (concrete === null) return null;
|
||||
if (concrete === null) {return null;}
|
||||
return jsoncOps.lookup(node, concrete);
|
||||
},
|
||||
*predicate(node, pred) {
|
||||
@@ -325,12 +325,12 @@ function walkJsonl(
|
||||
const jsonlOps: WalkOps<JsonlAst> = {
|
||||
*enumerate(ast) {
|
||||
for (const l of ast.lines) {
|
||||
if (l.kind === "value") yield { keySub: `L${l.line}`, child: lineHolder(ast, l) };
|
||||
if (l.kind === "value") {yield { keySub: `L${l.line}`, child: lineHolder(ast, l) };}
|
||||
}
|
||||
},
|
||||
lookup(ast, key) {
|
||||
const line = pickLine(ast, key);
|
||||
if (line === null) return null;
|
||||
if (line === null) {return null;}
|
||||
const concreteAddr = line.kind === "value" ? `L${line.line}` : key;
|
||||
return { keySub: concreteAddr, child: lineHolder(ast, line) };
|
||||
},
|
||||
@@ -339,7 +339,7 @@ const jsonlOps: WalkOps<JsonlAst> = {
|
||||
},
|
||||
*predicate(ast, pred) {
|
||||
for (const l of ast.lines) {
|
||||
if (l.kind !== "value") continue;
|
||||
if (l.kind !== "value") {continue;}
|
||||
const actual = topLevelLeafText(l.value, pred.key);
|
||||
if (evaluatePredicate(actual, pred)) {
|
||||
yield { keySub: `L${l.line}`, child: lineHolder(ast, l) };
|
||||
@@ -359,7 +359,7 @@ const jsonlOps: WalkOps<JsonlAst> = {
|
||||
onMatch(walked);
|
||||
return;
|
||||
}
|
||||
if (line.kind !== "value") return;
|
||||
if (line.kind !== "value") {return;}
|
||||
walkJsonc(line.value, subs, i, walked, onMatch);
|
||||
},
|
||||
};
|
||||
@@ -382,12 +382,12 @@ function unwrapHolder(holder: JsonlAst): JsonlLine | null {
|
||||
}
|
||||
|
||||
function topLevelLeafText(value: JsoncValue, key: string): string | null {
|
||||
if (value.kind !== "object") return null;
|
||||
if (value.kind !== "object") {return null;}
|
||||
const entry = value.entries.find((e) => e.key === key);
|
||||
if (entry === undefined) return null;
|
||||
if (entry === undefined) {return null;}
|
||||
const v = entry.value;
|
||||
if (v.kind === "string") return v.value;
|
||||
if (v.kind === "number" || v.kind === "boolean") return String(v.value);
|
||||
if (v.kind === "string") {return v.value;}
|
||||
if (v.kind === "number" || v.kind === "boolean") {return String(v.value);}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -395,15 +395,15 @@ function pickLine(ast: JsonlAst, addr: string): JsonlLine | null {
|
||||
if (addr === "$last") {
|
||||
for (let i = ast.lines.length - 1; i >= 0; i--) {
|
||||
const l = ast.lines[i];
|
||||
if (l !== undefined && l.kind === "value") return l;
|
||||
if (l !== undefined && l.kind === "value") {return l;}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const m = /^L(\d+)$/.exec(addr);
|
||||
if (m === null || m[1] === undefined) return null;
|
||||
if (m === null || m[1] === undefined) {return null;}
|
||||
const target = Number(m[1]);
|
||||
for (const l of ast.lines) {
|
||||
if (l.line === target) return l;
|
||||
if (l.line === target) {return l;}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -449,7 +449,7 @@ function walkMd(
|
||||
}
|
||||
const fmKey = isQuotedSeg(next.value) ? unquoteSeg(next.value) : next.value;
|
||||
const entry = level.ast.frontmatter.find((e) => e.key === fmKey);
|
||||
if (entry === undefined) return;
|
||||
if (entry === undefined) {return;}
|
||||
onMatch([
|
||||
{ slot: cur.slot, value: cur.value },
|
||||
{ slot: next.slot, value: next.value },
|
||||
@@ -472,34 +472,34 @@ function walkMdItemField(
|
||||
walked: readonly SlotSub[],
|
||||
onMatch: OnMatch,
|
||||
): void {
|
||||
if (item.kv === undefined) return;
|
||||
if (item.kv === undefined) {return;}
|
||||
const key = item.kv.key;
|
||||
const emit = (value: string): void => {
|
||||
onMatch([...walked, { slot: cur.slot, value }]);
|
||||
};
|
||||
if (isUnionSeg(cur.value)) {
|
||||
const alts = parseUnionSeg(cur.value);
|
||||
if (alts === null) return;
|
||||
if (alts === null) {return;}
|
||||
for (const alt of alts) {
|
||||
if (alt.toLowerCase() === key.toLowerCase()) emit(key);
|
||||
if (alt.toLowerCase() === key.toLowerCase()) {emit(key);}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isPredicateSeg(cur.value)) {
|
||||
const pred = parsePredicateSeg(cur.value);
|
||||
if (pred !== null && mdItemMatchesPredicate(item, pred)) emit(key);
|
||||
if (pred !== null && mdItemMatchesPredicate(item, pred)) {emit(key);}
|
||||
return;
|
||||
}
|
||||
if (cur.value === WILDCARD_SINGLE || cur.value === WILDCARD_RECURSIVE) {
|
||||
emit(key);
|
||||
return;
|
||||
}
|
||||
if (key.toLowerCase() === cur.value.toLowerCase()) emit(cur.value);
|
||||
if (key.toLowerCase() === cur.value.toLowerCase()) {emit(cur.value);}
|
||||
}
|
||||
|
||||
function blockSlugCounts(items: readonly MdItem[]): Map<string, number> {
|
||||
const counts = new Map<string, number>();
|
||||
for (const item of items) counts.set(item.slug, (counts.get(item.slug) ?? 0) + 1);
|
||||
for (const item of items) {counts.set(item.slug, (counts.get(item.slug) ?? 0) + 1);}
|
||||
return counts;
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ const mdOps: WalkOps<MdLevel> = {
|
||||
// Ordinal `#N` short-circuits slug lookup.
|
||||
if (isOrdinalSeg(key)) {
|
||||
const n = parseOrdinalSeg(key);
|
||||
if (n === null || n < 0 || n >= level.block.items.length) return null;
|
||||
if (n === null || n < 0 || n >= level.block.items.length) {return null;}
|
||||
return { keySub: key, child: { kind: "item", item: level.block.items[n], ast: level.ast } };
|
||||
}
|
||||
const target = key.toLowerCase();
|
||||
@@ -544,12 +544,12 @@ const mdOps: WalkOps<MdLevel> = {
|
||||
return null;
|
||||
},
|
||||
positional(level, seg) {
|
||||
if (level.kind !== "block") return null;
|
||||
if (level.kind !== "block") {return null;}
|
||||
const concrete = resolvePositionalSeg(seg, {
|
||||
indexable: true,
|
||||
size: level.block.items.length,
|
||||
});
|
||||
if (concrete === null) return null;
|
||||
if (concrete === null) {return null;}
|
||||
// Preserve the positional token in keySub so the resolver
|
||||
// re-evaluates positionally on round-trip.
|
||||
const item = level.block.items[Number(concrete)];
|
||||
@@ -579,14 +579,14 @@ const mdOps: WalkOps<MdLevel> = {
|
||||
};
|
||||
|
||||
function mdItemMatchesPredicate(item: MdItem, pred: PredicateSpec): boolean {
|
||||
if (item.kv === undefined) return false;
|
||||
if (item.kv.key.toLowerCase() !== pred.key.toLowerCase()) return false;
|
||||
if (item.kv === undefined) {return false;}
|
||||
if (item.kv.key.toLowerCase() !== pred.key.toLowerCase()) {return false;}
|
||||
return evaluatePredicate(item.kv.value, pred);
|
||||
}
|
||||
|
||||
function mdBlockHasMatchingItem(block: MdBlock, pred: PredicateSpec): boolean {
|
||||
for (const item of block.items) {
|
||||
if (mdItemMatchesPredicate(item, pred)) return true;
|
||||
if (mdItemMatchesPredicate(item, pred)) {return true;}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -596,12 +596,12 @@ function jsoncChildMatchesPredicate(node: JsoncValue, pred: PredicateSpec): bool
|
||||
}
|
||||
|
||||
function jsoncChildFieldText(node: JsoncValue, key: string): string | null {
|
||||
if (node.kind !== "object") return null;
|
||||
if (node.kind !== "object") {return null;}
|
||||
const e = node.entries.find((entry) => entry.key === key);
|
||||
if (e === undefined) return null;
|
||||
if (e === undefined) {return null;}
|
||||
const v = e.value;
|
||||
if (v.kind === "string") return v.value;
|
||||
if (v.kind === "number" || v.kind === "boolean") return String(v.value);
|
||||
if (v.kind === "null") return "null";
|
||||
if (v.kind === "string") {return v.value;}
|
||||
if (v.kind === "number" || v.kind === "boolean") {return String(v.value);}
|
||||
if (v.kind === "null") {return "null";}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function emitJsonc(ast: JsoncAst, opts: JsoncEmitOptions = {}): string {
|
||||
}
|
||||
|
||||
// Render mode loses comments; walks leaves for caller-injected sentinel.
|
||||
if (ast.root === null) return "";
|
||||
if (ast.root === null) {return "";}
|
||||
return renderValue(ast.root, guardPath, []);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ export type JsoncOcPathMatch =
|
||||
};
|
||||
|
||||
export function resolveJsoncOcPath(ast: JsoncAst, path: OcPath): JsoncOcPathMatch | null {
|
||||
if (ast.root === null) return null;
|
||||
if (ast.root === null) {return null;}
|
||||
|
||||
const segments: string[] = [];
|
||||
const collect = (slot: string | undefined): void => {
|
||||
if (slot === undefined) return;
|
||||
if (slot === undefined) {return;}
|
||||
for (const s of splitRespectingBrackets(slot, ".")) {
|
||||
segments.push(isQuotedSeg(s) ? unquoteSeg(s) : s);
|
||||
}
|
||||
@@ -39,35 +39,35 @@ export function resolveJsoncOcPath(ast: JsoncAst, path: OcPath): JsoncOcPathMatc
|
||||
collect(path.item);
|
||||
collect(path.field);
|
||||
|
||||
if (segments.length === 0) return { kind: "root", node: ast };
|
||||
if (segments.length === 0) {return { kind: "root", node: ast };}
|
||||
|
||||
let current: JsoncValue = ast.root;
|
||||
let lastEntry: JsoncEntry | null = null;
|
||||
const walked: string[] = [];
|
||||
|
||||
for (let seg of segments) {
|
||||
if (seg.length === 0) return null;
|
||||
if (seg.length === 0) {return null;}
|
||||
// `-N` on an indexable container is positional; on a keyed
|
||||
// container it falls through to literal-key lookup (e.g. Telegram
|
||||
// supergroup IDs — openclaw#59934).
|
||||
if (isPositionalSeg(seg)) {
|
||||
const concrete = positionalForJsonc(current, seg);
|
||||
if (concrete !== null) seg = concrete;
|
||||
if (concrete !== null) {seg = concrete;}
|
||||
}
|
||||
walked.push(seg);
|
||||
if (current.kind === "object") {
|
||||
const entry = current.entries.find((e) => e.key === seg);
|
||||
if (entry === undefined) return null;
|
||||
if (entry === undefined) {return null;}
|
||||
lastEntry = entry;
|
||||
current = entry.value;
|
||||
continue;
|
||||
}
|
||||
if (current.kind === "array") {
|
||||
const idx = Number(seg);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= current.items.length) return null;
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= current.items.length) {return null;}
|
||||
lastEntry = null;
|
||||
const item = current.items[idx];
|
||||
if (item === undefined) return null;
|
||||
if (item === undefined) {return null;}
|
||||
current = item;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ function replaceAt(
|
||||
newValue: JsoncValue,
|
||||
): JsoncValue | null {
|
||||
const seg = segments[i];
|
||||
if (seg === undefined) return newValue;
|
||||
if (seg.length === 0) return null;
|
||||
if (seg === undefined) {return newValue;}
|
||||
if (seg.length === 0) {return null;}
|
||||
|
||||
if (current.kind === "object") {
|
||||
// Positional tokens resolve against the entries' ordered key list;
|
||||
@@ -96,16 +96,16 @@ function replaceAt(
|
||||
size: current.entries.length,
|
||||
keys: current.entries.map((e) => e.key),
|
||||
});
|
||||
if (resolved === null) return null;
|
||||
if (resolved === null) {return null;}
|
||||
segNorm = resolved;
|
||||
}
|
||||
const lookupKey = isQuotedSeg(segNorm) ? unquoteSeg(segNorm) : segNorm;
|
||||
const idx = current.entries.findIndex((e) => e.key === lookupKey);
|
||||
if (idx === -1) return null;
|
||||
if (idx === -1) {return null;}
|
||||
const child = current.entries[idx];
|
||||
if (child === undefined) return null;
|
||||
if (child === undefined) {return null;}
|
||||
const replacedChild = replaceAt(child.value, segments, i + 1, newValue);
|
||||
if (replacedChild === null) return null;
|
||||
if (replacedChild === null) {return null;}
|
||||
const newEntry: JsoncEntry = { ...child, value: replacedChild };
|
||||
const newEntries = current.entries.slice();
|
||||
newEntries[idx] = newEntry;
|
||||
@@ -123,15 +123,15 @@ function replaceAt(
|
||||
indexable: true,
|
||||
size: current.items.length,
|
||||
});
|
||||
if (resolved === null) return null;
|
||||
if (resolved === null) {return null;}
|
||||
segNorm = resolved;
|
||||
}
|
||||
const idx = Number(segNorm);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= current.items.length) return null;
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= current.items.length) {return null;}
|
||||
const child = current.items[idx];
|
||||
if (child === undefined) return null;
|
||||
if (child === undefined) {return null;}
|
||||
const replacedChild = replaceAt(child, segments, i + 1, newValue);
|
||||
if (replacedChild === null) return null;
|
||||
if (replacedChild === null) {return null;}
|
||||
const newItems = current.items.slice();
|
||||
newItems[idx] = replacedChild;
|
||||
return {
|
||||
@@ -147,12 +147,12 @@ function replaceAt(
|
||||
function pickLineIndex(ast: JsonlAst, addr: string): number {
|
||||
if (addr === "$last") {
|
||||
for (let i = ast.lines.length - 1; i >= 0; i--) {
|
||||
if (ast.lines[i]?.kind === "value") return i;
|
||||
if (ast.lines[i]?.kind === "value") {return i;}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
const m = /^L(\d+)$/.exec(addr);
|
||||
if (m === null || m[1] === undefined) return -1;
|
||||
if (m === null || m[1] === undefined) {return -1;}
|
||||
const target = Number(m[1]);
|
||||
return ast.lines.findIndex((l) => l.line === target);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const BOM = "";
|
||||
function hasControlChar(s: string): boolean {
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const cc = s.charCodeAt(i);
|
||||
if (cc <= 0x1f || cc === 0x7f) return true;
|
||||
if (cc <= 0x1f || cc === 0x7f) {return true;}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -206,9 +206,9 @@ export function formatOcPath(path: OcPath): string {
|
||||
// (quoted, predicate, union, sentinel). Plain concatenation would
|
||||
// silently split a raw `foo/bar` slot into two segments at parse.
|
||||
const formatSubSegment = (sub: string): string => {
|
||||
if (isQuotedSeg(sub)) return sub;
|
||||
if (sub.startsWith("[") && sub.endsWith("]")) return sub;
|
||||
if (sub.startsWith("{") && sub.endsWith("}")) return sub;
|
||||
if (isQuotedSeg(sub)) {return sub;}
|
||||
if (sub.startsWith("[") && sub.endsWith("]")) {return sub;}
|
||||
if (sub.startsWith("{") && sub.endsWith("}")) {return sub;}
|
||||
return quoteSeg(sub);
|
||||
};
|
||||
const validateSubForFormat = (sub: string, slotName: string): void => {
|
||||
@@ -241,10 +241,10 @@ export function formatOcPath(path: OcPath): string {
|
||||
const fileNeedsQuote = /[/[\]{}?&%"\s]/.test(path.file);
|
||||
const formattedFile = fileNeedsQuote ? quoteSeg(path.file) : path.file;
|
||||
let out = OC_SCHEME + formattedFile;
|
||||
if (path.section !== undefined) out += "/" + formatSlot(path.section, "section");
|
||||
if (path.item !== undefined) out += "/" + formatSlot(path.item, "item");
|
||||
if (path.field !== undefined) out += "/" + formatSlot(path.field, "field");
|
||||
if (path.session !== undefined) out += "?session=" + path.session;
|
||||
if (path.section !== undefined) {out += "/" + formatSlot(path.section, "section");}
|
||||
if (path.item !== undefined) {out += "/" + formatSlot(path.item, "item");}
|
||||
if (path.field !== undefined) {out += "/" + formatSlot(path.field, "field");}
|
||||
if (path.session !== undefined) {out += "?session=" + path.session;}
|
||||
|
||||
if (out.length > MAX_PATH_LENGTH) {
|
||||
fail(
|
||||
@@ -263,7 +263,7 @@ export function formatOcPath(path: OcPath): string {
|
||||
|
||||
/** True iff `input` is a string `parseOcPath` would accept. */
|
||||
export function isValidOcPath(input: unknown): input is string {
|
||||
if (typeof input !== "string") return false;
|
||||
if (typeof input !== "string") {return false;}
|
||||
try {
|
||||
parseOcPath(input);
|
||||
return true;
|
||||
@@ -305,8 +305,8 @@ export interface PositionalContainer {
|
||||
|
||||
// Resolve `$last` against a container; null when empty.
|
||||
export function resolvePositionalSeg(seg: string, container: PositionalContainer): string | null {
|
||||
if (seg !== POS_LAST || container.size === 0) return null;
|
||||
if (!container.indexable) return container.keys?.[container.keys.length - 1] ?? null;
|
||||
if (seg !== POS_LAST || container.size === 0) {return null;}
|
||||
if (!container.indexable) {return container.keys?.[container.keys.length - 1] ?? null;}
|
||||
return String(container.size - 1);
|
||||
}
|
||||
|
||||
@@ -325,13 +325,13 @@ export const WILDCARD_RECURSIVE = "**";
|
||||
*/
|
||||
export function isPattern(path: OcPath): boolean {
|
||||
for (const slot of [path.section, path.item, path.field]) {
|
||||
if (slot === undefined) continue;
|
||||
if (slot === undefined) {continue;}
|
||||
// Quote-aware split — `slot.split('.')` would shred quoted keys
|
||||
// containing literal `*` and falsely flag them as wildcards.
|
||||
for (const sub of splitRespectingBrackets(slot, ".")) {
|
||||
if (sub === WILDCARD_SINGLE || sub === WILDCARD_RECURSIVE) return true;
|
||||
if (isUnionSeg(sub)) return true;
|
||||
if (isPredicateSeg(sub)) return true;
|
||||
if (sub === WILDCARD_SINGLE || sub === WILDCARD_RECURSIVE) {return true;}
|
||||
if (isUnionSeg(sub)) {return true;}
|
||||
if (isPredicateSeg(sub)) {return true;}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -346,11 +346,11 @@ export function isUnionSeg(seg: string): boolean {
|
||||
}
|
||||
|
||||
export function parseUnionSeg(seg: string): readonly string[] | null {
|
||||
if (!isUnionSeg(seg)) return null;
|
||||
if (!isUnionSeg(seg)) {return null;}
|
||||
const inner = seg.slice(1, -1);
|
||||
if (inner.length === 0) return null;
|
||||
if (inner.length === 0) {return null;}
|
||||
const alts = inner.split(",");
|
||||
if (alts.some((a) => a.length === 0)) return null;
|
||||
if (alts.some((a) => a.length === 0)) {return null;}
|
||||
return alts;
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ export type PredicateOp = "=" | "!=" | "<" | "<=" | ">" | ">=";
|
||||
const PREDICATE_OPS: readonly PredicateOp[] = ["!=", "<=", ">=", "<", ">", "="];
|
||||
|
||||
export function isPredicateSeg(seg: string): boolean {
|
||||
if (seg.length < 4 || !seg.startsWith("[") || !seg.endsWith("]")) return false;
|
||||
if (seg.length < 4 || !seg.startsWith("[") || !seg.endsWith("]")) {return false;}
|
||||
const inner = new Set(seg.slice(1, -1));
|
||||
return PREDICATE_OPS.some((op) => inner.has(op));
|
||||
}
|
||||
@@ -375,14 +375,14 @@ export interface PredicateSpec {
|
||||
}
|
||||
|
||||
export function parsePredicateSeg(seg: string): PredicateSpec | null {
|
||||
if (seg.length < 4 || !seg.startsWith("[") || !seg.endsWith("]")) return null;
|
||||
if (seg.length < 4 || !seg.startsWith("[") || !seg.endsWith("]")) {return null;}
|
||||
const inner = seg.slice(1, -1);
|
||||
// Leftmost operator wins; at each position, multi-char beats single
|
||||
// (so `[a<=b]` parses as op=`<=`, not op=`<`).
|
||||
for (let i = 1; i < inner.length; i++) {
|
||||
for (const op of PREDICATE_OPS) {
|
||||
if (!inner.startsWith(op, i)) continue;
|
||||
if (i + op.length >= inner.length) continue; // empty value
|
||||
if (!inner.startsWith(op, i)) {continue;}
|
||||
if (i + op.length >= inner.length) {continue;} // empty value
|
||||
return { key: inner.slice(0, i), op, value: inner.slice(i + op.length) };
|
||||
}
|
||||
}
|
||||
@@ -391,7 +391,7 @@ export function parsePredicateSeg(seg: string): PredicateSpec | null {
|
||||
|
||||
// Numeric ops require both sides to coerce to finite numbers.
|
||||
export function evaluatePredicate(actual: string | null, pred: PredicateSpec): boolean {
|
||||
if (actual === null) return false;
|
||||
if (actual === null) {return false;}
|
||||
switch (pred.op) {
|
||||
case "=":
|
||||
return actual === pred.value;
|
||||
@@ -403,10 +403,10 @@ export function evaluatePredicate(actual: string | null, pred: PredicateSpec): b
|
||||
case ">=": {
|
||||
const a = Number(actual);
|
||||
const b = Number(pred.value);
|
||||
if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
|
||||
if (pred.op === "<") return a < b;
|
||||
if (pred.op === "<=") return a <= b;
|
||||
if (pred.op === ">") return a > b;
|
||||
if (!Number.isFinite(a) || !Number.isFinite(b)) {return false;}
|
||||
if (pred.op === "<") {return a < b;}
|
||||
if (pred.op === "<=") {return a <= b;}
|
||||
if (pred.op === ">") {return a > b;}
|
||||
return a >= b;
|
||||
}
|
||||
}
|
||||
@@ -464,13 +464,13 @@ export function repackPath(pattern: OcPath, subs: readonly string[]): OcPath {
|
||||
}
|
||||
|
||||
function extractSession(queryPart: string): string | undefined {
|
||||
if (queryPart.length === 0) return undefined;
|
||||
if (queryPart.length === 0) {return undefined;}
|
||||
for (const pair of queryPart.split("&")) {
|
||||
const eqIndex = pair.indexOf("=");
|
||||
if (eqIndex === -1) continue;
|
||||
if (eqIndex === -1) {continue;}
|
||||
const key = pair.slice(0, eqIndex);
|
||||
const value = pair.slice(eqIndex + 1);
|
||||
if (key === "session" && value.length > 0) return value;
|
||||
if (key === "session" && value.length > 0) {return value;}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -486,23 +486,23 @@ function scanBracketAware(s: string, onChar: ScanCallback, onUnbalanced: () => n
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const c = s[i];
|
||||
if (inQuote) {
|
||||
if (c === '"') inQuote = false;
|
||||
if (onChar(c, i, false) === "stop") return;
|
||||
if (c === '"') {inQuote = false;}
|
||||
if (onChar(c, i, false) === "stop") {return;}
|
||||
continue;
|
||||
}
|
||||
if (c === '"') {
|
||||
inQuote = true;
|
||||
if (onChar(c, i, false) === "stop") return;
|
||||
if (onChar(c, i, false) === "stop") {return;}
|
||||
continue;
|
||||
}
|
||||
if (c === "[") depthBracket++;
|
||||
else if (c === "]") depthBracket--;
|
||||
else if (c === "{") depthBrace++;
|
||||
else if (c === "}") depthBrace--;
|
||||
if (depthBracket < 0 || depthBrace < 0) onUnbalanced();
|
||||
if (onChar(c, i, depthBracket === 0 && depthBrace === 0) === "stop") return;
|
||||
if (c === "[") {depthBracket++;}
|
||||
else if (c === "]") {depthBracket--;}
|
||||
else if (c === "{") {depthBrace++;}
|
||||
else if (c === "}") {depthBrace--;}
|
||||
if (depthBracket < 0 || depthBrace < 0) {onUnbalanced();}
|
||||
if (onChar(c, i, depthBracket === 0 && depthBrace === 0) === "stop") {return;}
|
||||
}
|
||||
if (depthBracket !== 0 || depthBrace !== 0 || inQuote) onUnbalanced();
|
||||
if (depthBracket !== 0 || depthBrace !== 0 || inQuote) {onUnbalanced();}
|
||||
}
|
||||
|
||||
/** First top-level occurrence of `ch` in `s`; -1 when absent. */
|
||||
@@ -563,7 +563,7 @@ export function unquoteSeg(seg: string): string {
|
||||
|
||||
// Refuses values with `"` or `\` — no escape mechanism.
|
||||
export function quoteSeg(value: string): string {
|
||||
if (value.length === 0) return '""';
|
||||
if (value.length === 0) {return '""';}
|
||||
if (value.includes('"') || value.includes("\\")) {
|
||||
fail(
|
||||
`Cannot quote value containing '"' or '\\\\': ${printable(value)}`,
|
||||
@@ -607,8 +607,8 @@ function validateSubSegment(sub: string, input: string): void {
|
||||
}
|
||||
// Quoted content is byte-literal but can't contain `"` or `\`.
|
||||
if (isQuotedSeg(sub)) {
|
||||
const inner = sub.slice(1, -1);
|
||||
if (inner.includes('"') || inner.includes("\\")) {
|
||||
const inner = new Set(sub.slice(1, -1));
|
||||
if (inner.has('"') || inner.has("\\")) {
|
||||
fail(
|
||||
`Quoted segment cannot contain '"' or '\\\\': ${printable(sub)}`,
|
||||
input,
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
AstItem,
|
||||
Diagnostic,
|
||||
FrontmatterEntry,
|
||||
MdAst,
|
||||
ParseResult,
|
||||
} from "./ast.js";
|
||||
import { slugify } from "./slug.js";
|
||||
@@ -154,7 +153,7 @@ function extractItems(tokens: readonly Token[], bodyFileLine: number): AstItem[]
|
||||
const items: AstItem[] = [];
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const t = tokens[i];
|
||||
if (t.type !== "list_item_open" || t.map === null) continue;
|
||||
if (t.type !== "list_item_open" || t.map === null) {continue;}
|
||||
// First inline at the item's own depth is the item text.
|
||||
let nestedDepth = 0;
|
||||
let text = "";
|
||||
|
||||
@@ -35,39 +35,39 @@ export type OcPathMatch =
|
||||
export function resolveMdOcPath(ast: MdAst, path: OcPath): OcPathMatch | null {
|
||||
if (path.section === "[frontmatter]") {
|
||||
const key = path.item ?? path.field;
|
||||
if (key === undefined) return null;
|
||||
if (key === undefined) {return null;}
|
||||
const entry = ast.frontmatter.find((e) => e.key === key);
|
||||
if (entry === undefined) return null;
|
||||
if (entry === undefined) {return null;}
|
||||
return { kind: "frontmatter", node: entry };
|
||||
}
|
||||
|
||||
if (path.section === undefined) return { kind: "root", node: ast };
|
||||
if (path.section === undefined) {return { kind: "root", node: ast };}
|
||||
|
||||
const block = ast.blocks.find((b) => b.slug === path.section!.toLowerCase());
|
||||
if (block === undefined) return null;
|
||||
if (path.item === undefined) return { kind: "block", node: block };
|
||||
if (block === undefined) {return null;}
|
||||
if (path.item === undefined) {return { kind: "block", node: block };}
|
||||
|
||||
// Item dispatch: ordinal (#N) > positional ($first/$last/-N) > slug.
|
||||
// Ordinal uses document order so duplicate-slug items stay distinct.
|
||||
let item: AstItem | undefined;
|
||||
if (isOrdinalSeg(path.item)) {
|
||||
const n = parseOrdinalSeg(path.item);
|
||||
if (n === null || n < 0 || n >= block.items.length) return null;
|
||||
if (n === null || n < 0 || n >= block.items.length) {return null;}
|
||||
item = block.items[n];
|
||||
} else if (isPositionalSeg(path.item)) {
|
||||
const concrete = resolvePositionalSeg(path.item, {
|
||||
indexable: true,
|
||||
size: block.items.length,
|
||||
});
|
||||
if (concrete === null) return null;
|
||||
if (concrete === null) {return null;}
|
||||
item = block.items[Number(concrete)];
|
||||
} else {
|
||||
item = block.items.find((i) => i.slug === path.item!.toLowerCase());
|
||||
}
|
||||
if (item === undefined) return null;
|
||||
if (path.field === undefined) return { kind: "item", node: item, block };
|
||||
if (item === undefined) {return null;}
|
||||
if (path.field === undefined) {return { kind: "item", node: item, block };}
|
||||
|
||||
if (item.kv === undefined) return null;
|
||||
if (item.kv.key.toLowerCase() !== path.field.toLowerCase()) return null;
|
||||
if (item.kv === undefined) {return null;}
|
||||
if (item.kv.key.toLowerCase() !== path.field.toLowerCase()) {return null;}
|
||||
return { kind: "item-field", node: item, block, value: item.kv.value };
|
||||
}
|
||||
|
||||
@@ -99,13 +99,13 @@ export interface InsertionInfo {
|
||||
|
||||
export function detectInsertion(path: OcPath): InsertionInfo | null {
|
||||
const segments: Array<{ slot: "section" | "item" | "field"; value: string }> = [];
|
||||
if (path.section !== undefined) segments.push({ slot: "section", value: path.section });
|
||||
if (path.item !== undefined) segments.push({ slot: "item", value: path.item });
|
||||
if (path.field !== undefined) segments.push({ slot: "field", value: path.field });
|
||||
if (segments.length === 0) return null;
|
||||
if (path.section !== undefined) {segments.push({ slot: "section", value: path.section });}
|
||||
if (path.item !== undefined) {segments.push({ slot: "item", value: path.item });}
|
||||
if (path.field !== undefined) {segments.push({ slot: "field", value: path.field });}
|
||||
if (segments.length === 0) {return null;}
|
||||
|
||||
const last = segments[segments.length - 1];
|
||||
if (!last.value.startsWith("+")) return null;
|
||||
if (!last.value.startsWith("+")) {return null;}
|
||||
|
||||
const rest = last.value.slice(1);
|
||||
const marker: InsertionInfo["marker"] =
|
||||
@@ -137,7 +137,7 @@ export function resolveOcPath(ast: OcAst, path: OcPath): OcMatch | null {
|
||||
);
|
||||
}
|
||||
const insertion = detectInsertion(path);
|
||||
if (insertion !== null) return resolveInsertion(ast, insertion);
|
||||
if (insertion !== null) {return resolveInsertion(ast, insertion);}
|
||||
|
||||
switch (ast.kind) {
|
||||
case "md":
|
||||
@@ -151,7 +151,7 @@ export function resolveOcPath(ast: OcAst, path: OcPath): OcMatch | null {
|
||||
|
||||
function resolveMdToUniversal(ast: MdAst, path: OcPath): OcMatch | null {
|
||||
const m = resolveMdOcPath(ast, path);
|
||||
if (m === null) return null;
|
||||
if (m === null) {return null;}
|
||||
switch (m.kind) {
|
||||
case "root":
|
||||
return { kind: "root", ast, line: 1 };
|
||||
@@ -168,9 +168,9 @@ function resolveMdToUniversal(ast: MdAst, path: OcPath): OcMatch | null {
|
||||
|
||||
function resolveJsoncToUniversal(ast: JsoncAst, path: OcPath): OcMatch | null {
|
||||
const m = resolveJsoncOcPath(ast, path);
|
||||
if (m === null) return null;
|
||||
if (m.kind === "root") return { kind: "root", ast, line: 1 };
|
||||
if (m.kind === "object-entry") return jsoncValueToMatch(m.node.value, m.node.line);
|
||||
if (m === null) {return null;}
|
||||
if (m.kind === "root") {return { kind: "root", ast, line: 1 };}
|
||||
if (m.kind === "object-entry") {return jsoncValueToMatch(m.node.value, m.node.line);}
|
||||
return jsoncValueToMatch(m.node, m.node.line ?? 1);
|
||||
}
|
||||
|
||||
@@ -193,12 +193,12 @@ function jsoncValueToMatch(value: JsoncValue, line: number): OcMatch {
|
||||
|
||||
function resolveJsonlToUniversal(ast: JsonlAst, path: OcPath): OcMatch | null {
|
||||
const m = resolveJsonlOcPath(ast, path);
|
||||
if (m === null) return null;
|
||||
if (m.kind === "root") return { kind: "root", ast, line: 1 };
|
||||
if (m.kind === "line") return { kind: "node", descriptor: "jsonl-line", line: m.node.line };
|
||||
if (m === null) {return null;}
|
||||
if (m.kind === "root") {return { kind: "root", ast, line: 1 };}
|
||||
if (m.kind === "line") {return { kind: "node", descriptor: "jsonl-line", line: m.node.line };}
|
||||
// Inside-line jsonc nodes always have line=1; use the JsonlLine's
|
||||
// file-level line instead since every inside-line node sits there.
|
||||
if (m.kind === "object-entry") return jsoncValueToMatch(m.node.value, m.line);
|
||||
if (m.kind === "object-entry") {return jsoncValueToMatch(m.node.value, m.line);}
|
||||
return jsoncValueToMatch(m.node, m.line);
|
||||
}
|
||||
|
||||
@@ -215,13 +215,13 @@ function resolveInsertion(ast: OcAst, info: InsertionInfo): OcMatch | null {
|
||||
|
||||
function resolveMdInsertion(ast: MdAst, info: InsertionInfo): OcMatch | null {
|
||||
const p = info.parentPath;
|
||||
if (p.section === undefined) return { kind: "insertion-point", container: "md-file", line: 1 };
|
||||
if (p.section === undefined) {return { kind: "insertion-point", container: "md-file", line: 1 };}
|
||||
if (p.section === "[frontmatter]") {
|
||||
return { kind: "insertion-point", container: "md-frontmatter", line: 1 };
|
||||
}
|
||||
if (p.item === undefined && p.field === undefined) {
|
||||
const m = resolveMdOcPath(ast, p);
|
||||
if (m === null || m.kind !== "block") return null;
|
||||
if (m === null || m.kind !== "block") {return null;}
|
||||
return { kind: "insertion-point", container: "md-section", line: m.node.line };
|
||||
}
|
||||
return null;
|
||||
@@ -256,7 +256,7 @@ function resolveJsoncInsertion(ast: JsoncAst, info: InsertionInfo): OcMatch | nu
|
||||
function resolveJsonlInsertion(ast: JsonlAst, info: InsertionInfo): OcMatch | null {
|
||||
// jsonl insertion only makes sense at file level (`oc://FILE/+`).
|
||||
// Surfaced line is lastLine+1 so consumers render correctly.
|
||||
if (info.parentPath.section !== undefined) return null;
|
||||
if (info.parentPath.section !== undefined) {return null;}
|
||||
const lastLine = ast.lines.length > 0 ? ast.lines[ast.lines.length - 1].line : 0;
|
||||
return { kind: "insertion-point", container: "jsonl-file", line: lastLine + 1 };
|
||||
}
|
||||
@@ -317,7 +317,7 @@ function setStructuredLeaf<A extends OcAst, M extends StructuredLeafMatch>(
|
||||
onLine?: () => SetResult,
|
||||
): SetResult {
|
||||
const existing = resolve(ast, path);
|
||||
if (existing === null) return { ok: false, reason: "unresolved" };
|
||||
if (existing === null) {return { ok: false, reason: "unresolved" };}
|
||||
if (existing.kind === "root") {
|
||||
return { ok: false, reason: "not-writable", detail: "root replacement is not supported via setOcPath" };
|
||||
}
|
||||
@@ -445,9 +445,9 @@ function setJsoncInsertion(ast: JsoncAst, info: InsertionInfo, value: string): S
|
||||
return { ok: false, reason: "type-mismatch", detail: "cannot insert by key into array" };
|
||||
}
|
||||
return mutateJsoncContainer(ast, info.parentPath, (container) => {
|
||||
if (container.kind !== "array") return null;
|
||||
if (container.kind !== "array") {return null;}
|
||||
const items = container.items.slice();
|
||||
if (info.marker === "+") items.push(newJsoncValue);
|
||||
if (info.marker === "+") {items.push(newJsoncValue);}
|
||||
else if (typeof info.marker === "object" && info.marker.kind === "indexed") {
|
||||
const idx = Math.min(info.marker.index, items.length);
|
||||
items.splice(idx, 0, newJsoncValue);
|
||||
@@ -465,8 +465,8 @@ function setJsoncInsertion(ast: JsoncAst, info: InsertionInfo, value: string): S
|
||||
}
|
||||
const key = info.marker.key;
|
||||
return mutateJsoncContainer(ast, info.parentPath, (container) => {
|
||||
if (container.kind !== "object") return null;
|
||||
if (container.entries.some((e) => e.key === key)) return null; // duplicate
|
||||
if (container.kind !== "object") {return null;}
|
||||
if (container.entries.some((e) => e.key === key)) {return null;} // duplicate
|
||||
const newEntry: JsoncEntry = { key, value: newJsoncValue, line: 0 };
|
||||
return {
|
||||
kind: "object",
|
||||
@@ -495,14 +495,14 @@ function setJsonlInsertion(ast: JsonlAst, info: InsertionInfo, value: string): S
|
||||
// semantic node, only the bytes change.
|
||||
function coerceJsoncLeaf(valueText: string, existing: JsoncValue): JsoncValue | null {
|
||||
const lineExt = existing.line !== undefined ? { line: existing.line } : {};
|
||||
if (existing.kind === "string") return { kind: "string", value: valueText, ...lineExt };
|
||||
if (existing.kind === "string") {return { kind: "string", value: valueText, ...lineExt };}
|
||||
if (existing.kind === "number") {
|
||||
const n = Number(valueText);
|
||||
return Number.isFinite(n) ? { kind: "number", value: n, ...lineExt } : null;
|
||||
}
|
||||
if (existing.kind === "boolean") {
|
||||
if (valueText === "true") return { kind: "boolean", value: true, ...lineExt };
|
||||
if (valueText === "false") return { kind: "boolean", value: false, ...lineExt };
|
||||
if (valueText === "true") {return { kind: "boolean", value: true, ...lineExt };}
|
||||
if (valueText === "false") {return { kind: "boolean", value: false, ...lineExt };}
|
||||
return null;
|
||||
}
|
||||
if (existing.kind === "null") {
|
||||
@@ -522,10 +522,10 @@ function tryParseJson(value: string): unknown {
|
||||
|
||||
function jsonToJsoncValue(v: unknown): JsoncValue {
|
||||
// Synthetic values omit `line` — only the parser sets line metadata.
|
||||
if (v === null) return { kind: "null" };
|
||||
if (typeof v === "string") return { kind: "string", value: v };
|
||||
if (typeof v === "number") return { kind: "number", value: v };
|
||||
if (typeof v === "boolean") return { kind: "boolean", value: v };
|
||||
if (v === null) {return { kind: "null" };}
|
||||
if (typeof v === "string") {return { kind: "string", value: v };}
|
||||
if (typeof v === "number") {return { kind: "number", value: v };}
|
||||
if (typeof v === "boolean") {return { kind: "boolean", value: v };}
|
||||
if (Array.isArray(v)) {
|
||||
return { kind: "array", items: v.map(jsonToJsoncValue) };
|
||||
}
|
||||
@@ -549,7 +549,7 @@ function mutateJsoncContainer(
|
||||
parentPath: OcPath,
|
||||
mutate: (container: JsoncValue) => JsoncValue | null,
|
||||
): SetResult {
|
||||
if (ast.root === null) return { ok: false, reason: "no-root" };
|
||||
if (ast.root === null) {return { ok: false, reason: "no-root" };}
|
||||
|
||||
// Quote-aware split so insertion under a key with `/`/`.`/etc. works.
|
||||
const segments: string[] = [];
|
||||
@@ -565,7 +565,7 @@ function mutateJsoncContainer(
|
||||
|
||||
const newRoot =
|
||||
segments.length === 0 ? mutate(ast.root) : mutateAt(ast.root, segments, 0, mutate);
|
||||
if (newRoot === null) return { ok: false, reason: "unresolved" };
|
||||
if (newRoot === null) {return { ok: false, reason: "unresolved" };}
|
||||
|
||||
const next: JsoncAst = { kind: "jsonc", raw: "", root: newRoot };
|
||||
return { ok: true, ast: { ...next, raw: emitJsonc(next, { mode: "render" }) } };
|
||||
@@ -578,17 +578,17 @@ function mutateAt(
|
||||
mutate: (container: JsoncValue) => JsoncValue | null,
|
||||
): JsoncValue | null {
|
||||
const seg = segments[i];
|
||||
if (seg === undefined) return mutate(current);
|
||||
if (seg.length === 0) return null;
|
||||
if (seg === undefined) {return mutate(current);}
|
||||
if (seg.length === 0) {return null;}
|
||||
|
||||
if (current.kind === "object") {
|
||||
// AST keys are unquoted; strip quotes from the path segment.
|
||||
const lookupKey = isQuotedSeg(seg) ? unquoteSeg(seg) : seg;
|
||||
const idx = current.entries.findIndex((e) => e.key === lookupKey);
|
||||
if (idx === -1) return null;
|
||||
if (idx === -1) {return null;}
|
||||
const child = current.entries[idx];
|
||||
const replaced = mutateAt(child.value, segments, i + 1, mutate);
|
||||
if (replaced === null) return null;
|
||||
if (replaced === null) {return null;}
|
||||
const newEntries = current.entries.slice();
|
||||
newEntries[idx] = { ...child, value: replaced };
|
||||
return {
|
||||
@@ -599,10 +599,10 @@ function mutateAt(
|
||||
}
|
||||
if (current.kind === "array") {
|
||||
const idx = Number(seg);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= current.items.length) return null;
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= current.items.length) {return null;}
|
||||
const child = current.items[idx];
|
||||
const replaced = mutateAt(child, segments, i + 1, mutate);
|
||||
if (replaced === null) return null;
|
||||
if (replaced === null) {return null;}
|
||||
const newItems = current.items.slice();
|
||||
newItems[idx] = replaced;
|
||||
return {
|
||||
@@ -624,21 +624,21 @@ function rebuildMdRaw(ast: MdAst): MdAst {
|
||||
parts.push("---");
|
||||
}
|
||||
if (ast.preamble.length > 0) {
|
||||
if (parts.length > 0) parts.push("");
|
||||
if (parts.length > 0) {parts.push("");}
|
||||
parts.push(ast.preamble);
|
||||
}
|
||||
for (const block of ast.blocks) {
|
||||
if (parts.length > 0) parts.push("");
|
||||
if (parts.length > 0) {parts.push("");}
|
||||
parts.push(`## ${block.heading}`);
|
||||
if (block.bodyText.length > 0) parts.push(block.bodyText);
|
||||
if (block.bodyText.length > 0) {parts.push(block.bodyText);}
|
||||
}
|
||||
void emitJsonl;
|
||||
return { ...ast, raw: parts.join("\n") };
|
||||
}
|
||||
|
||||
function formatFrontmatterValue(value: string): string {
|
||||
if (value.length === 0) return '""';
|
||||
if (/[:#&*?|<>=!%@`,[\]{}\r\n]/.test(value)) return JSON.stringify(value);
|
||||
if (value.length === 0) {return '""';}
|
||||
if (/[:#&*?|<>=!%@`,[\]{}\r\n]/.test(value)) {return JSON.stringify(value);}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user