yamlover 0.3.2 → 0.3.4

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
@@ -912,8 +912,8 @@ function walkNodes(node, path8, visit, parent = null, label = null, pos = null)
912
912
  if (!node.entries) return;
913
913
  node.entries.forEach((e2, i) => {
914
914
  if (isPointer(e2.value)) return;
915
- const childPath = (path8 === ":" ? "" : path8) + (e2.key != null ? ":" + e2.key : "[" + i + "]");
916
- walkNodes(e2.value, childPath, visit, path8, e2.key, i);
915
+ const childPath2 = (path8 === ":" ? "" : path8) + (e2.key != null ? ":" + e2.key : "[" + i + "]");
916
+ walkNodes(e2.value, childPath2, visit, path8, e2.key, i);
917
917
  });
918
918
  }
919
919
  function formatFromMeta(node) {
@@ -1465,7 +1465,7 @@ var Block = class {
1465
1465
  node(minIndent) {
1466
1466
  const l = this.peek();
1467
1467
  if (!l || l.indent < minIndent) return null;
1468
- if (/^!!(mix|omni|set)(?=\s|$)/.test(l.text)) {
1468
+ if (/^!!(mix|var|omni|set)(?=\s|$)/.test(l.text)) {
1469
1469
  this.i++;
1470
1470
  return this.valueAfter(l.text, l.indent - 1, l.n, l.indent);
1471
1471
  }
@@ -1604,9 +1604,9 @@ var Block = class {
1604
1604
  ({ rest, col } = adv(rest, close + 1, col));
1605
1605
  }
1606
1606
  let typeTag;
1607
- const tag = /^!!(mix|omni|set)(?=\s|$)/.exec(rest);
1607
+ const tag = /^!!(mix|var|omni|set)(?=\s|$)/.exec(rest);
1608
1608
  if (tag) {
1609
- typeTag = tag[1];
1609
+ typeTag = tag[1] === "var" ? "omni" : tag[1];
1610
1610
  ({ rest, col } = adv(rest, tag[0].length, col));
1611
1611
  }
1612
1612
  const anchors = [];
@@ -2346,6 +2346,8 @@ async function walkTreeAsync(absDir, opts = {}) {
2346
2346
  return r.value;
2347
2347
  }
2348
2348
  var BUILTIN_TAG_SCHEMA = "type: object\nformat: x-yamlover-tag\nproperties:\n color:\n type: string\nadditionalProperties: *:: yamlover: $defs: tag\n";
2349
+ var BUILTIN_FRAGMENT_SCHEMA = "type: object\nformat: x-yamlover-fragment\n";
2350
+ var BUILTIN_ANNOTATION_SCHEMA = "type: variant\nformat: x-yamlover-annotation\n";
2349
2351
  var BUILTIN_TAGS_BODY = '!!<*yamlover:$defs:tag>\ncolors: The palette\n yellow:\n color: "#f9e2af"\n green:\n color: "#a6e3a1"\n sky:\n color: "#89dceb"\n mauve:\n color: "#cba6f7"\n pink:\n color: "#f5c2e7"\n peach:\n color: "#fab387"\n';
2350
2352
  var builtinTemplate = null;
2351
2353
  function builtinYamloverGraft() {
@@ -2354,15 +2356,29 @@ function builtinYamloverGraft() {
2354
2356
  tags: parseYamlover(BUILTIN_TAGS_BODY, "tags/.yamlover/body.yamlover").root
2355
2357
  };
2356
2358
  const tagCopy = structuredClone(builtinTemplate.tag);
2359
+ const fragCopy = parseYamlover(BUILTIN_FRAGMENT_SCHEMA, "$defs/fragment").root;
2360
+ const annCopy = parseYamlover(BUILTIN_ANNOTATION_SCHEMA, "$defs/annotation").root;
2357
2361
  const node = {
2358
2362
  kind: "mapping",
2359
2363
  array: false,
2360
2364
  entries: [
2361
- { key: "$defs", edge: "contain", value: { kind: "mapping", array: false, entries: [{ key: "tag", edge: "contain", value: tagCopy }] } },
2365
+ {
2366
+ key: "$defs",
2367
+ edge: "contain",
2368
+ value: {
2369
+ kind: "mapping",
2370
+ array: false,
2371
+ entries: [
2372
+ { key: "tag", edge: "contain", value: tagCopy },
2373
+ { key: "fragment", edge: "contain", value: fragCopy },
2374
+ { key: "annotation", edge: "contain", value: annCopy }
2375
+ ]
2376
+ }
2377
+ },
2362
2378
  { key: "tags", edge: "contain", value: structuredClone(builtinTemplate.tags) }
2363
2379
  ]
2364
2380
  };
2365
- return { node, defs: /* @__PURE__ */ new Map([["tag", tagCopy]]) };
2381
+ return { node, defs: /* @__PURE__ */ new Map([["tag", tagCopy], ["fragment", fragCopy], ["annotation", annCopy]]) };
2366
2382
  }
2367
2383
  function* walkTreeGen(absDir, opts = {}) {
2368
2384
  const ctx = { root: path.resolve(absDir), opts, files: /* @__PURE__ */ new Map(), count: 0 };
@@ -3567,6 +3583,157 @@ function compact(xs) {
3567
3583
  return xs.filter((x) => x !== null);
3568
3584
  }
3569
3585
 
3586
+ // src/server/embed.ts
3587
+ var ANNOTATIONS_KEY = "yamlover-annotations";
3588
+ var FRAGMENTS_KEY = "yamlover-fragments";
3589
+ var indentOf = (line) => {
3590
+ let i = 0;
3591
+ while (line[i] === " ") i++;
3592
+ return i;
3593
+ };
3594
+ var isContentLine = (line) => {
3595
+ const t2 = line.trim();
3596
+ return t2.length > 0 && !t2.startsWith("#");
3597
+ };
3598
+ function firstContentIndent(lines) {
3599
+ for (const l of lines) if (isContentLine(l)) return indentOf(l);
3600
+ return 0;
3601
+ }
3602
+ function keyToken(key) {
3603
+ return /^[A-Za-z0-9_][A-Za-z0-9_-]*$/.test(key) ? key : JSON.stringify(key);
3604
+ }
3605
+ function findKeyLine(lines, lo, hi, indent, key) {
3606
+ const tok = keyToken(key);
3607
+ for (let i = lo; i < hi; i++) {
3608
+ if (!isContentLine(lines[i])) continue;
3609
+ const ind = indentOf(lines[i]);
3610
+ if (ind < indent) return -1;
3611
+ if (ind !== indent) continue;
3612
+ const t2 = lines[i].trim();
3613
+ if (t2 === `${key}:` || t2.startsWith(`${key}: `) || t2 === `${tok}:` || t2.startsWith(`${tok}: `)) return i;
3614
+ }
3615
+ return -1;
3616
+ }
3617
+ function trimBack(lines, floor, end) {
3618
+ let e2 = end;
3619
+ while (e2 > floor + 1 && !isContentLine(lines[e2 - 1])) e2--;
3620
+ return e2;
3621
+ }
3622
+ function blockEnd(lines, from, hi, indent) {
3623
+ let last = from;
3624
+ for (let i = from; i < hi; i++) {
3625
+ if (!isContentLine(lines[i])) continue;
3626
+ if (indentOf(lines[i]) < indent) return trimBack(lines, last, i);
3627
+ last = i;
3628
+ }
3629
+ return trimBack(lines, last, hi);
3630
+ }
3631
+ function reachBody(lines, within) {
3632
+ let lo = 0;
3633
+ let hi = lines.length;
3634
+ let indent = firstContentIndent(lines);
3635
+ if (lines.length === 1 && lines[0] === "") {
3636
+ lines.length = 0;
3637
+ hi = 0;
3638
+ indent = 0;
3639
+ }
3640
+ for (const key of within) {
3641
+ const L = findKeyLine(lines, lo, hi, indent, key);
3642
+ if (L < 0) {
3643
+ const at = trimBack(lines, lo - 1, hi);
3644
+ lines.splice(at, 0, `${" ".repeat(indent)}${keyToken(key)}:`);
3645
+ lo = at + 1;
3646
+ hi = at + 1;
3647
+ indent += 2;
3648
+ continue;
3649
+ }
3650
+ const inline = lines[L].slice(indentOf(lines[L])).slice(`${lines[L].trim().split(":")[0]}:`.length);
3651
+ const bodyLo = L + 1;
3652
+ const bodyHi = blockEnd(lines, bodyLo, hi, indent + 1);
3653
+ let childIndent = indent + 2;
3654
+ for (let i = bodyLo; i < bodyHi; i++) {
3655
+ if (isContentLine(lines[i]) && indentOf(lines[i]) > indent) {
3656
+ childIndent = indentOf(lines[i]);
3657
+ break;
3658
+ }
3659
+ }
3660
+ lo = bodyLo;
3661
+ hi = bodyHi;
3662
+ indent = childIndent;
3663
+ }
3664
+ return { lo, hi, indent };
3665
+ }
3666
+ function seqItemLines(lines, region, key) {
3667
+ const keyLine = findKeyLine(lines, region.lo, region.hi, region.indent, key);
3668
+ if (keyLine < 0) return null;
3669
+ const items = [];
3670
+ let end = keyLine + 1;
3671
+ for (let i = keyLine + 1; i < region.hi; i++) {
3672
+ if (!isContentLine(lines[i])) continue;
3673
+ const ind = indentOf(lines[i]);
3674
+ if (ind < region.indent) break;
3675
+ if (ind === region.indent) {
3676
+ const t2 = lines[i].trim();
3677
+ if (t2 === "-" || t2.startsWith("- ")) {
3678
+ items.push(i);
3679
+ end = i;
3680
+ continue;
3681
+ }
3682
+ break;
3683
+ }
3684
+ end = i;
3685
+ }
3686
+ return { keyLine, items, end: trimBack(lines, end, region.hi) };
3687
+ }
3688
+ function appendAnnotation(text, within, render) {
3689
+ const lines = text.replace(/\n$/, "").split("\n");
3690
+ const region = reachBody(lines, within);
3691
+ const seq = seqItemLines(lines, region, ANNOTATIONS_KEY);
3692
+ if (!seq) {
3693
+ const at = trimBack(lines, region.lo - 1, region.hi);
3694
+ lines.splice(at, 0, `${" ".repeat(region.indent)}${ANNOTATIONS_KEY}:`, ...render(region.indent));
3695
+ } else {
3696
+ lines.splice(seq.end, 0, ...render(region.indent));
3697
+ }
3698
+ return lines.join("\n") + "\n";
3699
+ }
3700
+ function upsertFragment(text, within, slug, render) {
3701
+ const lines = text.replace(/\n$/, "").split("\n");
3702
+ const region = reachBody(lines, within);
3703
+ let fragKey = findKeyLine(lines, region.lo, region.hi, region.indent, FRAGMENTS_KEY);
3704
+ if (fragKey < 0) {
3705
+ const at = trimBack(lines, region.lo - 1, region.hi);
3706
+ lines.splice(at, 0, `${" ".repeat(region.indent)}${FRAGMENTS_KEY}:`);
3707
+ fragKey = at;
3708
+ }
3709
+ const mapIndent = region.indent + 2;
3710
+ const mapBody = { lo: fragKey + 1, hi: blockEnd(lines, fragKey + 1, lines.length, region.indent + 1), indent: mapIndent };
3711
+ const existing = findKeyLine(lines, mapBody.lo, mapBody.hi, mapIndent, slug);
3712
+ if (existing >= 0) {
3713
+ const end = blockEnd(lines, existing + 1, mapBody.hi, mapIndent + 1);
3714
+ lines.splice(existing, end - existing, ...render(mapIndent));
3715
+ } else {
3716
+ const at = trimBack(lines, fragKey, mapBody.hi);
3717
+ lines.splice(at, 0, ...render(mapIndent));
3718
+ }
3719
+ return lines.join("\n") + "\n";
3720
+ }
3721
+ function removeAnnotation(text, within, predicate) {
3722
+ const lines = text.replace(/\n$/, "").split("\n");
3723
+ const region = reachBody(lines, within);
3724
+ const seq = seqItemLines(lines, region, ANNOTATIONS_KEY);
3725
+ if (!seq) return text;
3726
+ for (let k = 0; k < seq.items.length; k++) {
3727
+ const i = seq.items[k];
3728
+ const itemText = lines[i].trim().replace(/^-\s*/, "");
3729
+ if (!predicate(itemText)) continue;
3730
+ const next = k + 1 < seq.items.length ? seq.items[k + 1] : seq.end;
3731
+ lines.splice(i, next - i);
3732
+ return lines.join("\n") + "\n";
3733
+ }
3734
+ return text;
3735
+ }
3736
+
3570
3737
  // src/server/gitignore.ts
3571
3738
  var import_ignore = __toESM(require_ignore(), 1);
3572
3739
  import fs5 from "node:fs";
@@ -3652,6 +3819,14 @@ function scalarType(v) {
3652
3819
  if (typeof v === "number") return Number.isInteger(v) ? "integer" : "number";
3653
3820
  return "string";
3654
3821
  }
3822
+ function facetsOf(s, p, row) {
3823
+ const ents = ownedEntries(s, p);
3824
+ return {
3825
+ valueType: row.type === "scalar" ? scalarType(row.value) : row.type === "blob" ? "binary" : null,
3826
+ hasKeyed: ents.some((e2) => e2.label !== null),
3827
+ hasOrdinal: ents.some((e2) => e2.label === null)
3828
+ };
3829
+ }
3655
3830
 
3656
3831
  // src/server/tasks.ts
3657
3832
  var PROGRESS_EMIT_MS = 150;
@@ -3899,31 +4074,40 @@ function createHandlers(dataRoot, opts = {}) {
3899
4074
  }
3900
4075
  if (req.method === "POST" && url.pathname === "/api/annotate") {
3901
4076
  readBody(req).then(
3902
- (data) => (
3903
- // Queued: an incremental row added while a full walk (whose disk snapshot predates
3904
- // this annotation) is committing would be silently swapped away.
3905
- enqueue(() => {
3906
- const a = data;
3907
- const tagStore = storePath(strToSegs(a.tag ?? ""));
3908
- if (!a?.tag || s.node(tagStore)?.format !== TAG_FORMAT) {
3909
- throw new Error("annotation needs a `tag` that is an x-yamlover-tag node");
3910
- }
3911
- const annPath = writeAnnotation(dataRoot, settings.annotations.location, a);
3912
- const doc = parseYamlover(fs6.readFileSync(path7.join(dataRoot, ...strToSegs(annPath).map(String)), "utf8"), annPath);
3913
- s.addAnnotation(storePath(strToSegs(annPath)), storePath(strToSegs(a.target)), doc, tagStore);
3914
- announce({ added: [relFileOf(annPath)] });
3915
- return { path: annPath };
3916
- })
3917
- )
4077
+ (data) => enqueue(async () => {
4078
+ const a = data;
4079
+ const tagStore = storePath(strToSegs(a.tag ?? ""));
4080
+ if (!a?.tag || s.node(tagStore)?.format !== TAG_FORMAT) {
4081
+ throw new Error("annotation needs a `tag` that is an x-yamlover-tag node");
4082
+ }
4083
+ embedAnnotation(dataRoot, s, a);
4084
+ broadcast(await doReindex());
4085
+ scheduleHasher();
4086
+ return { ok: true };
4087
+ })
4088
+ ).then((body) => sendJson(res, 201, body)).catch((e2) => sendJson(res, 400, { error: String(e2.message || e2) }));
4089
+ return;
4090
+ }
4091
+ if (req.method === "POST" && url.pathname === "/api/fragment") {
4092
+ readBody(req).then(
4093
+ (data) => enqueue(async () => {
4094
+ const f = data;
4095
+ if (!f?.selector || typeof f.selector !== "object") throw new Error("a fragment needs a selector");
4096
+ const made = embedFragment(dataRoot, s, f);
4097
+ broadcast(await doReindex());
4098
+ scheduleHasher();
4099
+ return made;
4100
+ })
3918
4101
  ).then((body) => sendJson(res, 201, body)).catch((e2) => sendJson(res, 400, { error: String(e2.message || e2) }));
3919
4102
  return;
3920
4103
  }
3921
4104
  if (req.method === "DELETE" && url.pathname === "/api/annotate") {
3922
- const annPath = url.searchParams.get("path") || "";
3923
- enqueue(() => {
3924
- deleteAnnotation(dataRoot, s, annPath);
3925
- s.removeAnnotation(storePath(strToSegs(annPath)));
3926
- announce({ removed: [relFileOf(annPath)] });
4105
+ const target = url.searchParams.get("target") ?? "";
4106
+ const tag = url.searchParams.get("tag") || "";
4107
+ enqueue(async () => {
4108
+ if (!tag) throw new Error("delete needs a `tag`");
4109
+ unembedAnnotation(dataRoot, s, target, tag);
4110
+ broadcast(await doReindex());
3927
4111
  }).then(() => sendJson(res, 200, { ok: true })).catch((e2) => sendJson(res, 400, { error: String(e2.message || e2) }));
3928
4112
  return;
3929
4113
  }
@@ -4023,6 +4207,8 @@ function createHandlers(dataRoot, opts = {}) {
4023
4207
  path: segsToStr(segs),
4024
4208
  type: tocType(s, p, row),
4025
4209
  format: row.format ?? null,
4210
+ ...facetsOf(s, p, row),
4211
+ // valueType / hasKeyed / hasOrdinal — the renderer dispatch facets (TYPES.md §9)
4026
4212
  concrete: concreteOf(dataRoot, segs, row),
4027
4213
  // dir | yamlover | null (stat-derived; engine tracks no per-node concrete yet)
4028
4214
  documentPath: documentPath(s, segs),
@@ -4148,7 +4334,7 @@ function linkMarker(dataRoot, s, segs) {
4148
4334
  const p = storePath(segs);
4149
4335
  const row = s.node(p);
4150
4336
  const k = displayKind(s, p, row);
4151
- const info = { kind: k, type: tocType(s, p, row), path: segsToStr(segs) };
4337
+ const info = { kind: k, type: tocType(s, p, row), ...facetsOf(s, p, row), path: segsToStr(segs) };
4152
4338
  if (row.format) info.format = row.format;
4153
4339
  const concrete = concreteOf(dataRoot, segs, row);
4154
4340
  if (concrete) info.concrete = concrete;
@@ -4207,6 +4393,7 @@ function buildTree(dataRoot, s, segs, label, depth) {
4207
4393
  label,
4208
4394
  type: tocType(s, p, row),
4209
4395
  format: row.format ?? null,
4396
+ ...facetsOf(s, p, row),
4210
4397
  concrete: concreteOf(dataRoot, segs, row),
4211
4398
  hasChildren: s.hasChildren(p),
4212
4399
  children: []
@@ -4225,45 +4412,77 @@ function labelFor(s, p, keyOrIdx) {
4225
4412
  return typeof keyOrIdx === "number" ? `[${keyOrIdx}]` : keyOrIdx;
4226
4413
  }
4227
4414
  var TAG_FORMAT = "x-yamlover-tag";
4228
- function appliedTag(s, annStorePath) {
4229
- const e2 = s.relationships(annStorePath).out.find(
4230
- (t2) => t2.kind === "back" && t2.label === null && s.node(t2.to)?.format === TAG_FORMAT
4231
- );
4232
- if (!e2) return null;
4233
- const segs = storePathToSegs(e2.to);
4234
- const color = s.node(e2.to + ":color")?.value;
4415
+ var ANN_KEY = "yamlover-annotations";
4416
+ var FRAG_KEY = "yamlover-fragments";
4417
+ var CROP_DIR = "fragments";
4418
+ var childPath = (parent, key) => (parent === ":" ? "" : parent) + ":" + key;
4419
+ function projectTag(s, tagStore) {
4420
+ if (s.node(tagStore)?.format !== TAG_FORMAT) return null;
4421
+ const segs = storePathToSegs(tagStore);
4422
+ const color = s.node(tagStore + ":color")?.value;
4235
4423
  return { path: segsToStr(segs), name: String(segs[segs.length - 1] ?? ""), color: typeof color === "string" ? color : null };
4236
4424
  }
4425
+ function readAnnotations(s, hostStore) {
4426
+ const arr = childPath(hostStore, ANN_KEY);
4427
+ if (!s.node(arr)) return [];
4428
+ const out = [];
4429
+ for (const e2 of s.entries(arr)) {
4430
+ if (e2.kind === "ref") {
4431
+ const tag = projectTag(s, e2.to);
4432
+ if (tag) out.push({ tag });
4433
+ } else if (e2.kind === "contain") {
4434
+ const tagEdge = s.relationships(e2.to).out.find((o) => o.kind === "ref" && o.label === "tag");
4435
+ const tag = tagEdge ? projectTag(s, tagEdge.to) : null;
4436
+ if (!tag) continue;
4437
+ const params = {};
4438
+ let description;
4439
+ for (const c of s.children(e2.to)) {
4440
+ const v = s.node(c.to)?.value;
4441
+ if (c.label === "description") description = v == null ? void 0 : String(v);
4442
+ else if (c.label) params[c.label] = v;
4443
+ }
4444
+ out.push({ tag, description, params: Object.keys(params).length ? params : void 0 });
4445
+ }
4446
+ }
4447
+ return out;
4448
+ }
4449
+ function readFragments(s, hostStore) {
4450
+ const frags = childPath(hostStore, FRAG_KEY);
4451
+ if (!s.node(frags)) return [];
4452
+ const out = [];
4453
+ for (const fc of s.children(frags)) {
4454
+ if (!fc.label) continue;
4455
+ const selector = {};
4456
+ for (const c of s.children(fc.to)) {
4457
+ if (c.label && c.label !== ANN_KEY && c.label !== "created") selector[c.label] = s.node(c.to)?.value;
4458
+ }
4459
+ const imgEdge = s.relationships(fc.to).out.find((o) => o.kind === "ref" && o.label === "image");
4460
+ const imageUrl = imgEdge ? `/api/blob?path=${encodeURIComponent(segsToStr(storePathToSegs(imgEdge.to)))}` : void 0;
4461
+ out.push({ slug: fc.label, node: fc.to, selector, imageUrl });
4462
+ }
4463
+ return out;
4464
+ }
4237
4465
  function annotationsFor(dataRoot, s, segs) {
4238
4466
  const p = storePath(segs);
4239
4467
  const out = [];
4240
- for (const e2 of s.relationships(p).in) {
4241
- if (e2.kind !== "ref") continue;
4242
- const src = s.node(e2.from);
4243
- if (src?.format !== "x-yamlover-annotation") continue;
4244
- const aSegs = storePathToSegs(e2.from);
4245
- out.push({
4246
- path: segsToStr(aSegs),
4247
- tag: appliedTag(s, e2.from),
4248
- ...projectValue(dataRoot, s, aSegs, 6, true)
4249
- });
4468
+ for (const a of readAnnotations(s, p)) out.push({ ...a });
4469
+ for (const f of readFragments(s, p)) {
4470
+ for (const a of readAnnotations(s, f.node)) {
4471
+ out.push({ ...a, selector: f.selector, fragmentSlug: f.slug, ...f.imageUrl ? { imageUrl: f.imageUrl } : {} });
4472
+ }
4250
4473
  }
4251
4474
  return out;
4252
4475
  }
4253
4476
  function taggedMaterials(dataRoot, s, tagStorePath) {
4254
4477
  const seen = /* @__PURE__ */ new Set();
4255
4478
  const out = [];
4256
- const backs = s.relationships(tagStorePath).in.filter((e2) => e2.kind === "back" && e2.from).sort((a, b) => a.from < b.from ? -1 : a.from > b.from ? 1 : 0);
4257
- for (const e2 of backs) {
4258
- let material = e2.from;
4259
- if (s.node(e2.from)?.format === "x-yamlover-annotation") {
4260
- const t2 = s.relationships(e2.from).out.find((o) => o.kind === "ref" && o.label === "target");
4261
- if (!t2) continue;
4262
- material = t2.to;
4263
- }
4264
- if (seen.has(material) || !s.node(material)) continue;
4265
- seen.add(material);
4266
- out.push(linkMarker(dataRoot, s, storePathToSegs(material)));
4479
+ const ins = s.relationships(tagStorePath).in.filter((e2) => (e2.kind === "ref" || e2.kind === "back") && e2.from).sort((a, b) => a.from < b.from ? -1 : a.from > b.from ? 1 : 0);
4480
+ for (const e2 of ins) {
4481
+ const arrOwner = e2.from.replace(/\[\d+\]$/, "").match(/^(.*):yamlover-annotations$/);
4482
+ const owner = arrOwner ? arrOwner[1] || ":" : e2.from;
4483
+ if (owner === tagStorePath || seen.has(owner) || !s.node(owner)) continue;
4484
+ seen.add(owner);
4485
+ out.push(linkMarker(dataRoot, s, storePathToSegs(owner)));
4267
4486
  }
4268
4487
  return out;
4269
4488
  }
@@ -4277,25 +4496,77 @@ function pointerRaw(clientPath) {
4277
4496
  function yScalar(v) {
4278
4497
  return typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(String(v ?? ""));
4279
4498
  }
4280
- function writeAnnotation(dataRoot, location, a) {
4281
- if (!a?.target || !a?.tag) throw new Error("annotation needs a target and a tag");
4282
- const dir = path7.resolve(dataRoot, ...strToSegs(location).map(String));
4283
- const root = path7.resolve(dataRoot);
4284
- if (dir !== root && !dir.startsWith(root + path7.sep)) throw new Error("annotation location escapes the data root");
4285
- fs6.mkdirSync(dir, { recursive: true });
4286
- const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4287
- const file = `${id}.yamlover`;
4288
- const lines = [
4289
- "!!<*yamlover/$defs/annotation>",
4290
- `target: ${pointerToken(pointerRaw(a.target))}`,
4291
- anchorToken(`${pointerRaw(a.tag)}[]`)
4292
- // the applied tag holds me (ordinal path anchor)
4293
- ];
4294
- if (a.selector) lines.push("selector:", ...Object.entries(a.selector).map(([k, v]) => ` ${k}: ${yScalar(v)}`));
4295
- if (a.description) lines.push(`description: ${yScalar(a.description)}`);
4296
- lines.push(`created: ${(/* @__PURE__ */ new Date()).toISOString()}`, "");
4297
- fs6.writeFileSync(path7.join(dir, file), lines.join("\n"));
4298
- return `${location}:${file}`;
4499
+ function hostFor(dataRoot, s, segs) {
4500
+ for (let i = segs.length; i >= 0; i--) {
4501
+ const sub = segs.slice(0, i);
4502
+ const abs = path7.resolve(dataRoot, ...sub.map(String));
4503
+ let st;
4504
+ try {
4505
+ st = fs6.statSync(abs);
4506
+ } catch {
4507
+ continue;
4508
+ }
4509
+ if (st.isDirectory()) return { bodyFile: path7.join(abs, ".yamlover", "body.yamlover"), within: segs.slice(i).map(String) };
4510
+ if (st.isFile()) {
4511
+ const node = s.node(storePath(sub));
4512
+ if (node?.meta?.documentRoot && node.type === "mapping" && !node.is_array) {
4513
+ return { bodyFile: abs, within: segs.slice(i).map(String) };
4514
+ }
4515
+ const dir = path7.resolve(dataRoot, ...sub.slice(0, -1).map(String));
4516
+ return { bodyFile: path7.join(dir, ".yamlover", "body.yamlover"), within: segs.slice(i - 1).map(String) };
4517
+ }
4518
+ }
4519
+ return { bodyFile: path7.join(dataRoot, ".yamlover", "body.yamlover"), within: segs.map(String) };
4520
+ }
4521
+ function annotationItemLines(a, indent) {
4522
+ const pad = " ".repeat(indent);
4523
+ const ptr = pointerToken(pointerRaw(a.tag));
4524
+ const params = { ...a.params ?? {} };
4525
+ if (a.description != null && a.description !== "") params.description = a.description;
4526
+ const keys = Object.keys(params);
4527
+ if (keys.length === 0) return [`${pad}- ${ptr}`];
4528
+ return [`${pad}- tag: ${ptr}`, ...keys.map((k) => `${pad} ${keyToken(k)}: ${yScalar(params[k])}`)];
4529
+ }
4530
+ function fragmentBlockLines(slug, selector, imagePtr, indent) {
4531
+ const pad = " ".repeat(indent);
4532
+ const lines = [`${pad}${keyToken(slug)}: !!<*::yamlover:$defs:fragment>`];
4533
+ for (const [k, v] of Object.entries(selector)) lines.push(`${pad} ${keyToken(k)}: ${yScalar(v)}`);
4534
+ if (imagePtr) lines.push(`${pad} image: ${imagePtr}`);
4535
+ lines.push(`${pad} created: ${(/* @__PURE__ */ new Date()).toISOString()}`);
4536
+ return lines;
4537
+ }
4538
+ function embedAnnotation(dataRoot, s, a) {
4539
+ const { bodyFile, within } = hostFor(dataRoot, s, strToSegs(a.target || ":"));
4540
+ fs6.mkdirSync(path7.dirname(bodyFile), { recursive: true });
4541
+ const src = fs6.existsSync(bodyFile) ? fs6.readFileSync(bodyFile, "utf8") : "";
4542
+ fs6.writeFileSync(bodyFile, appendAnnotation(src, within, (indent) => annotationItemLines(a, indent)));
4543
+ }
4544
+ function embedFragment(dataRoot, s, f) {
4545
+ const segs = strToSegs(f.target || ":");
4546
+ const { bodyFile, within } = hostFor(dataRoot, s, segs);
4547
+ const slug = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4548
+ let imagePtr = null;
4549
+ if (f.imageBase64) {
4550
+ const bytes = Buffer.from(String(f.imageBase64).replace(/^data:[^,]*,/, ""), "base64");
4551
+ if (bytes.length > 0) {
4552
+ const cropDir = path7.join(dataRoot, CROP_DIR);
4553
+ fs6.mkdirSync(cropDir, { recursive: true });
4554
+ const cropName = `${slug}.png`;
4555
+ writeInside(dataRoot, cropDir, cropName, bytes);
4556
+ imagePtr = pointerToken(pointerRaw(segsToStr([CROP_DIR, cropName])));
4557
+ }
4558
+ }
4559
+ fs6.mkdirSync(path7.dirname(bodyFile), { recursive: true });
4560
+ const src = fs6.existsSync(bodyFile) ? fs6.readFileSync(bodyFile, "utf8") : "";
4561
+ fs6.writeFileSync(bodyFile, upsertFragment(src, within, slug, (indent) => fragmentBlockLines(slug, f.selector, imagePtr, indent)));
4562
+ return { slug, fragmentPath: segsToStr([...segs, FRAG_KEY, slug]) };
4563
+ }
4564
+ function unembedAnnotation(dataRoot, s, target, tag) {
4565
+ const { bodyFile, within } = hostFor(dataRoot, s, strToSegs(target || ":"));
4566
+ if (!fs6.existsSync(bodyFile)) return;
4567
+ const needle = pointerRaw(tag);
4568
+ const src = fs6.readFileSync(bodyFile, "utf8");
4569
+ fs6.writeFileSync(bodyFile, removeAnnotation(src, within, (itemText) => itemText.includes(needle)));
4299
4570
  }
4300
4571
  function writeTag(dataRoot, location, name) {
4301
4572
  if (/[/\\\r\n:]/.test(name)) throw new Error("a tag name cannot contain '/', '\\', ':' or line breaks");
@@ -4306,7 +4577,7 @@ function writeTag(dataRoot, location, name) {
4306
4577
  const createdFile = !fs6.existsSync(file);
4307
4578
  const head = "# Named tags created from the annotation picker (settings.yamlover: tags.location).\n";
4308
4579
  const existing = createdFile ? head : fs6.readFileSync(file, "utf8");
4309
- const body = (existing === "" || existing.endsWith("\n") ? existing : existing + "\n") + `${name}: !!<*yamlover/$defs/tag>
4580
+ const body = (existing === "" || existing.endsWith("\n") ? existing : existing + "\n") + `${name}: !!<*::yamlover:$defs:tag>
4310
4581
  `;
4311
4582
  const entries = parseYamlover(body, file).root.entries ?? [];
4312
4583
  const pos = entries.findIndex((e2) => e2.key === name);
@@ -4318,16 +4589,6 @@ function writeTag(dataRoot, location, name) {
4318
4589
  fs6.writeFileSync(file, body);
4319
4590
  return { node: entry.value, pos, file: [...strToSegs(location).map(String), ".yamlover", "body.yamlover"].join("/"), createdFile };
4320
4591
  }
4321
- function deleteAnnotation(dataRoot, s, annPath) {
4322
- const segs = strToSegs(annPath);
4323
- if (!String(segs[segs.length - 1] ?? "").endsWith(".yamlover")) throw new Error("not an annotation file");
4324
- if (s.node(storePath(segs))?.format !== "x-yamlover-annotation") throw new Error("not an annotation node");
4325
- const root = path7.resolve(dataRoot);
4326
- const file = path7.resolve(dataRoot, ...segs.map(String));
4327
- if (!file.startsWith(root + path7.sep)) throw new Error("outside the served root");
4328
- if (!fs6.existsSync(file) || fs6.statSync(file).isDirectory()) throw new Error("not an annotation file");
4329
- fs6.rmSync(file, { force: true });
4330
- }
4331
4592
  function handlePaste(dataRoot, s, input) {
4332
4593
  const segs = strToSegs(input.path || ":");
4333
4594
  const row = s.node(storePath(segs));
@@ -4398,7 +4659,7 @@ function pasteTextAsChapterFile(dataRoot, segs, text) {
4398
4659
  const dir = path7.resolve(dataRoot, ...dirSegs.map(String));
4399
4660
  const title = titleFromText(text);
4400
4661
  const final = uniqueName(dir, chapterFileName(title));
4401
- const src = ["!!<*yamlover/$defs/chapter>", `title: ${JSON.stringify(title)}`, "chunks:", ...textChunkLines(text, 0), ""].join("\n");
4662
+ const src = ["!!<*::yamlover:$defs:chapter>", `title: ${JSON.stringify(title)}`, "chunks:", ...textChunkLines(text, 0), ""].join("\n");
4402
4663
  writeInside(dataRoot, dir, final, Buffer.from(src, "utf8"));
4403
4664
  return { path: segsToStr([...dirSegs, final]), dir: segsToStr(dirSegs), open: dirSegs.length !== segs.length };
4404
4665
  }
@@ -4486,7 +4747,7 @@ function pasteRichAsChapter(dataRoot, segs, rich) {
4486
4747
  return { path: segsToStr([...dirSegs, name]), dir: segsToStr(dirSegs), open: dirSegs.length !== segs.length };
4487
4748
  }
4488
4749
  function renderChapterSource(title, rich, pointerFor) {
4489
- const lines = ["!!<*yamlover/$defs/chapter>", `title: ${JSON.stringify(title)}`];
4750
+ const lines = ["!!<*::yamlover:$defs:chapter>", `title: ${JSON.stringify(title)}`];
4490
4751
  if (rich.chunks.length) lines.push("chunks:", ...rich.chunks.flatMap((c) => richItemLines(c, 0, pointerFor)));
4491
4752
  if (rich.children.length) lines.push("children:", ...rich.children.flatMap((k) => richChildLines(k, 0, pointerFor)));
4492
4753
  return lines.join("\n") + "\n";
@@ -4522,12 +4783,12 @@ function writeInside(dataRoot, dir, name, bytes) {
4522
4783
  if (target !== root && !target.startsWith(root + path7.sep)) throw new Error("target escapes the data root");
4523
4784
  fs6.writeFileSync(target, bytes);
4524
4785
  }
4525
- var indentOf = (line) => {
4786
+ var indentOf2 = (line) => {
4526
4787
  let i = 0;
4527
4788
  while (line[i] === " ") i++;
4528
4789
  return i;
4529
4790
  };
4530
- var isContentLine = (line) => {
4791
+ var isContentLine2 = (line) => {
4531
4792
  const t2 = line.trim();
4532
4793
  return t2.length > 0 && !t2.startsWith("#");
4533
4794
  };
@@ -4543,10 +4804,10 @@ function appendToList(text, chapterPath, key, renderItems) {
4543
4804
  const lines = text.split("\n");
4544
4805
  let lo = 0;
4545
4806
  let hi = lines.length;
4546
- let indent = firstContentIndent(lines);
4807
+ let indent = firstContentIndent2(lines);
4547
4808
  for (let i = 0; i < chapterPath.length; i += 2) {
4548
4809
  const idx = Number(chapterPath[i + 1]);
4549
- const kids = findKeyLine(lines, lo, hi, indent, "children");
4810
+ const kids = findKeyLine2(lines, lo, hi, indent, "children");
4550
4811
  if (kids < 0) throw new Error(`no 'children:' at indent ${indent}`);
4551
4812
  const items = seqItems(lines, kids + 1, hi, indent);
4552
4813
  if (!(idx >= 0 && idx < items.length)) throw new Error(`children[${idx}] out of range (${items.length})`);
@@ -4554,9 +4815,9 @@ function appendToList(text, chapterPath, key, renderItems) {
4554
4815
  lo = items[idx] + 1;
4555
4816
  indent += 2;
4556
4817
  }
4557
- const keyLine = findKeyLine(lines, lo, hi, indent, key);
4818
+ const keyLine = findKeyLine2(lines, lo, hi, indent, key);
4558
4819
  if (keyLine < 0) {
4559
- const end = trimBack(lines, lo - 1, hi);
4820
+ const end = trimBack2(lines, lo - 1, hi);
4560
4821
  lines.splice(end, 0, `${" ".repeat(indent)}${key}:`, ...renderItems(indent));
4561
4822
  } else {
4562
4823
  const end = seqEnd(lines, keyLine + 1, hi, indent);
@@ -4564,14 +4825,14 @@ function appendToList(text, chapterPath, key, renderItems) {
4564
4825
  }
4565
4826
  return lines.join("\n");
4566
4827
  }
4567
- function firstContentIndent(lines) {
4568
- for (const l of lines) if (isContentLine(l)) return indentOf(l);
4828
+ function firstContentIndent2(lines) {
4829
+ for (const l of lines) if (isContentLine2(l)) return indentOf2(l);
4569
4830
  return 0;
4570
4831
  }
4571
- function findKeyLine(lines, lo, hi, indent, key) {
4832
+ function findKeyLine2(lines, lo, hi, indent, key) {
4572
4833
  for (let i = lo; i < hi; i++) {
4573
- if (!isContentLine(lines[i])) continue;
4574
- const ind = indentOf(lines[i]);
4834
+ if (!isContentLine2(lines[i])) continue;
4835
+ const ind = indentOf2(lines[i]);
4575
4836
  if (ind < indent) return -1;
4576
4837
  if (ind !== indent) continue;
4577
4838
  const t2 = lines[i].trim();
@@ -4582,8 +4843,8 @@ function findKeyLine(lines, lo, hi, indent, key) {
4582
4843
  function seqItems(lines, from, hi, indent) {
4583
4844
  const out = [];
4584
4845
  for (let i = from; i < hi; i++) {
4585
- if (!isContentLine(lines[i])) continue;
4586
- const ind = indentOf(lines[i]);
4846
+ if (!isContentLine2(lines[i])) continue;
4847
+ const ind = indentOf2(lines[i]);
4587
4848
  if (ind < indent) break;
4588
4849
  if (ind !== indent) continue;
4589
4850
  const t2 = lines[i].trim();
@@ -4595,24 +4856,24 @@ function seqItems(lines, from, hi, indent) {
4595
4856
  function seqEnd(lines, from, hi, indent) {
4596
4857
  let last = from;
4597
4858
  for (let i = from; i < hi; i++) {
4598
- if (!isContentLine(lines[i])) continue;
4599
- const ind = indentOf(lines[i]);
4600
- if (ind < indent) return trimBack(lines, last, i);
4859
+ if (!isContentLine2(lines[i])) continue;
4860
+ const ind = indentOf2(lines[i]);
4861
+ if (ind < indent) return trimBack2(lines, last, i);
4601
4862
  if (ind === indent) {
4602
4863
  const t2 = lines[i].trim();
4603
4864
  if (t2 === "-" || t2.startsWith("- ")) {
4604
4865
  last = i;
4605
4866
  continue;
4606
4867
  }
4607
- return trimBack(lines, last, i);
4868
+ return trimBack2(lines, last, i);
4608
4869
  }
4609
4870
  last = i;
4610
4871
  }
4611
- return trimBack(lines, last, hi);
4872
+ return trimBack2(lines, last, hi);
4612
4873
  }
4613
- function trimBack(lines, lastItemLine, end) {
4874
+ function trimBack2(lines, lastItemLine, end) {
4614
4875
  let e2 = end;
4615
- while (e2 > lastItemLine + 1 && !isContentLine(lines[e2 - 1])) e2--;
4876
+ while (e2 > lastItemLine + 1 && !isContentLine2(lines[e2 - 1])) e2--;
4616
4877
  return e2;
4617
4878
  }
4618
4879
  function readBody(req) {