yamlover 0.3.22 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -66856,64 +66856,17 @@ function createHandlers(dataRoot, opts = {}) {
66856
66856
  (data) => enqueue(async () => {
66857
66857
  const d = data;
66858
66858
  const edits = Array.isArray(d.edits) ? d.edits : [d];
66859
- const touched = applyEdits2(dataRoot, s2, edits);
66860
- for (const f2 of touched) broadcast(await doReindexFile(f2));
66859
+ const { touched, created, appended } = applyEdits2(dataRoot, s2, edits);
66860
+ if (created.length) broadcast(await doReindex());
66861
+ else for (const f2 of touched) broadcast(await doReindexFile(f2));
66861
66862
  scheduleHasher();
66863
+ if (created.length) return { ok: true, path: created[created.length - 1] };
66864
+ if (appended.length) return { ok: true, path: lastBodyChildPath(s2, appended[appended.length - 1]) };
66862
66865
  return { ok: true };
66863
66866
  })
66864
66867
  ).then((body) => sendJson(res, 200, body)).catch((e3) => sendJson(res, 400, { error: String(e3.message || e3) }));
66865
66868
  return;
66866
66869
  }
66867
- if (req.method === "POST" && url.pathname === "/api/create") {
66868
- readBody(req).then(
66869
- (data) => enqueue(async () => {
66870
- const b = data;
66871
- const reg = CREATABLE[String(b.schema ?? "")];
66872
- if (!reg) throw new Error(`unknown schema: ${b.schema}`);
66873
- const concrete = String(b.concrete ?? "");
66874
- const parentSegs = strToSegs(b.parent ?? ":");
66875
- const row2 = s2.node(storePath(parentSegs));
66876
- if (!row2) throw new Error(`no such node: ${b.parent}`);
66877
- const title = String(b.title ?? "").trim() || defaultTitle(String(b.schema));
66878
- const body = reg.body(title);
66879
- const objSrc = objectFileSource(reg.tag, body);
66880
- if (reg.childOf.includes(row2.format ?? "")) {
66881
- const { docSegs, bodyFile, dirBacked } = chapterSource(dataRoot, s2, parentSegs);
66882
- const within = parentSegs.slice(docSegs.length);
66883
- if (concrete === "yamlover") {
66884
- const src = fs10.readFileSync(bodyFile, "utf8");
66885
- fs10.writeFileSync(bodyFile, appendBody(src, within, (indent) => inlineChildLines(body, indent)));
66886
- broadcast(await doReindexFile(bodyFile));
66887
- scheduleHasher();
66888
- return { path: lastBodyChildPath(s2, parentSegs) };
66889
- }
66890
- if (concrete === "file/yamlover" || concrete === "dir/yamlover") {
66891
- const writeDirSegs = dirBacked ? docSegs : docSegs.slice(0, -1);
66892
- const writeDir = path10.resolve(dataRoot, ...writeDirSegs.map(String));
66893
- const final = writeObject(dataRoot, writeDir, sanitizeName(title), concrete, objSrc);
66894
- const targetSegs = [...writeDirSegs, final];
66895
- const pointer = dirBacked ? `*/${final}` : `*/${segsToStr(targetSegs)}`;
66896
- const src = fs10.readFileSync(bodyFile, "utf8");
66897
- fs10.writeFileSync(bodyFile, appendBody(src, within, (indent) => [`${" ".repeat(indent)}- ${pointer}`]));
66898
- broadcast(await doReindex());
66899
- scheduleHasher();
66900
- return { path: segsToStr(targetSegs) };
66901
- }
66902
- throw new Error(`invalid concrete for a child: ${concrete}`);
66903
- }
66904
- const abs = path10.resolve(dataRoot, ...parentSegs.map(String));
66905
- if (parentSegs.every((g) => typeof g === "string") && fs10.existsSync(abs) && fs10.statSync(abs).isDirectory()) {
66906
- if (concrete !== "file/yamlover" && concrete !== "dir/yamlover") throw new Error(`invalid concrete for a directory member: ${concrete}`);
66907
- const final = writeObject(dataRoot, abs, objectBaseName(title), concrete, objSrc);
66908
- broadcast(await doReindex());
66909
- scheduleHasher();
66910
- return { path: segsToStr([...parentSegs, final]) };
66911
- }
66912
- throw new Error("this schema can be created only inside a directory or a compatible parent");
66913
- })
66914
- ).then((body) => sendJson(res, 201, body)).catch((e3) => sendJson(res, 400, { error: String(e3.message || e3) }));
66915
- return;
66916
- }
66917
66870
  if (req.method === "POST" && url.pathname === "/api/mv") {
66918
66871
  readBody(req).then(
66919
66872
  (data) => enqueue(async () => {
@@ -67694,13 +67647,13 @@ function handlePaste(dataRoot, s2, input) {
67694
67647
  const bytes = Buffer.from(input.contentBase64 || "", "base64");
67695
67648
  if (bytes.length === 0) throw new Error("empty paste (no file bytes)");
67696
67649
  const name = sanitizeName(input.filename ?? "");
67697
- if (row.format === "x-yamlover-chapter") return pasteIntoChapter(dataRoot, s2, segs, name, bytes);
67650
+ if (row.format === "x-yamlover-chapter" && !input.inline) return pasteIntoChapter(dataRoot, s2, segs, name, bytes);
67698
67651
  const dirSegs = nearestDirSegs(dataRoot, segs);
67699
67652
  if (!dirSegs) throw new Error("no enclosing directory to paste into");
67700
67653
  const dir = path10.resolve(dataRoot, ...dirSegs.map(String));
67701
67654
  const final = uniqueName(dir, name);
67702
67655
  writeInside(dataRoot, dir, final, bytes);
67703
- return { path: segsToStr([...dirSegs, final]), dir: segsToStr(dirSegs), open: dirSegs.length !== segs.length };
67656
+ return { path: segsToStr([...dirSegs, final]), dir: segsToStr(dirSegs), open: !input.inline && dirSegs.length !== segs.length };
67704
67657
  }
67705
67658
  function nearestDirSegs(dataRoot, segs) {
67706
67659
  for (let i2 = segs.length; i2 >= 0; i2--) {
@@ -67850,32 +67803,11 @@ function titleFromText(text) {
67850
67803
  const t3 = first.replace(/^#{1,6}\s+/, "").trim();
67851
67804
  return (t3.length > 80 ? t3.slice(0, 79).trimEnd() + "\u2026" : t3) || "Pasted text";
67852
67805
  }
67853
- var CREATABLE = {
67854
- "::yamlover:$defs:chapter": {
67855
- tag: "*::yamlover:$defs:chapter",
67856
- childOf: ["x-yamlover-chapter", "x-yamlover-task"],
67857
- // a chapter with one empty prose chunk (a positional body item), so it is immediately editable
67858
- body: (title) => [`title: ${JSON.stringify(title)}`, `- ""`]
67859
- }
67860
- };
67861
67806
  function lastBodyChildPath(s2, parentSegs) {
67862
67807
  const positional = s2.entries(storePath(parentSegs)).filter((e3) => e3.kind === "contain" && e3.label == null);
67863
67808
  const last = positional[positional.length - 1];
67864
67809
  return last ? segsToStr(storePathToSegs(last.to)) : segsToStr([...parentSegs, 0]);
67865
67810
  }
67866
- function defaultTitle(schema) {
67867
- const segs = strToSegs(schema);
67868
- return "New " + String(segs[segs.length - 1] ?? "object");
67869
- }
67870
- function objectFileSource(tag, body) {
67871
- return `!!<${tag}>
67872
- ${body.join("\n")}
67873
- `;
67874
- }
67875
- function inlineChildLines(body, indent) {
67876
- const pad = " ".repeat(indent);
67877
- return body.map((line, i2) => i2 === 0 ? `${pad}- ${line}` : `${pad} ${line}`);
67878
- }
67879
67811
  function objectBaseName(title) {
67880
67812
  const base = title.replace(/[^\p{L}\p{N} ._-]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, 60).trim().replace(/^\.+/, "");
67881
67813
  return base || "new";
@@ -67952,13 +67884,20 @@ var isContentLine2 = (line) => {
67952
67884
  const t3 = line.trim();
67953
67885
  return t3.length > 0 && !t3.startsWith("#");
67954
67886
  };
67955
- function textChunkLines(text, indent) {
67956
- const pad = " ".repeat(indent);
67887
+ function escapeScalarSrc(text) {
67957
67888
  const first = text.split("\n").find((l2) => l2.trim().length > 0);
67958
- if (!first || /^\s/.test(first)) return [`${pad}- ${JSON.stringify(text)}`];
67889
+ if (!first || /^\s/.test(first)) return JSON.stringify(text);
67959
67890
  const body = text.endsWith("\n") ? text.slice(0, -1) : text;
67960
67891
  const head = text.endsWith("\n") ? "|" : "|-";
67961
- return [`${pad}- ${head}`, ...body.split("\n").map((l2) => l2.trim().length ? `${pad} ${l2}` : "")];
67892
+ return [head, ...body.split("\n").map((l2) => l2.trim().length ? " " + l2 : "")].join("\n");
67893
+ }
67894
+ function renderEntry(valueSrc, indent, marker, tag, deeper = false) {
67895
+ const pad = " ".repeat(indent + (deeper ? 2 : 0));
67896
+ const [first, ...rest] = valueSrc.split("\n");
67897
+ return [`${" ".repeat(indent)}${marker}${tag ? tag + " " : ""}${first}`, ...rest.map((l2) => l2.trim().length ? pad + l2 : "")];
67898
+ }
67899
+ function textChunkLines(text, indent) {
67900
+ return renderEntry(escapeScalarSrc(text), indent, "- ");
67962
67901
  }
67963
67902
  function chapterEntries(lines, r2) {
67964
67903
  const starts = [];
@@ -67983,16 +67922,36 @@ function chapterEntries(lines, r2) {
67983
67922
  inline: s2.inline
67984
67923
  }));
67985
67924
  }
67986
- function bodyItems(lines, r2) {
67987
- return chapterEntries(lines, r2).filter((e3) => e3.key === null);
67925
+ function findEntry(lines, r2, seg) {
67926
+ const entries = chapterEntries(lines, r2);
67927
+ return typeof seg === "number" ? entries[seg] : entries.find((e3) => e3.key === seg);
67928
+ }
67929
+ function entryHead(lines, e3) {
67930
+ let t3 = lines[e3.start].trim().replace(/^-\s*/, "").replace(/^!!<[^>]*>\s*/, "");
67931
+ if (e3.key) t3 = t3.slice(t3.indexOf(":") + 1).trim();
67932
+ return t3;
67933
+ }
67934
+ function isContainerEntry(lines, e3, childIndent) {
67935
+ const head = entryHead(lines, e3);
67936
+ if (/^[|>][+-]?\d*$/.test(head.trim())) return itemHasFields(lines, e3, childIndent);
67937
+ if (/^[^\s"'*|>#-][^:]*:(\s|$)/.test(head)) return true;
67938
+ for (let i2 = e3.start + 1; i2 < e3.end; i2++) {
67939
+ if (!isContentLine2(lines[i2])) continue;
67940
+ const ind = indentOf2(lines[i2]);
67941
+ if (ind < childIndent) break;
67942
+ if (ind === childIndent) return true;
67943
+ }
67944
+ return false;
67988
67945
  }
67989
- function reachChapter(lines, chapterPath) {
67946
+ function reachChapter(lines, within) {
67990
67947
  let r2 = { lo: 0, hi: lines.length, indent: firstContentIndent2(lines), marker: -1 };
67991
- for (const seg of chapterPath) {
67992
- const idx = Number(seg);
67993
- const item = chapterEntries(lines, r2)[idx];
67994
- if (!item || item.key !== null) throw new Error(`no subchapter at [${idx}]`);
67995
- r2 = { lo: item.start + 1, hi: item.end, indent: r2.indent + 2, marker: item.start };
67948
+ for (const seg of within) {
67949
+ const entry = findEntry(lines, r2, seg);
67950
+ if (!entry) throw new Error(`no entry at ${typeof seg === "number" ? `[${seg}]` : seg}`);
67951
+ if (!isContainerEntry(lines, entry, r2.indent + 2)) {
67952
+ throw new Error(`cannot descend into a scalar element at ${typeof seg === "number" ? `[${seg}]` : seg}`);
67953
+ }
67954
+ r2 = { lo: entry.start + 1, hi: entry.end, indent: r2.indent + 2, marker: entry.start };
67996
67955
  }
67997
67956
  return r2;
67998
67957
  }
@@ -68009,60 +67968,106 @@ function appendBody(text, chapterPath, renderItems) {
68009
67968
  lines.splice(bodyAppendPoint(lines, r2), 0, ...renderItems(r2.indent));
68010
67969
  return lines.join("\n");
68011
67970
  }
68012
- function bodyItemHead(lines, r2, rank) {
68013
- const items = bodyItems(lines, r2);
68014
- if (!(rank >= 0 && rank < items.length)) throw new Error(`body[${rank}] out of range (${items.length})`);
68015
- return lines[items[rank].start].trim().replace(/^-\s*/, "");
67971
+ var opensEntry = (t3) => t3 === "-" || t3.startsWith("- ") || /^[^\s"'*|>#-][^:]*:(\s|$)/.test(t3);
67972
+ var isBlockHeader = (head) => /^[|>][+-]?\d*$/.test(head.trim());
67973
+ function groupEntries(lines, at) {
67974
+ const starts = lines.map((l2, i2) => i2).filter((i2) => isContentLine2(lines[i2]) && indentOf2(lines[i2]) === at);
67975
+ const keyed = [];
67976
+ const ordinal = [];
67977
+ starts.forEach((s2, k) => {
67978
+ const group = lines.slice(s2, k + 1 < starts.length ? starts[k + 1] : lines.length).map((l2) => l2.slice(at));
67979
+ (lines[s2].trim() === "-" || lines[s2].trim().startsWith("- ") ? ordinal : keyed).push(group);
67980
+ });
67981
+ return { keyed, ordinal };
67982
+ }
67983
+ function payloadFacets(src) {
67984
+ const lines = src.split("\n");
67985
+ const first = lines.find(isContentLine2);
67986
+ if (first === void 0) return { keyed: [], ordinal: [] };
67987
+ if (!opensEntry(first.trim())) return { scalar: src, keyed: [], ordinal: [] };
67988
+ return { scalar: void 0, ...groupEntries(lines, 0) };
67989
+ }
67990
+ function entryFacets(lines, e3, indent) {
67991
+ const marked = lines[e3.start].trim().replace(/^-\s*/, "");
67992
+ const tag = marked.match(/^(!!<[^>]*>)/)?.[1];
67993
+ const head = entryHead(lines, e3);
67994
+ const childIndent = indent + 2;
67995
+ const value = (from, to2, at) => [head, ...lines.slice(from, to2).map((l2) => l2.trim().length ? " " + l2.slice(at) : "")].join("\n").replace(/\n+$/, "");
67996
+ if (!isContainerEntry(lines, e3, childIndent)) {
67997
+ return { tag, scalar: value(e3.start + 1, e3.end, childIndent), keyed: [], ordinal: [] };
67998
+ }
67999
+ const blockValue = isBlockHeader(head);
68000
+ let firstChild = e3.end;
68001
+ for (let i2 = e3.start + 1; i2 < e3.end; i2++) {
68002
+ if (isContentLine2(lines[i2]) && indentOf2(lines[i2]) === childIndent) {
68003
+ firstChild = i2;
68004
+ break;
68005
+ }
68006
+ }
68007
+ const scalar2 = blockValue ? value(e3.start + 1, firstChild, childIndent + 2) : void 0;
68008
+ const groups = groupEntries(lines.slice(firstChild, e3.end), childIndent);
68009
+ if (!blockValue && head) groups.keyed.unshift([head]);
68010
+ return { tag, scalar: scalar2, ...groups };
68016
68011
  }
68017
- function setScalarKey(lines, r2, key, value) {
68018
- const entry = chapterEntries(lines, r2).find((e3) => e3.key === key);
68019
- if (entry?.inline) {
68020
- const pad3 = " ".repeat(indentOf2(lines[entry.start]));
68021
- lines[entry.start] = `${pad3}- ${key}: ${JSON.stringify(value)}`;
68012
+ function renderNode(f2, indent, marker, tag) {
68013
+ const pad = " ".repeat(indent);
68014
+ const childPad = " ".repeat(indent + 2);
68015
+ const groups = [...f2.keyed, ...f2.ordinal];
68016
+ const child = (g) => g.map((l2) => l2.trim().length ? childPad + l2 : "");
68017
+ if (f2.scalar !== void 0) {
68018
+ return [...renderEntry(f2.scalar, indent, marker, tag, groups.length > 0), ...groups.flatMap(child)];
68019
+ }
68020
+ if (!groups.length) return [`${pad}${marker.trimEnd()}${tag ? " " + tag : ""}`];
68021
+ if (marker.startsWith("-") && !tag) {
68022
+ const [g0, ...rest] = groups;
68023
+ return [`${pad}${marker}${g0[0]}`, ...child(g0.slice(1)), ...rest.flatMap(child)];
68024
+ }
68025
+ return [`${pad}${marker.trimEnd()}${tag ? " " + tag : ""}`, ...groups.flatMap(child)];
68026
+ }
68027
+ function metaTag(meta, existing, preserve) {
68028
+ if (meta === null) return void 0;
68029
+ if (meta !== void 0) return `!!<${meta}>`;
68030
+ return preserve ? existing : void 0;
68031
+ }
68032
+ function assignAt(lines, r2, seg, op, valueSrc, meta) {
68033
+ const marker = (s2) => typeof s2 === "string" ? `${s2}: ` : "- ";
68034
+ if (op === "insert") {
68035
+ if (typeof seg === "string") throw new Error("`insert` needs a positional target (a path ending `[i]`, or the node itself)");
68036
+ const entry2 = seg === void 0 ? void 0 : findEntry(lines, r2, seg);
68037
+ const at = entry2 ? entry2.start : bodyAppendPoint(lines, r2);
68038
+ lines.splice(at, 0, ...renderNode(payloadFacets(valueSrc), r2.indent, "- ", metaTag(meta, void 0, false)));
68022
68039
  return;
68023
68040
  }
68024
- const pad = " ".repeat(r2.indent);
68025
- if (!value) {
68026
- if (entry) lines.splice(entry.start, 1);
68041
+ if (seg === void 0) throw new Error(`\`${op}\` needs a key or index target`);
68042
+ const entry = findEntry(lines, r2, seg);
68043
+ if (op === "remove") {
68044
+ if (!entry) throw new Error(`no entry at ${typeof seg === "number" ? `[${seg}]` : seg}`);
68045
+ lines.splice(entry.start, entry.end - entry.start);
68027
68046
  return;
68028
68047
  }
68029
- const rendered = `${pad}${key}: ${JSON.stringify(value)}`;
68030
- if (entry) lines.splice(entry.start, 1, rendered);
68031
- else lines.splice(r2.marker >= 0 ? r2.marker + 1 : trimBack2(lines, r2.lo - 1, r2.hi), 0, rendered);
68032
- }
68033
- function replaceBodyItem(lines, r2, rank, text) {
68034
- const items = bodyItems(lines, r2);
68035
- if (!(rank >= 0 && rank < items.length)) throw new Error(`body[${rank}] out of range (${items.length})`);
68036
- const { start, end } = items[rank];
68037
- const head = lines[start].trim().replace(/^-\s*/, "");
68038
- const tag = head.match(/^(!!<[^>]*>)/)?.[1];
68039
- const rendered = tag ? taggedChunkLines(text, r2.indent, tag) : textChunkLines(text, r2.indent);
68040
- lines.splice(start, end - start, ...rendered);
68041
- }
68042
- function insertBodyItem(lines, r2, rank, text) {
68043
- const items = bodyItems(lines, r2);
68044
- const at = rank < items.length ? items[rank].start : bodyAppendPoint(lines, r2);
68045
- lines.splice(at, 0, ...textChunkLines(text, r2.indent));
68046
- }
68047
- function removeBodyItem(lines, r2, rank) {
68048
- const items = bodyItems(lines, r2);
68049
- if (!(rank >= 0 && rank < items.length)) throw new Error(`body[${rank}] out of range (${items.length})`);
68050
- const { start, end } = items[rank];
68051
- lines.splice(start, end - start);
68052
- }
68053
- function taggedChunkLines(text, indent, tag) {
68054
- const pad = " ".repeat(indent);
68055
- const first = text.split("\n").find((l2) => l2.trim().length > 0);
68056
- if (!first || /^\s/.test(first)) return [`${pad}- ${tag} ${JSON.stringify(text)}`];
68057
- const body = text.endsWith("\n") ? text.slice(0, -1) : text;
68058
- const head = text.endsWith("\n") ? "|" : "|-";
68059
- return [`${pad}- ${tag} ${head}`, ...body.split("\n").map((l2) => l2.trim().length ? `${pad} ${l2}` : "")];
68060
- }
68061
- function assertProseBodyItem(lines, r2, rank) {
68062
- const head = bodyItemHead(lines, r2, rank);
68063
- if (head.startsWith("*")) throw new Error("cannot edit a file/pointer chunk as text");
68064
- const tag = head.match(/^!!<([^>]*)>/)?.[1];
68065
- if (tag && !/text\/(markdown|marklower|x-latex)/.test(tag)) throw new Error("cannot edit a non-text chunk as text");
68048
+ if (op !== "emplace" && op !== "replace") throw new Error(`unknown edit op: ${op}`);
68049
+ if (!entry) {
68050
+ if (op === "replace") throw new Error(`no entry at ${typeof seg === "number" ? `[${seg}]` : seg}`);
68051
+ const rendered = renderNode(payloadFacets(valueSrc), r2.indent, marker(seg), metaTag(meta, void 0, false));
68052
+ const at = typeof seg === "string" ? r2.marker >= 0 ? r2.marker + 1 : trimBack2(lines, r2.lo - 1, r2.hi) : bodyAppendPoint(lines, r2);
68053
+ lines.splice(at, 0, ...rendered);
68054
+ return;
68055
+ }
68056
+ const had = entryFacets(lines, entry, r2.indent);
68057
+ const payload = payloadFacets(valueSrc);
68058
+ const inlineKey = entry.inline ? entry.key : null;
68059
+ const next = op === "replace" ? payload : {
68060
+ scalar: payload.scalar ?? had.scalar,
68061
+ keyed: payload.keyed.length ? payload.keyed : had.keyed,
68062
+ ordinal: payload.ordinal.length ? payload.ordinal : had.ordinal
68063
+ };
68064
+ const tag = metaTag(meta, had.tag, op === "emplace");
68065
+ if (inlineKey) {
68066
+ const pad = " ".repeat(indentOf2(lines[entry.start]));
68067
+ lines[entry.start] = `${pad}- ${tag ? tag + " " : ""}${inlineKey}: ${next.scalar ?? '""'}`;
68068
+ return;
68069
+ }
68070
+ lines.splice(entry.start, entry.end - entry.start, ...renderNode(next, r2.indent, marker(seg), tag));
68066
68071
  }
68067
68072
  function reachChapterItem(lines, indices) {
68068
68073
  const parent = reachChapter(lines, indices.slice(0, -1));
@@ -68125,49 +68130,109 @@ function splitChunkWithin(within) {
68125
68130
  while (i2 < within.length && typeof within[i2] === "number") i2++;
68126
68131
  return { indices: within.slice(0, i2).map(Number), keys: within.slice(i2).map(String) };
68127
68132
  }
68128
- function editChapterSource(src, within, op, text, index) {
68129
- const lines = src.split("\n");
68130
- if (op === "set") {
68131
- const key = within[within.length - 1];
68132
- if (key !== "title" && key !== "description") throw new Error("`set` needs a title/description target");
68133
- setScalarKey(lines, reachChapter(lines, within.slice(0, -1)), key, text);
68134
- return lines.join("\n");
68133
+ function setRootTag(lines, meta) {
68134
+ const at = lines.findIndex(isContentLine2);
68135
+ const existing = at >= 0 && lines[at].trim().startsWith("!!<") ? at : -1;
68136
+ if (meta === null) {
68137
+ if (existing >= 0) lines.splice(existing, 1);
68138
+ return;
68135
68139
  }
68136
- if (op === "insert") {
68137
- insertBodyItem(lines, reachChapter(lines, within), index ?? Number.MAX_SAFE_INTEGER, text);
68138
- return lines.join("\n");
68139
- }
68140
- const rank = within[within.length - 1];
68141
- if (typeof rank !== "number") throw new Error(`\`${op}\` needs a body-element target (path ends \`[rank]\`)`);
68142
- const region = reachChapter(lines, within.slice(0, -1));
68143
- if (op === "replace") {
68144
- assertProseBodyItem(lines, region, rank);
68145
- replaceBodyItem(lines, region, rank, text);
68146
- } else if (op === "remove") {
68147
- removeBodyItem(lines, region, rank);
68148
- } else {
68149
- throw new Error(`unknown edit op: ${op}`);
68140
+ if (existing >= 0) lines[existing] = `!!<${meta}>`;
68141
+ else lines.splice(Math.max(at, 0), 0, `!!<${meta}>`);
68142
+ }
68143
+ function editChapterSource(src, within, op, valueSrc, meta) {
68144
+ const lines = src.split("\n");
68145
+ if (within.length === 0) {
68146
+ if (op === "insert") {
68147
+ assignAt(lines, reachChapter(lines, []), void 0, op, valueSrc, meta);
68148
+ return lines.join("\n");
68149
+ }
68150
+ if (op === "emplace" && meta !== void 0 && !valueSrc) {
68151
+ setRootTag(lines, meta);
68152
+ return lines.join("\n");
68153
+ }
68154
+ throw new Error(`\`${op}\` at a document root needs a key or index target`);
68150
68155
  }
68156
+ assignAt(lines, reachChapter(lines, within.slice(0, -1)), within[within.length - 1], op, valueSrc, meta);
68151
68157
  return lines.join("\n");
68152
68158
  }
68159
+ var isPointerValue = (src) => /^\*\S*$/.test(src.trim()) && !src.includes("\n");
68160
+ function isPlainDir(dataRoot, s2, segs) {
68161
+ if (!segs.every((g) => typeof g === "string")) return false;
68162
+ const abs = path10.resolve(dataRoot, ...segs.map(String));
68163
+ if (!fs10.existsSync(abs) || !fs10.statSync(abs).isDirectory()) return false;
68164
+ try {
68165
+ chapterSource(dataRoot, s2, segs);
68166
+ return false;
68167
+ } catch {
68168
+ return true;
68169
+ }
68170
+ }
68171
+ function bornAsDocument(dataRoot, e3, tag) {
68172
+ const writeDirSegs = e3.dirBacked ? e3.docSegs : e3.docSegs.slice(0, -1);
68173
+ const writeDir = path10.resolve(dataRoot, ...writeDirSegs.map(String));
68174
+ const src = [...tag ? [tag] : [], e3.valueSrc, ""].join("\n");
68175
+ const final = writeObject(dataRoot, writeDir, sanitizeName(e3.name || "object"), e3.concrete, src);
68176
+ const targetSegs = [...writeDirSegs, final];
68177
+ return { pointer: e3.dirBacked ? `*/${final}` : `*/${segsToStr(targetSegs)}`, path: segsToStr(targetSegs) };
68178
+ }
68153
68179
  function applyEdits2(dataRoot, s2, edits) {
68154
68180
  const byFile = /* @__PURE__ */ new Map();
68181
+ const appended = [];
68182
+ const created = [];
68155
68183
  for (const e3 of edits) {
68156
68184
  const editSegs = strToSegs(e3.path ?? "");
68157
- const { docSegs, bodyFile } = chapterSource(dataRoot, s2, editSegs);
68158
- const within = editSegs.slice(docSegs.length);
68185
+ const op = String(e3.op ?? "");
68186
+ if (op === "insert" && (e3.concrete === "file/yamlover" || e3.concrete === "dir/yamlover") && isPlainDir(dataRoot, s2, editSegs)) {
68187
+ const dir = path10.resolve(dataRoot, ...editSegs.map(String));
68188
+ const src = [...metaTag(e3.meta, void 0, false) ? [metaTag(e3.meta, void 0, false)] : [], String(e3.yamlover ?? ""), ""].join("\n");
68189
+ if (e3.yamlover) parseYamlover(String(e3.yamlover), "<edit>");
68190
+ const final = writeObject(dataRoot, dir, objectBaseName(String(e3.name || "new")), String(e3.concrete), src);
68191
+ created.push(segsToStr([...editSegs, final]));
68192
+ continue;
68193
+ }
68194
+ const { docSegs, bodyFile, dirBacked } = chapterSource(dataRoot, s2, editSegs);
68195
+ if (op === "insert" && !e3.concrete?.includes("/")) {
68196
+ const last = editSegs[editSegs.length - 1];
68197
+ if (typeof last !== "number") appended.push(editSegs);
68198
+ else if (!s2.node(storePath(editSegs))) appended.push(editSegs.slice(0, -1));
68199
+ }
68200
+ const concrete = e3.concrete ? String(e3.concrete) : void 0;
68201
+ if (concrete && concrete !== "yamlover" && op !== "insert" && !(op === "emplace" && !s2.node(storePath(editSegs)))) {
68202
+ throw new Error("`concrete` is only for content being created \u2014 converting an existing node is a move, not an edit");
68203
+ }
68159
68204
  const list = byFile.get(bodyFile) ?? [];
68160
- list.push({ within, op: String(e3.op ?? ""), text: String(e3.text ?? ""), index: typeof e3.index === "number" ? e3.index : void 0 });
68205
+ list.push({
68206
+ within: editSegs.slice(docSegs.length),
68207
+ op,
68208
+ valueSrc: String(e3.yamlover ?? ""),
68209
+ meta: e3.meta === void 0 ? void 0 : e3.meta === null ? null : String(e3.meta),
68210
+ concrete,
68211
+ name: e3.name ? String(e3.name) : void 0,
68212
+ docSegs,
68213
+ dirBacked
68214
+ });
68161
68215
  byFile.set(bodyFile, list);
68162
68216
  }
68163
68217
  const touched = [];
68164
68218
  for (const [bodyFile, ops] of byFile) {
68165
68219
  let src = fs10.readFileSync(bodyFile, "utf8");
68166
- for (const o2 of ops) src = editChapterSource(src, o2.within, o2.op, o2.text, o2.index);
68220
+ for (const o2 of ops) {
68221
+ let { valueSrc, meta } = o2;
68222
+ const tag = metaTag(meta, void 0, false);
68223
+ if (valueSrc && !isPointerValue(valueSrc)) parseYamlover(valueSrc, "<edit>");
68224
+ if (o2.concrete === "file/yamlover" || o2.concrete === "dir/yamlover") {
68225
+ const born = bornAsDocument(dataRoot, o2, tag);
68226
+ created.push(born.path);
68227
+ valueSrc = born.pointer;
68228
+ meta = void 0;
68229
+ }
68230
+ src = editChapterSource(src, o2.within, o2.op, valueSrc, meta);
68231
+ }
68167
68232
  fs10.writeFileSync(bodyFile, src);
68168
68233
  touched.push(bodyFile);
68169
68234
  }
68170
- return touched;
68235
+ return { touched, created, appended };
68171
68236
  }
68172
68237
  function firstContentIndent2(lines) {
68173
68238
  for (const l2 of lines) if (isContentLine2(l2)) return indentOf2(l2);
@@ -68211,7 +68276,7 @@ function descriptionOf(s2, p2) {
68211
68276
  function scalarKeyOf(s2, p2, key) {
68212
68277
  const kp = (p2 === ":" ? "" : p2) + ":" + key;
68213
68278
  const t3 = s2.node(kp);
68214
- if (t3 && t3.type === "scalar" && !s2.hasChildren(kp) && t3.value != null) return String(t3.value);
68279
+ if (t3 && t3.type === "scalar" && t3.value != null) return String(t3.value);
68215
68280
  return null;
68216
68281
  }
68217
68282
  var PATH_TOKEN = /\[\d+\]|[^:\[\]]+/g;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yamlover",
3
- "version": "0.3.22",
3
+ "version": "0.3.23",
4
4
  "description": "Browse a yamlover tree in the web: npx yamlover <root> serves a React SPA over a directory.",
5
5
  "type": "module",
6
6
  "license": "MIT",