diffwiki-core 0.6.1 → 0.7.0-rc.202607251444.5d678fd

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/index.cjs CHANGED
@@ -577,10 +577,12 @@ __export(index_exports, {
577
577
  addTags: () => addTags,
578
578
  buildArticleTree: () => buildArticleTree,
579
579
  buildCollections: () => buildCollections,
580
+ buildLinkGraph: () => buildLinkGraph,
580
581
  buildNavTree: () => buildNavTree,
581
582
  buildSearchIndex: () => buildSearchIndex,
582
583
  collectionDir: () => collectionDir,
583
584
  collectionGitStatus: () => collectionGitStatus,
585
+ collectionMeta: () => collectionMeta,
584
586
  collectionsDir: () => collectionsDir,
585
587
  configPath: () => configPath,
586
588
  createArticle: () => createArticle,
@@ -601,6 +603,7 @@ __export(index_exports, {
601
603
  gitToplevel: () => gitToplevel,
602
604
  initRepoWiki: () => initRepoWiki,
603
605
  installPlugin: () => installPlugin,
606
+ invalidateLinkGraph: () => invalidateLinkGraph,
604
607
  isLinkedWorktree: () => isLinkedWorktree,
605
608
  listArticleTree: () => listArticleTree,
606
609
  listAvailablePlugins: () => listAvailablePlugins,
@@ -609,6 +612,7 @@ __export(index_exports, {
609
612
  listPlugins: () => listPlugins,
610
613
  listSearchModes: () => listSearchModes,
611
614
  loadSearchProvider: () => loadSearchProvider,
615
+ localGraph: () => localGraph,
612
616
  nativeSearch: () => nativeSearch,
613
617
  parseAddTarget: () => parseAddTarget,
614
618
  parseArticle: () => parseArticle,
@@ -726,22 +730,39 @@ var YAML_ENGINE = {
726
730
  stringify: (data) => (0, import_yaml.stringify)(data)
727
731
  };
728
732
  var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
733
+ function coerceAudience(value) {
734
+ return value === "private" ? "private" : "public";
735
+ }
736
+ function coerceStatus(value) {
737
+ return value === "draft" ? "draft" : "published";
738
+ }
729
739
  function parseFallback(raw) {
730
740
  const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
731
741
  const fenceMatch = fenceRe.exec(raw);
732
742
  let title = "";
743
+ let audience = "public";
744
+ let status = "published";
733
745
  let body;
746
+ const clean = (s) => s.trim().replace(/^["']|["']$/g, "");
734
747
  if (fenceMatch) {
735
748
  const inner = fenceMatch[1];
736
749
  const titleMatch = /^title:\s*(.+)$/m.exec(inner);
737
750
  if (titleMatch) {
738
- title = titleMatch[1].trim().replace(/^["']|["']$/g, "");
751
+ title = clean(titleMatch[1]);
752
+ }
753
+ const audienceMatch = /^audience:\s*(.+)$/m.exec(inner);
754
+ if (audienceMatch) {
755
+ audience = coerceAudience(clean(audienceMatch[1]));
756
+ }
757
+ const statusMatch = /^status:\s*(.+)$/m.exec(inner);
758
+ if (statusMatch) {
759
+ status = coerceStatus(clean(statusMatch[1]));
739
760
  }
740
761
  body = raw.slice(fenceMatch[0].length);
741
762
  } else {
742
763
  body = raw;
743
764
  }
744
- return { title, tags: [], body: body.replace(/^\n+/, "") };
765
+ return { title, tags: [], audience, status, body: body.replace(/^\n+/, "") };
745
766
  }
746
767
  function parseArticle(raw) {
747
768
  try {
@@ -749,13 +770,17 @@ function parseArticle(raw) {
749
770
  const title = typeof data.title === "string" ? data.title : "";
750
771
  const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
751
772
  const created = typeof data.created === "string" ? data.created : void 0;
752
- return { title, tags, created, body: content.replace(/^\n+/, "") };
773
+ const audience = coerceAudience(data.audience);
774
+ const status = coerceStatus(data.status);
775
+ return { title, tags, created, audience, status, body: content.replace(/^\n+/, "") };
753
776
  } catch {
754
777
  return parseFallback(raw);
755
778
  }
756
779
  }
757
780
  function serializeArticle(article) {
758
781
  const front = { title: article.title, tags: article.tags };
782
+ if (article.audience === "private") front.audience = "private";
783
+ if (article.status === "draft") front.status = "draft";
759
784
  return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
760
785
  }
761
786
 
@@ -836,7 +861,17 @@ async function loadArticle(target) {
836
861
  async function updateArticleBody(target, body) {
837
862
  const { filePath, dir, raw, collection } = await loadArticle(target);
838
863
  const parsed = parseArticle(raw);
839
- await import_promises5.default.writeFile(filePath, serializeArticle({ title: parsed.title, tags: parsed.tags, body }), "utf8");
864
+ await import_promises5.default.writeFile(
865
+ filePath,
866
+ serializeArticle({
867
+ title: parsed.title,
868
+ tags: parsed.tags,
869
+ audience: parsed.audience,
870
+ status: parsed.status,
871
+ body
872
+ }),
873
+ "utf8"
874
+ );
840
875
  void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
841
876
  return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
842
877
  }
@@ -844,7 +879,17 @@ async function mutateTags(target, fn) {
844
879
  const { filePath, dir, raw, collection } = await loadArticle(target);
845
880
  const parsed = parseArticle(raw);
846
881
  const tags = fn(parsed.tags);
847
- await import_promises5.default.writeFile(filePath, serializeArticle({ title: parsed.title, tags, body: parsed.body }), "utf8");
882
+ await import_promises5.default.writeFile(
883
+ filePath,
884
+ serializeArticle({
885
+ title: parsed.title,
886
+ tags,
887
+ audience: parsed.audience,
888
+ status: parsed.status,
889
+ body: parsed.body
890
+ }),
891
+ "utf8"
892
+ );
848
893
  void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
849
894
  return { title: parsed.title, tags, path: filePath, collection };
850
895
  }
@@ -943,8 +988,8 @@ function isEpochish(d) {
943
988
  }
944
989
  var timestampCache = /* @__PURE__ */ new Map();
945
990
  async function fileTimestamps(absPath) {
946
- const cached = timestampCache.get(absPath);
947
- if (cached) return cached;
991
+ const cached2 = timestampCache.get(absPath);
992
+ if (cached2) return cached2;
948
993
  const dir = import_node_path4.default.dirname(absPath);
949
994
  const base = import_node_path4.default.basename(absPath);
950
995
  const [addLog, lastLog] = await Promise.all([
@@ -1165,20 +1210,30 @@ function pickIndexFile(names) {
1165
1210
  };
1166
1211
  return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
1167
1212
  }
1168
- async function cheapTitle(absPath) {
1213
+ var CHEAP_META_BYTES = 8192;
1214
+ async function cheapMeta(absPath) {
1169
1215
  let handle;
1170
1216
  try {
1171
1217
  handle = await import_promises8.default.open(absPath, "r");
1172
- const buf = Buffer.alloc(512);
1173
- const { bytesRead } = await handle.read(buf, 0, 512, 0);
1218
+ const buf = Buffer.alloc(CHEAP_META_BYTES);
1219
+ const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
1174
1220
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
1175
- const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
1176
- if (m) return m[1].trim().replace(/^["']|["']$/g, "");
1221
+ const fence = /^---[ \t]*\n([\s\S]*?)(?:\n---[ \t]*\n|$)/.exec(snippet);
1222
+ if (!fence) return {};
1223
+ const inner = fence[1];
1224
+ const clean = (s) => s.trim().replace(/^["']|["']$/g, "");
1225
+ const titleM = /^title:\s*(.+)$/m.exec(inner);
1226
+ const audienceM = /^audience:\s*(.+)$/m.exec(inner);
1227
+ const statusM = /^status:\s*(.+)$/m.exec(inner);
1228
+ const title = titleM ? clean(titleM[1]) : void 0;
1229
+ const audience = audienceM && clean(audienceM[1]) === "private" ? "private" : void 0;
1230
+ const status = statusM && clean(statusM[1]) === "draft" ? "draft" : void 0;
1231
+ return { title, audience, status };
1177
1232
  } catch {
1178
1233
  } finally {
1179
1234
  await handle?.close();
1180
1235
  }
1181
- return void 0;
1236
+ return {};
1182
1237
  }
1183
1238
  async function enrichTitles(nodes, collDir) {
1184
1239
  return Promise.all(
@@ -1186,21 +1241,41 @@ async function enrichTitles(nodes, collDir) {
1186
1241
  if (node.children) {
1187
1242
  const children = await enrichTitles(node.children, collDir);
1188
1243
  let title = node.title;
1244
+ let audience = node.audience;
1245
+ let status = node.status;
1189
1246
  if (node.slug) {
1190
1247
  const dir = import_node_path6.default.join(collDir, node.slug);
1191
1248
  const idx = pickIndexFile(await readdirSafe(dir));
1192
- if (idx) title = await cheapTitle(import_node_path6.default.join(dir, idx)) ?? title;
1249
+ if (idx) {
1250
+ const meta = await cheapMeta(import_node_path6.default.join(dir, idx));
1251
+ title = meta.title ?? title;
1252
+ audience = meta.audience ?? audience;
1253
+ status = meta.status ?? status;
1254
+ }
1193
1255
  }
1194
- return { ...node, title, children };
1256
+ return { ...node, title, audience, status, children };
1195
1257
  }
1196
1258
  for (const ext of [".mdx", ".md"]) {
1197
- const title = await cheapTitle(import_node_path6.default.join(collDir, `${node.slug}${ext}`));
1198
- if (title) return { ...node, title };
1259
+ const meta = await cheapMeta(import_node_path6.default.join(collDir, `${node.slug}${ext}`));
1260
+ if (meta.title || meta.audience || meta.status) {
1261
+ return {
1262
+ ...node,
1263
+ title: meta.title ?? node.title,
1264
+ audience: meta.audience ?? node.audience,
1265
+ status: meta.status ?? node.status
1266
+ };
1267
+ }
1199
1268
  }
1200
1269
  return node;
1201
1270
  })
1202
1271
  );
1203
1272
  }
1273
+ async function collectionMeta(collDir) {
1274
+ const idx = pickIndexFile(await readdirSafe(collDir));
1275
+ if (!idx) return {};
1276
+ const meta = await cheapMeta(import_node_path6.default.join(collDir, idx));
1277
+ return { audience: meta.audience, status: meta.status };
1278
+ }
1204
1279
  async function listArticleTree(coll) {
1205
1280
  const entry = await findCollection(coll);
1206
1281
  if (!entry) throw new CollectionNotFoundError(coll);
@@ -1242,7 +1317,17 @@ async function readArticle(coll, slug) {
1242
1317
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1243
1318
  const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
1244
1319
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1245
- return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs, isIndex };
1320
+ return {
1321
+ coll,
1322
+ slug: safeSlug,
1323
+ title,
1324
+ tags: parsed.tags,
1325
+ audience: parsed.audience,
1326
+ status: parsed.status,
1327
+ body: parsed.body,
1328
+ path: abs,
1329
+ isIndex
1330
+ };
1246
1331
  }
1247
1332
 
1248
1333
  // src/render.ts
@@ -1275,10 +1360,15 @@ function rehypeRewriteLinks(ctx) {
1275
1360
  if (pathPart === "") return;
1276
1361
  const joined = import_node_path7.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1277
1362
  const normalized = import_node_path7.default.posix.normalize(joined);
1363
+ const toRoute = (p) => p.replace(/\.(mdx?)$/i, "").replace(/(^|\/)(index|readme)$/i, "$1").replace(/\/+$/, "").replace(/^\/+/, "");
1364
+ if (normalized.startsWith("../")) {
1365
+ const sibling = normalized.slice(3);
1366
+ if (sibling === "" || sibling.startsWith("..")) return;
1367
+ node.properties = { ...node.properties, href: `/${toRoute(sibling)}${suffix}` };
1368
+ return;
1369
+ }
1278
1370
  if (normalized.startsWith("..")) return;
1279
- let resolved = normalized.replace(/\.(mdx?)$/i, "");
1280
- resolved = resolved.replace(/(^|\/)(index|readme)$/i, "$1");
1281
- resolved = resolved.replace(/\/+$/, "").replace(/^\/+/, "");
1371
+ const resolved = toRoute(normalized);
1282
1372
  node.properties = {
1283
1373
  ...node.properties,
1284
1374
  href: `/${ctx.coll}${resolved ? `/${resolved}` : ""}${suffix}`
@@ -1304,6 +1394,56 @@ function rehypeCollectToc(toc) {
1304
1394
  });
1305
1395
  };
1306
1396
  }
1397
+ function isExternalHref(href) {
1398
+ if (!href) return true;
1399
+ if (href.startsWith("#")) return true;
1400
+ if (href.startsWith("//")) return true;
1401
+ return /^[a-z][a-z0-9+.-]*:/i.test(href);
1402
+ }
1403
+ function resolveHref(href, ctx) {
1404
+ if (isExternalHref(href)) return null;
1405
+ const clean = (p) => p.replace(/[?#].*$/, "").replace(/\.(mdx?|html)$/i, "").replace(/\/+$/, "");
1406
+ const target = clean(href);
1407
+ if (!target) return null;
1408
+ if (target.startsWith("/")) {
1409
+ const base = ctx.base ?? process.env.DIFFWIKI_BASE ?? "";
1410
+ const baseSegs2 = base.split("/").filter(Boolean);
1411
+ let segs = target.split("/").filter(Boolean);
1412
+ if (baseSegs2.length > 0 && baseSegs2.every((b, i) => segs[i] === b)) {
1413
+ segs = segs.slice(baseSegs2.length);
1414
+ }
1415
+ if (segs.length < 2) return null;
1416
+ return segs.join("/");
1417
+ }
1418
+ const dir = ctx.isIndex ? ctx.slug : ctx.slug.includes("/") ? ctx.slug.slice(0, ctx.slug.lastIndexOf("/")) : "";
1419
+ const baseSegs = dir ? dir.split("/") : [];
1420
+ const out = [...baseSegs];
1421
+ for (const seg of target.split("/")) {
1422
+ if (seg === "" || seg === ".") continue;
1423
+ if (seg === "..") {
1424
+ if (out.length === 0) return null;
1425
+ out.pop();
1426
+ continue;
1427
+ }
1428
+ out.push(seg);
1429
+ }
1430
+ if (out.length === 0) return null;
1431
+ return `${ctx.coll}/${out.join("/")}`;
1432
+ }
1433
+ function rehypeCollectLinks(links, ctx) {
1434
+ const seen = /* @__PURE__ */ new Set();
1435
+ return (tree) => {
1436
+ (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1437
+ if (node.tagName !== "a") return;
1438
+ const href = node.properties?.href;
1439
+ if (typeof href !== "string") return;
1440
+ const id = resolveHref(href, ctx);
1441
+ if (!id || seen.has(id)) return;
1442
+ seen.add(id);
1443
+ links.push(id);
1444
+ });
1445
+ };
1446
+ }
1307
1447
  function rehypeStripLeadingH1() {
1308
1448
  return (tree) => {
1309
1449
  let removed = false;
@@ -1331,26 +1471,134 @@ function rehypeMarkMermaid() {
1331
1471
  }
1332
1472
  async function renderArticle(body, ctx) {
1333
1473
  const toc = [];
1334
- const file = await (0, import_unified.unified)().use(import_remark_parse.default).use(import_remark_gfm.default).use(import_remark_rehype.default).use(import_rehype_slug.default).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(rehypeRewriteLinks, ctx).use(import_rehype.default, {
1474
+ const links = [];
1475
+ const pipeline = (0, import_unified.unified)().use(import_remark_parse.default).use(import_remark_gfm.default).use(import_remark_rehype.default).use(import_rehype_slug.default).use(rehypeCollectToc, toc).use(rehypeCollectLinks, links, ctx);
1476
+ const file = await pipeline.use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(rehypeRewriteLinks, ctx).use(import_rehype.default, {
1335
1477
  themes: { light: "github-light", dark: "github-dark" },
1336
1478
  defaultColor: false,
1337
1479
  langAlias: LANG_ALIASES,
1338
1480
  fallbackLanguage: "plaintext"
1339
1481
  }).use(import_rehype_stringify.default).process(body);
1340
- return { html: String(file), toc };
1482
+ return { html: String(file), toc, links };
1483
+ }
1484
+
1485
+ // src/graph.ts
1486
+ init_collections();
1487
+ init_errors();
1488
+ function tagId(tag) {
1489
+ return `tag:${tag}`;
1490
+ }
1491
+ async function collectArticle(coll, slug, fallbackTitle, outNodes, outEdges, outTags) {
1492
+ let art;
1493
+ try {
1494
+ art = await readArticle(coll, slug);
1495
+ } catch (err) {
1496
+ if (err instanceof ArticleNotFoundError) return;
1497
+ throw err;
1498
+ }
1499
+ const id = `${coll}/${slug}`;
1500
+ outNodes.push({ id, kind: "article", coll, slug, title: art.title ?? fallbackTitle ?? slug });
1501
+ const { links } = await renderArticle(art.body, { coll, slug, isIndex: art.isIndex });
1502
+ for (const target of links) outEdges.push({ source: id, target, kind: "link" });
1503
+ for (const tag of art.tags) outTags.push({ source: id, tag });
1504
+ }
1505
+ async function collectCollection(coll, nodes, outNodes, outEdges, outTags) {
1506
+ for (const n of nodes) {
1507
+ if (n.slug) await collectArticle(coll, n.slug, n.title ?? n.name, outNodes, outEdges, outTags);
1508
+ if (n.children) await collectCollection(coll, n.children, outNodes, outEdges, outTags);
1509
+ }
1510
+ }
1511
+ var cached;
1512
+ function invalidateLinkGraph() {
1513
+ cached = void 0;
1514
+ }
1515
+ async function buildLinkGraph() {
1516
+ if (cached) return cached;
1517
+ const nodes = [];
1518
+ const rawEdges = [];
1519
+ const rawTags = [];
1520
+ for (const e of await listCollections()) {
1521
+ try {
1522
+ await collectArticle(e.name, "", e.name, nodes, rawEdges, rawTags);
1523
+ await collectCollection(e.name, await listArticleTree(e.name), nodes, rawEdges, rawTags);
1524
+ } catch {
1525
+ }
1526
+ }
1527
+ const nodeIds = new Set(nodes.map((n) => n.id));
1528
+ const seen = /* @__PURE__ */ new Set();
1529
+ const edges = [];
1530
+ for (const edge of rawEdges) {
1531
+ if (edge.source === edge.target) continue;
1532
+ if (!nodeIds.has(edge.target)) continue;
1533
+ const key = `${edge.source} ${edge.target}`;
1534
+ if (seen.has(key)) continue;
1535
+ seen.add(key);
1536
+ edges.push(edge);
1537
+ }
1538
+ const tagSeen = /* @__PURE__ */ new Set();
1539
+ for (const { source, tag } of rawTags) {
1540
+ if (!nodeIds.has(source)) continue;
1541
+ const id = tagId(tag);
1542
+ if (!tagSeen.has(id)) {
1543
+ tagSeen.add(id);
1544
+ nodes.push({ id, kind: "tag", coll: "", slug: "", title: `#${tag}` });
1545
+ }
1546
+ const key = `${source} ${id}`;
1547
+ if (seen.has(key)) continue;
1548
+ seen.add(key);
1549
+ edges.push({ source, target: id, kind: "tag" });
1550
+ }
1551
+ cached = { nodes, edges };
1552
+ return cached;
1553
+ }
1554
+ function localGraph(graph, focusId, include, siblingCap = 12) {
1555
+ const scoped = include ? {
1556
+ // Tag hubs survive the filter — never a page, so they can't 404.
1557
+ nodes: graph.nodes.filter((n) => n.kind === "tag" || include.has(n.coll)),
1558
+ edges: graph.edges
1559
+ } : graph;
1560
+ const byId = new Map(scoped.nodes.map((n) => [n.id, n]));
1561
+ const focus = byId.get(focusId);
1562
+ if (!focus) return { focus: focusId, nodes: [], edges: [] };
1563
+ const keep = /* @__PURE__ */ new Set([focusId]);
1564
+ for (const e of scoped.edges) {
1565
+ if (e.source === focusId && byId.has(e.target)) keep.add(e.target);
1566
+ if (e.target === focusId && byId.has(e.source)) keep.add(e.source);
1567
+ }
1568
+ const focusTagHubs = new Set([...keep].filter((id) => byId.get(id)?.kind === "tag"));
1569
+ let siblings = 0;
1570
+ for (const e of scoped.edges) {
1571
+ if (siblings >= siblingCap) break;
1572
+ if (e.kind === "tag" && focusTagHubs.has(e.target) && byId.has(e.source) && !keep.has(e.source)) {
1573
+ keep.add(e.source);
1574
+ siblings++;
1575
+ }
1576
+ }
1577
+ const nodes = scoped.nodes.filter((n) => keep.has(n.id));
1578
+ const edges = scoped.edges.filter((e) => keep.has(e.source) && keep.has(e.target));
1579
+ return { focus: focusId, nodes, edges };
1341
1580
  }
1342
1581
 
1343
1582
  // src/site.ts
1344
1583
  init_errors();
1345
1584
  var isStatic = () => process.env.DIFFWIKI_STATIC === "1";
1346
1585
  function toTreeNodes(coll, nodes) {
1347
- return nodes.map(
1586
+ const visible = isStatic() ? nodes.filter((n) => n.audience !== "private" && n.status !== "draft") : nodes;
1587
+ return visible.map(
1348
1588
  (n) => n.children ? {
1349
1589
  name: n.name,
1350
1590
  title: n.title ?? n.name,
1351
1591
  path: n.slug ? `/${coll}/${n.slug}` : void 0,
1592
+ ...n.audience ? { audience: n.audience } : {},
1593
+ ...n.status ? { status: n.status } : {},
1352
1594
  children: toTreeNodes(coll, n.children)
1353
- } : { name: n.name, title: n.title ?? n.name, path: `/${coll}/${n.slug}` }
1595
+ } : {
1596
+ name: n.name,
1597
+ title: n.title ?? n.name,
1598
+ path: `/${coll}/${n.slug}`,
1599
+ ...n.audience ? { audience: n.audience } : {},
1600
+ ...n.status ? { status: n.status } : {}
1601
+ }
1354
1602
  );
1355
1603
  }
1356
1604
  async function gitInfo(e) {
@@ -1363,23 +1611,40 @@ async function gitInfo(e) {
1363
1611
  }
1364
1612
  async function buildCollections() {
1365
1613
  const entries = await listCollections();
1366
- return Promise.all(
1367
- entries.map(async (e) => ({
1368
- name: e.name,
1369
- type: e.type,
1370
- path: e.path,
1371
- ...await gitInfo(e)
1372
- }))
1614
+ const infos = await Promise.all(
1615
+ entries.map(async (e) => {
1616
+ const { audience, status } = await collectionMeta(e.path);
1617
+ return {
1618
+ name: e.name,
1619
+ type: e.type,
1620
+ path: e.path,
1621
+ ...await gitInfo(e),
1622
+ ...audience ? { audience } : {},
1623
+ ...status ? { status } : {}
1624
+ };
1625
+ })
1373
1626
  );
1627
+ return isStatic() ? infos.filter((c) => c.audience !== "private" && c.status !== "draft") : infos;
1374
1628
  }
1375
1629
  async function buildNavTree() {
1376
1630
  const entries = await listCollections();
1377
- return Promise.all(
1378
- entries.map(async (e) => ({
1379
- collection: { name: e.name, type: e.type, path: e.path, ...await gitInfo(e) },
1380
- nodes: toTreeNodes(e.name, await listArticleTree(e.name))
1381
- }))
1631
+ const trees = await Promise.all(
1632
+ entries.map(async (e) => {
1633
+ const { audience, status } = await collectionMeta(e.path);
1634
+ return {
1635
+ collection: {
1636
+ name: e.name,
1637
+ type: e.type,
1638
+ path: e.path,
1639
+ ...await gitInfo(e),
1640
+ ...audience ? { audience } : {},
1641
+ ...status ? { status } : {}
1642
+ },
1643
+ nodes: toTreeNodes(e.name, await listArticleTree(e.name))
1644
+ };
1645
+ })
1382
1646
  );
1647
+ return isStatic() ? trees.filter((t) => t.collection.audience !== "private" && t.collection.status !== "draft") : trees;
1383
1648
  }
1384
1649
  function findNode(nodes, slug) {
1385
1650
  for (const n of nodes) {
@@ -1391,7 +1656,7 @@ function findNode(nodes, slug) {
1391
1656
  }
1392
1657
  return void 0;
1393
1658
  }
1394
- async function resolvePage(coll, slug) {
1659
+ async function resolvePage(coll, slug, opts) {
1395
1660
  try {
1396
1661
  const art = await readArticle(coll, slug);
1397
1662
  const { html, toc } = await renderArticle(art.body, {
@@ -1400,17 +1665,21 @@ async function resolvePage(coll, slug) {
1400
1665
  isIndex: art.isIndex
1401
1666
  });
1402
1667
  const { created, updated } = await fileTimestamps(art.path);
1668
+ const graph = localGraph(await buildLinkGraph(), `${art.coll}/${art.slug}`, opts?.include);
1403
1669
  return {
1404
1670
  kind: "doc",
1405
1671
  coll: art.coll,
1406
1672
  slug: art.slug,
1407
1673
  title: art.title,
1408
1674
  tags: art.tags,
1675
+ ...art.audience === "private" ? { audience: art.audience } : {},
1676
+ ...art.status === "draft" ? { status: art.status } : {},
1409
1677
  html,
1410
1678
  toc,
1411
1679
  path: `/${art.coll}/${art.slug}`,
1412
1680
  created,
1413
- updated
1681
+ updated,
1682
+ graph
1414
1683
  };
1415
1684
  } catch (err) {
1416
1685
  if (err instanceof CollectionNotFoundError) return null;
@@ -1654,6 +1923,7 @@ async function query(term, opts) {
1654
1923
  var import_promises10 = __toESM(require("fs/promises"), 1);
1655
1924
  var import_node_path9 = __toESM(require("path"), 1);
1656
1925
  init_collections();
1926
+ var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
1657
1927
  async function readArticleParts(collDir, slug) {
1658
1928
  for (const ext of [".mdx", ".md"]) {
1659
1929
  try {
@@ -1666,6 +1936,7 @@ async function readArticleParts(collDir, slug) {
1666
1936
  }
1667
1937
  async function flatten(coll, collDir, nodes, out) {
1668
1938
  for (const n of nodes) {
1939
+ if (isStatic2() && (n.audience === "private" || n.status === "draft")) continue;
1669
1940
  if (n.slug && !n.children) {
1670
1941
  const title = n.title ?? n.name;
1671
1942
  const { tags, body } = await readArticleParts(collDir, n.slug);
@@ -1692,6 +1963,10 @@ async function buildSearchIndex(collection) {
1692
1963
  try {
1693
1964
  const collEntry = await findCollection(e.name);
1694
1965
  const collDir = import_node_path9.default.resolve(collEntry?.path ?? e.path);
1966
+ if (isStatic2()) {
1967
+ const cm = await collectionMeta(collDir);
1968
+ if (cm.audience === "private" || cm.status === "draft") continue;
1969
+ }
1695
1970
  await flatten(e.name, collDir, await listArticleTree(e.name), out);
1696
1971
  } catch {
1697
1972
  }
@@ -2097,10 +2372,12 @@ init_registry2();
2097
2372
  addTags,
2098
2373
  buildArticleTree,
2099
2374
  buildCollections,
2375
+ buildLinkGraph,
2100
2376
  buildNavTree,
2101
2377
  buildSearchIndex,
2102
2378
  collectionDir,
2103
2379
  collectionGitStatus,
2380
+ collectionMeta,
2104
2381
  collectionsDir,
2105
2382
  configPath,
2106
2383
  createArticle,
@@ -2121,6 +2398,7 @@ init_registry2();
2121
2398
  gitToplevel,
2122
2399
  initRepoWiki,
2123
2400
  installPlugin,
2401
+ invalidateLinkGraph,
2124
2402
  isLinkedWorktree,
2125
2403
  listArticleTree,
2126
2404
  listAvailablePlugins,
@@ -2129,6 +2407,7 @@ init_registry2();
2129
2407
  listPlugins,
2130
2408
  listSearchModes,
2131
2409
  loadSearchProvider,
2410
+ localGraph,
2132
2411
  nativeSearch,
2133
2412
  parseAddTarget,
2134
2413
  parseArticle,