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 +318 -39
- package/dist/index.d.cts +142 -13
- package/dist/index.d.ts +142 -13
- package/dist/index.js +312 -39
- package/package.json +1 -1
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]
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
947
|
-
if (
|
|
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
|
-
|
|
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(
|
|
1173
|
-
const { bytesRead } = await handle.read(buf, 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
|
|
1176
|
-
if (
|
|
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
|
|
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)
|
|
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
|
|
1198
|
-
if (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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
} : {
|
|
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
|
-
|
|
1367
|
-
entries.map(async (e) =>
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1378
|
-
entries.map(async (e) =>
|
|
1379
|
-
|
|
1380
|
-
|
|
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,
|