diffwiki-core 0.6.0 → 0.6.1-rc.202607250144.696649b
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 +281 -73
- package/dist/index.d.cts +138 -10
- package/dist/index.d.ts +138 -10
- package/dist/index.js +274 -72
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -577,6 +577,7 @@ __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,
|
|
@@ -591,6 +592,7 @@ __export(index_exports, {
|
|
|
591
592
|
emitPluginEvent: () => emitPluginEvent,
|
|
592
593
|
ensureGitRepo: () => ensureGitRepo,
|
|
593
594
|
ensurePluginRoot: () => ensurePluginRoot,
|
|
595
|
+
fileTimestamps: () => fileTimestamps,
|
|
594
596
|
findCollection: () => findCollection,
|
|
595
597
|
findCollectionById: () => findCollectionById,
|
|
596
598
|
findEnabledPlugin: () => findEnabledPlugin,
|
|
@@ -600,6 +602,7 @@ __export(index_exports, {
|
|
|
600
602
|
gitToplevel: () => gitToplevel,
|
|
601
603
|
initRepoWiki: () => initRepoWiki,
|
|
602
604
|
installPlugin: () => installPlugin,
|
|
605
|
+
invalidateLinkGraph: () => invalidateLinkGraph,
|
|
603
606
|
isLinkedWorktree: () => isLinkedWorktree,
|
|
604
607
|
listArticleTree: () => listArticleTree,
|
|
605
608
|
listAvailablePlugins: () => listAvailablePlugins,
|
|
@@ -608,6 +611,7 @@ __export(index_exports, {
|
|
|
608
611
|
listPlugins: () => listPlugins,
|
|
609
612
|
listSearchModes: () => listSearchModes,
|
|
610
613
|
loadSearchProvider: () => loadSearchProvider,
|
|
614
|
+
localGraph: () => localGraph,
|
|
611
615
|
nativeSearch: () => nativeSearch,
|
|
612
616
|
parseAddTarget: () => parseAddTarget,
|
|
613
617
|
parseArticle: () => parseArticle,
|
|
@@ -755,8 +759,6 @@ function parseArticle(raw) {
|
|
|
755
759
|
}
|
|
756
760
|
function serializeArticle(article) {
|
|
757
761
|
const front = { title: article.title, tags: article.tags };
|
|
758
|
-
if (article.created) front.created = article.created;
|
|
759
|
-
if (article.updated) front.updated = article.updated;
|
|
760
762
|
return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
|
|
761
763
|
}
|
|
762
764
|
|
|
@@ -816,11 +818,10 @@ async function createArticle(opts) {
|
|
|
816
818
|
const { collection, title } = parseAddTarget(opts.target);
|
|
817
819
|
const { name, dir } = await resolveCollectionDir(collection);
|
|
818
820
|
const filePath = import_node_path3.default.join(dir, `${slugify(title)}.md`);
|
|
819
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
820
821
|
const body = opts.body ?? `# ${title}
|
|
821
822
|
`;
|
|
822
823
|
await import_promises5.default.mkdir(dir, { recursive: true });
|
|
823
|
-
await import_promises5.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags,
|
|
824
|
+
await import_promises5.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, body }), "utf8");
|
|
824
825
|
void fireHook2("article-created", name, import_node_path3.default.relative(dir, filePath));
|
|
825
826
|
return { title, tags: opts.tags, path: filePath, collection: name };
|
|
826
827
|
}
|
|
@@ -838,17 +839,7 @@ async function loadArticle(target) {
|
|
|
838
839
|
async function updateArticleBody(target, body) {
|
|
839
840
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
840
841
|
const parsed = parseArticle(raw);
|
|
841
|
-
await import_promises5.default.writeFile(
|
|
842
|
-
filePath,
|
|
843
|
-
serializeArticle({
|
|
844
|
-
title: parsed.title,
|
|
845
|
-
tags: parsed.tags,
|
|
846
|
-
created: parsed.created,
|
|
847
|
-
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
848
|
-
body
|
|
849
|
-
}),
|
|
850
|
-
"utf8"
|
|
851
|
-
);
|
|
842
|
+
await import_promises5.default.writeFile(filePath, serializeArticle({ title: parsed.title, tags: parsed.tags, body }), "utf8");
|
|
852
843
|
void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
|
|
853
844
|
return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
|
|
854
845
|
}
|
|
@@ -856,17 +847,7 @@ async function mutateTags(target, fn) {
|
|
|
856
847
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
857
848
|
const parsed = parseArticle(raw);
|
|
858
849
|
const tags = fn(parsed.tags);
|
|
859
|
-
await import_promises5.default.writeFile(
|
|
860
|
-
filePath,
|
|
861
|
-
serializeArticle({
|
|
862
|
-
title: parsed.title,
|
|
863
|
-
tags,
|
|
864
|
-
created: parsed.created,
|
|
865
|
-
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
866
|
-
body: parsed.body
|
|
867
|
-
}),
|
|
868
|
-
"utf8"
|
|
869
|
-
);
|
|
850
|
+
await import_promises5.default.writeFile(filePath, serializeArticle({ title: parsed.title, tags, body: parsed.body }), "utf8");
|
|
870
851
|
void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
|
|
871
852
|
return { title: parsed.title, tags, path: filePath, collection };
|
|
872
853
|
}
|
|
@@ -888,7 +869,7 @@ async function removeArticle(target) {
|
|
|
888
869
|
}
|
|
889
870
|
|
|
890
871
|
// src/repo.ts
|
|
891
|
-
var
|
|
872
|
+
var import_promises7 = __toESM(require("fs/promises"), 1);
|
|
892
873
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
893
874
|
var import_node_child_process3 = require("child_process");
|
|
894
875
|
var import_node_util2 = require("util");
|
|
@@ -897,6 +878,7 @@ init_collections();
|
|
|
897
878
|
init_registry();
|
|
898
879
|
|
|
899
880
|
// src/git.ts
|
|
881
|
+
var import_promises6 = __toESM(require("fs/promises"), 1);
|
|
900
882
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
901
883
|
var import_node_child_process2 = require("child_process");
|
|
902
884
|
var import_node_util = require("util");
|
|
@@ -959,6 +941,35 @@ async function ensureGitRepo(cwd) {
|
|
|
959
941
|
if (!after) throw new Error("git init did not produce a repository");
|
|
960
942
|
return after;
|
|
961
943
|
}
|
|
944
|
+
function isEpochish(d) {
|
|
945
|
+
return !Number.isFinite(d.getTime()) || d.getTime() <= 0;
|
|
946
|
+
}
|
|
947
|
+
var timestampCache = /* @__PURE__ */ new Map();
|
|
948
|
+
async function fileTimestamps(absPath) {
|
|
949
|
+
const cached2 = timestampCache.get(absPath);
|
|
950
|
+
if (cached2) return cached2;
|
|
951
|
+
const dir = import_node_path4.default.dirname(absPath);
|
|
952
|
+
const base = import_node_path4.default.basename(absPath);
|
|
953
|
+
const [addLog, lastLog] = await Promise.all([
|
|
954
|
+
git(dir, ["log", "--follow", "--diff-filter=A", "--format=%aI", "--", base]),
|
|
955
|
+
git(dir, ["log", "-1", "--format=%aI", "--", base])
|
|
956
|
+
]);
|
|
957
|
+
const gitCreated = addLog ? addLog.split("\n").filter(Boolean).pop() : void 0;
|
|
958
|
+
const gitUpdated = lastLog || void 0;
|
|
959
|
+
let created = gitCreated;
|
|
960
|
+
let updated = gitUpdated;
|
|
961
|
+
if (!created || !updated) {
|
|
962
|
+
try {
|
|
963
|
+
const st = await import_promises6.default.stat(absPath);
|
|
964
|
+
if (!updated) updated = st.mtime.toISOString();
|
|
965
|
+
if (!created) created = isEpochish(st.birthtime) ? st.mtime.toISOString() : st.birthtime.toISOString();
|
|
966
|
+
} catch {
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const result = { created, updated };
|
|
970
|
+
timestampCache.set(absPath, result);
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
962
973
|
|
|
963
974
|
// src/repo.ts
|
|
964
975
|
init_errors();
|
|
@@ -974,7 +985,7 @@ async function detectRepoName(cwd) {
|
|
|
974
985
|
}
|
|
975
986
|
async function readRepoConfig(cwd) {
|
|
976
987
|
try {
|
|
977
|
-
const raw = await
|
|
988
|
+
const raw = await import_promises7.default.readFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), "utf8");
|
|
978
989
|
const parsed = (0, import_yaml2.parse)(raw) ?? {};
|
|
979
990
|
if (!parsed.path) return void 0;
|
|
980
991
|
return {
|
|
@@ -1004,9 +1015,9 @@ async function initRepoWiki(opts) {
|
|
|
1004
1015
|
n += 1;
|
|
1005
1016
|
}
|
|
1006
1017
|
}
|
|
1007
|
-
await
|
|
1018
|
+
await import_promises7.default.mkdir(absWiki, { recursive: true });
|
|
1008
1019
|
const config = { collection: name, path: wikiPath };
|
|
1009
|
-
await
|
|
1020
|
+
await import_promises7.default.writeFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
|
|
1010
1021
|
const entry = {
|
|
1011
1022
|
name,
|
|
1012
1023
|
type: "repo",
|
|
@@ -1078,7 +1089,7 @@ async function collectionGitStatus(entry) {
|
|
|
1078
1089
|
init_collections();
|
|
1079
1090
|
|
|
1080
1091
|
// src/browse.ts
|
|
1081
|
-
var
|
|
1092
|
+
var import_promises8 = __toESM(require("fs/promises"), 1);
|
|
1082
1093
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
1083
1094
|
init_collections();
|
|
1084
1095
|
init_errors();
|
|
@@ -1125,7 +1136,7 @@ function buildArticleTree(relPaths) {
|
|
|
1125
1136
|
async function collectRelPaths(dir, rel = "") {
|
|
1126
1137
|
let dirents;
|
|
1127
1138
|
try {
|
|
1128
|
-
dirents = await
|
|
1139
|
+
dirents = await import_promises8.default.readdir(dir, { withFileTypes: true });
|
|
1129
1140
|
} catch {
|
|
1130
1141
|
return [];
|
|
1131
1142
|
}
|
|
@@ -1143,7 +1154,7 @@ async function collectRelPaths(dir, rel = "") {
|
|
|
1143
1154
|
}
|
|
1144
1155
|
async function readdirSafe(dir) {
|
|
1145
1156
|
try {
|
|
1146
|
-
return await
|
|
1157
|
+
return await import_promises8.default.readdir(dir);
|
|
1147
1158
|
} catch {
|
|
1148
1159
|
return [];
|
|
1149
1160
|
}
|
|
@@ -1160,7 +1171,7 @@ function pickIndexFile(names) {
|
|
|
1160
1171
|
async function cheapTitle(absPath) {
|
|
1161
1172
|
let handle;
|
|
1162
1173
|
try {
|
|
1163
|
-
handle = await
|
|
1174
|
+
handle = await import_promises8.default.open(absPath, "r");
|
|
1164
1175
|
const buf = Buffer.alloc(512);
|
|
1165
1176
|
const { bytesRead } = await handle.read(buf, 0, 512, 0);
|
|
1166
1177
|
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
@@ -1210,11 +1221,12 @@ async function readArticle(coll, slug) {
|
|
|
1210
1221
|
const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
1211
1222
|
const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path6.default.sep);
|
|
1212
1223
|
let abs;
|
|
1224
|
+
let isIndex = false;
|
|
1213
1225
|
for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
|
|
1214
1226
|
const candidate = import_node_path6.default.resolve(collDir, rel);
|
|
1215
1227
|
if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
|
|
1216
1228
|
try {
|
|
1217
|
-
await
|
|
1229
|
+
await import_promises8.default.access(candidate);
|
|
1218
1230
|
abs = candidate;
|
|
1219
1231
|
break;
|
|
1220
1232
|
} catch {
|
|
@@ -1224,16 +1236,20 @@ async function readArticle(coll, slug) {
|
|
|
1224
1236
|
const dir = import_node_path6.default.resolve(collDir, safeSlug);
|
|
1225
1237
|
if (inJail(dir)) {
|
|
1226
1238
|
const idx = pickIndexFile(await readdirSafe(dir));
|
|
1227
|
-
if (idx)
|
|
1239
|
+
if (idx) {
|
|
1240
|
+
abs = import_node_path6.default.join(dir, idx);
|
|
1241
|
+
isIndex = true;
|
|
1242
|
+
}
|
|
1228
1243
|
}
|
|
1229
1244
|
}
|
|
1230
1245
|
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
1231
|
-
const parsed = parseArticle(await
|
|
1246
|
+
const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
|
|
1232
1247
|
const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
|
|
1233
|
-
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
|
|
1248
|
+
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs, isIndex };
|
|
1234
1249
|
}
|
|
1235
1250
|
|
|
1236
1251
|
// src/render.ts
|
|
1252
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1237
1253
|
var import_unified = require("unified");
|
|
1238
1254
|
var import_remark_parse = __toESM(require("remark-parse"), 1);
|
|
1239
1255
|
var import_remark_gfm = __toESM(require("remark-gfm"), 1);
|
|
@@ -1243,6 +1259,36 @@ var import_rehype = __toESM(require("@shikijs/rehype"), 1);
|
|
|
1243
1259
|
var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
|
|
1244
1260
|
var import_unist_util_visit = require("unist-util-visit");
|
|
1245
1261
|
var import_hast_util_to_string = require("hast-util-to-string");
|
|
1262
|
+
function isNonRelativeHref(href) {
|
|
1263
|
+
if (href === "") return true;
|
|
1264
|
+
if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) return true;
|
|
1265
|
+
if (href.startsWith("//")) return true;
|
|
1266
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(href);
|
|
1267
|
+
}
|
|
1268
|
+
function rehypeRewriteLinks(ctx) {
|
|
1269
|
+
const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path7.default.posix.dirname(ctx.slug || ".");
|
|
1270
|
+
return (tree) => {
|
|
1271
|
+
(0, import_unist_util_visit.visit)(tree, "element", (node) => {
|
|
1272
|
+
if (node.tagName !== "a") return;
|
|
1273
|
+
const raw = node.properties?.href;
|
|
1274
|
+
if (typeof raw !== "string" || isNonRelativeHref(raw)) return;
|
|
1275
|
+
const suffixMatch = /[#?].*$/.exec(raw);
|
|
1276
|
+
const suffix = suffixMatch ? suffixMatch[0] : "";
|
|
1277
|
+
const pathPart = suffix ? raw.slice(0, raw.length - suffix.length) : raw;
|
|
1278
|
+
if (pathPart === "") return;
|
|
1279
|
+
const joined = import_node_path7.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
|
|
1280
|
+
const normalized = import_node_path7.default.posix.normalize(joined);
|
|
1281
|
+
if (normalized.startsWith("..")) return;
|
|
1282
|
+
let resolved = normalized.replace(/\.(mdx?)$/i, "");
|
|
1283
|
+
resolved = resolved.replace(/(^|\/)(index|readme)$/i, "$1");
|
|
1284
|
+
resolved = resolved.replace(/\/+$/, "").replace(/^\/+/, "");
|
|
1285
|
+
node.properties = {
|
|
1286
|
+
...node.properties,
|
|
1287
|
+
href: `/${ctx.coll}${resolved ? `/${resolved}` : ""}${suffix}`
|
|
1288
|
+
};
|
|
1289
|
+
});
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1246
1292
|
var LANG_ALIASES = {
|
|
1247
1293
|
"c#": "csharp",
|
|
1248
1294
|
cs: "csharp",
|
|
@@ -1261,6 +1307,56 @@ function rehypeCollectToc(toc) {
|
|
|
1261
1307
|
});
|
|
1262
1308
|
};
|
|
1263
1309
|
}
|
|
1310
|
+
function isExternalHref(href) {
|
|
1311
|
+
if (!href) return true;
|
|
1312
|
+
if (href.startsWith("#")) return true;
|
|
1313
|
+
if (href.startsWith("//")) return true;
|
|
1314
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(href);
|
|
1315
|
+
}
|
|
1316
|
+
function resolveHref(href, ctx) {
|
|
1317
|
+
if (isExternalHref(href)) return null;
|
|
1318
|
+
const clean = (p) => p.replace(/[?#].*$/, "").replace(/\.(mdx?|html)$/i, "").replace(/\/+$/, "");
|
|
1319
|
+
const target = clean(href);
|
|
1320
|
+
if (!target) return null;
|
|
1321
|
+
if (target.startsWith("/")) {
|
|
1322
|
+
const base = ctx.base ?? process.env.DIFFWIKI_BASE ?? "";
|
|
1323
|
+
const baseSegs2 = base.split("/").filter(Boolean);
|
|
1324
|
+
let segs = target.split("/").filter(Boolean);
|
|
1325
|
+
if (baseSegs2.length > 0 && baseSegs2.every((b, i) => segs[i] === b)) {
|
|
1326
|
+
segs = segs.slice(baseSegs2.length);
|
|
1327
|
+
}
|
|
1328
|
+
if (segs.length < 2) return null;
|
|
1329
|
+
return segs.join("/");
|
|
1330
|
+
}
|
|
1331
|
+
const dir = ctx.isIndex ? ctx.slug : ctx.slug.includes("/") ? ctx.slug.slice(0, ctx.slug.lastIndexOf("/")) : "";
|
|
1332
|
+
const baseSegs = dir ? dir.split("/") : [];
|
|
1333
|
+
const out = [...baseSegs];
|
|
1334
|
+
for (const seg of target.split("/")) {
|
|
1335
|
+
if (seg === "" || seg === ".") continue;
|
|
1336
|
+
if (seg === "..") {
|
|
1337
|
+
if (out.length === 0) return null;
|
|
1338
|
+
out.pop();
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
out.push(seg);
|
|
1342
|
+
}
|
|
1343
|
+
if (out.length === 0) return null;
|
|
1344
|
+
return `${ctx.coll}/${out.join("/")}`;
|
|
1345
|
+
}
|
|
1346
|
+
function rehypeCollectLinks(links, ctx) {
|
|
1347
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1348
|
+
return (tree) => {
|
|
1349
|
+
(0, import_unist_util_visit.visit)(tree, "element", (node) => {
|
|
1350
|
+
if (node.tagName !== "a") return;
|
|
1351
|
+
const href = node.properties?.href;
|
|
1352
|
+
if (typeof href !== "string") return;
|
|
1353
|
+
const id = resolveHref(href, ctx);
|
|
1354
|
+
if (!id || seen.has(id)) return;
|
|
1355
|
+
seen.add(id);
|
|
1356
|
+
links.push(id);
|
|
1357
|
+
});
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1264
1360
|
function rehypeStripLeadingH1() {
|
|
1265
1361
|
return (tree) => {
|
|
1266
1362
|
let removed = false;
|
|
@@ -1286,15 +1382,114 @@ function rehypeMarkMermaid() {
|
|
|
1286
1382
|
});
|
|
1287
1383
|
};
|
|
1288
1384
|
}
|
|
1289
|
-
async function renderArticle(body) {
|
|
1385
|
+
async function renderArticle(body, ctx) {
|
|
1290
1386
|
const toc = [];
|
|
1291
|
-
const
|
|
1387
|
+
const links = [];
|
|
1388
|
+
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);
|
|
1389
|
+
const file = await pipeline.use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(rehypeRewriteLinks, ctx).use(import_rehype.default, {
|
|
1292
1390
|
themes: { light: "github-light", dark: "github-dark" },
|
|
1293
1391
|
defaultColor: false,
|
|
1294
1392
|
langAlias: LANG_ALIASES,
|
|
1295
1393
|
fallbackLanguage: "plaintext"
|
|
1296
1394
|
}).use(import_rehype_stringify.default).process(body);
|
|
1297
|
-
return { html: String(file), toc };
|
|
1395
|
+
return { html: String(file), toc, links };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/graph.ts
|
|
1399
|
+
init_collections();
|
|
1400
|
+
init_errors();
|
|
1401
|
+
function tagId(tag) {
|
|
1402
|
+
return `tag:${tag}`;
|
|
1403
|
+
}
|
|
1404
|
+
async function collectArticle(coll, slug, fallbackTitle, outNodes, outEdges, outTags) {
|
|
1405
|
+
let art;
|
|
1406
|
+
try {
|
|
1407
|
+
art = await readArticle(coll, slug);
|
|
1408
|
+
} catch (err) {
|
|
1409
|
+
if (err instanceof ArticleNotFoundError) return;
|
|
1410
|
+
throw err;
|
|
1411
|
+
}
|
|
1412
|
+
const id = `${coll}/${slug}`;
|
|
1413
|
+
outNodes.push({ id, kind: "article", coll, slug, title: art.title ?? fallbackTitle ?? slug });
|
|
1414
|
+
const { links } = await renderArticle(art.body, { coll, slug, isIndex: art.isIndex });
|
|
1415
|
+
for (const target of links) outEdges.push({ source: id, target, kind: "link" });
|
|
1416
|
+
for (const tag of art.tags) outTags.push({ source: id, tag });
|
|
1417
|
+
}
|
|
1418
|
+
async function collectCollection(coll, nodes, outNodes, outEdges, outTags) {
|
|
1419
|
+
for (const n of nodes) {
|
|
1420
|
+
if (n.slug) await collectArticle(coll, n.slug, n.title ?? n.name, outNodes, outEdges, outTags);
|
|
1421
|
+
if (n.children) await collectCollection(coll, n.children, outNodes, outEdges, outTags);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
var cached;
|
|
1425
|
+
function invalidateLinkGraph() {
|
|
1426
|
+
cached = void 0;
|
|
1427
|
+
}
|
|
1428
|
+
async function buildLinkGraph() {
|
|
1429
|
+
if (cached) return cached;
|
|
1430
|
+
const nodes = [];
|
|
1431
|
+
const rawEdges = [];
|
|
1432
|
+
const rawTags = [];
|
|
1433
|
+
for (const e of await listCollections()) {
|
|
1434
|
+
try {
|
|
1435
|
+
await collectArticle(e.name, "", e.name, nodes, rawEdges, rawTags);
|
|
1436
|
+
await collectCollection(e.name, await listArticleTree(e.name), nodes, rawEdges, rawTags);
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
1441
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1442
|
+
const edges = [];
|
|
1443
|
+
for (const edge of rawEdges) {
|
|
1444
|
+
if (edge.source === edge.target) continue;
|
|
1445
|
+
if (!nodeIds.has(edge.target)) continue;
|
|
1446
|
+
const key = `${edge.source} ${edge.target}`;
|
|
1447
|
+
if (seen.has(key)) continue;
|
|
1448
|
+
seen.add(key);
|
|
1449
|
+
edges.push(edge);
|
|
1450
|
+
}
|
|
1451
|
+
const tagSeen = /* @__PURE__ */ new Set();
|
|
1452
|
+
for (const { source, tag } of rawTags) {
|
|
1453
|
+
if (!nodeIds.has(source)) continue;
|
|
1454
|
+
const id = tagId(tag);
|
|
1455
|
+
if (!tagSeen.has(id)) {
|
|
1456
|
+
tagSeen.add(id);
|
|
1457
|
+
nodes.push({ id, kind: "tag", coll: "", slug: "", title: `#${tag}` });
|
|
1458
|
+
}
|
|
1459
|
+
const key = `${source} ${id}`;
|
|
1460
|
+
if (seen.has(key)) continue;
|
|
1461
|
+
seen.add(key);
|
|
1462
|
+
edges.push({ source, target: id, kind: "tag" });
|
|
1463
|
+
}
|
|
1464
|
+
cached = { nodes, edges };
|
|
1465
|
+
return cached;
|
|
1466
|
+
}
|
|
1467
|
+
function localGraph(graph, focusId, include, siblingCap = 12) {
|
|
1468
|
+
const scoped = include ? {
|
|
1469
|
+
// Tag hubs survive the filter — never a page, so they can't 404.
|
|
1470
|
+
nodes: graph.nodes.filter((n) => n.kind === "tag" || include.has(n.coll)),
|
|
1471
|
+
edges: graph.edges
|
|
1472
|
+
} : graph;
|
|
1473
|
+
const byId = new Map(scoped.nodes.map((n) => [n.id, n]));
|
|
1474
|
+
const focus = byId.get(focusId);
|
|
1475
|
+
if (!focus) return { focus: focusId, nodes: [], edges: [] };
|
|
1476
|
+
const keep = /* @__PURE__ */ new Set([focusId]);
|
|
1477
|
+
for (const e of scoped.edges) {
|
|
1478
|
+
if (e.source === focusId && byId.has(e.target)) keep.add(e.target);
|
|
1479
|
+
if (e.target === focusId && byId.has(e.source)) keep.add(e.source);
|
|
1480
|
+
}
|
|
1481
|
+
const focusTagHubs = new Set([...keep].filter((id) => byId.get(id)?.kind === "tag"));
|
|
1482
|
+
let siblings = 0;
|
|
1483
|
+
for (const e of scoped.edges) {
|
|
1484
|
+
if (siblings >= siblingCap) break;
|
|
1485
|
+
if (e.kind === "tag" && focusTagHubs.has(e.target) && byId.has(e.source) && !keep.has(e.source)) {
|
|
1486
|
+
keep.add(e.source);
|
|
1487
|
+
siblings++;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const nodes = scoped.nodes.filter((n) => keep.has(n.id));
|
|
1491
|
+
const edges = scoped.edges.filter((e) => keep.has(e.source) && keep.has(e.target));
|
|
1492
|
+
return { focus: focusId, nodes, edges };
|
|
1298
1493
|
}
|
|
1299
1494
|
|
|
1300
1495
|
// src/site.ts
|
|
@@ -1348,10 +1543,16 @@ function findNode(nodes, slug) {
|
|
|
1348
1543
|
}
|
|
1349
1544
|
return void 0;
|
|
1350
1545
|
}
|
|
1351
|
-
async function resolvePage(coll, slug) {
|
|
1546
|
+
async function resolvePage(coll, slug, opts) {
|
|
1352
1547
|
try {
|
|
1353
1548
|
const art = await readArticle(coll, slug);
|
|
1354
|
-
const { html, toc } = await renderArticle(art.body
|
|
1549
|
+
const { html, toc } = await renderArticle(art.body, {
|
|
1550
|
+
coll: art.coll,
|
|
1551
|
+
slug: art.slug,
|
|
1552
|
+
isIndex: art.isIndex
|
|
1553
|
+
});
|
|
1554
|
+
const { created, updated } = await fileTimestamps(art.path);
|
|
1555
|
+
const graph = localGraph(await buildLinkGraph(), `${art.coll}/${art.slug}`, opts?.include);
|
|
1355
1556
|
return {
|
|
1356
1557
|
kind: "doc",
|
|
1357
1558
|
coll: art.coll,
|
|
@@ -1360,7 +1561,10 @@ async function resolvePage(coll, slug) {
|
|
|
1360
1561
|
tags: art.tags,
|
|
1361
1562
|
html,
|
|
1362
1563
|
toc,
|
|
1363
|
-
path: `/${art.coll}/${art.slug}
|
|
1564
|
+
path: `/${art.coll}/${art.slug}`,
|
|
1565
|
+
created,
|
|
1566
|
+
updated,
|
|
1567
|
+
graph
|
|
1364
1568
|
};
|
|
1365
1569
|
} catch (err) {
|
|
1366
1570
|
if (err instanceof CollectionNotFoundError) return null;
|
|
@@ -1418,8 +1622,8 @@ function resolveCollectionSelection(all, sel = {}) {
|
|
|
1418
1622
|
}
|
|
1419
1623
|
|
|
1420
1624
|
// src/query.ts
|
|
1421
|
-
var
|
|
1422
|
-
var
|
|
1625
|
+
var import_promises9 = __toESM(require("fs/promises"), 1);
|
|
1626
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1423
1627
|
init_collections();
|
|
1424
1628
|
init_errors();
|
|
1425
1629
|
init_logger();
|
|
@@ -1488,13 +1692,13 @@ var log2 = getLogger("query");
|
|
|
1488
1692
|
async function collectMarkdownFiles(dir) {
|
|
1489
1693
|
let entries;
|
|
1490
1694
|
try {
|
|
1491
|
-
entries = await
|
|
1695
|
+
entries = await import_promises9.default.readdir(dir, { withFileTypes: true });
|
|
1492
1696
|
} catch {
|
|
1493
1697
|
return [];
|
|
1494
1698
|
}
|
|
1495
1699
|
const files = [];
|
|
1496
1700
|
for (const entry of entries) {
|
|
1497
|
-
const full =
|
|
1701
|
+
const full = import_node_path8.default.join(dir, entry.name);
|
|
1498
1702
|
if (entry.isDirectory()) {
|
|
1499
1703
|
files.push(...await collectMarkdownFiles(full));
|
|
1500
1704
|
} else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
|
|
@@ -1513,12 +1717,12 @@ async function nativeSearch(term, collections, collection) {
|
|
|
1513
1717
|
const docs = [];
|
|
1514
1718
|
for (const entry of targets) {
|
|
1515
1719
|
for (const full of await collectMarkdownFiles(entry.path)) {
|
|
1516
|
-
const fallbackTitle =
|
|
1720
|
+
const fallbackTitle = import_node_path8.default.basename(full).replace(/\.mdx?$/, "");
|
|
1517
1721
|
let title = fallbackTitle;
|
|
1518
1722
|
let tags = [];
|
|
1519
1723
|
let body = "";
|
|
1520
1724
|
try {
|
|
1521
|
-
const parsed = parseArticle(await
|
|
1725
|
+
const parsed = parseArticle(await import_promises9.default.readFile(full, "utf8"));
|
|
1522
1726
|
if (parsed.title) title = parsed.title;
|
|
1523
1727
|
tags = parsed.tags;
|
|
1524
1728
|
body = parsed.body;
|
|
@@ -1601,13 +1805,13 @@ async function query(term, opts) {
|
|
|
1601
1805
|
}
|
|
1602
1806
|
|
|
1603
1807
|
// src/search.ts
|
|
1604
|
-
var
|
|
1605
|
-
var
|
|
1808
|
+
var import_promises10 = __toESM(require("fs/promises"), 1);
|
|
1809
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1606
1810
|
init_collections();
|
|
1607
1811
|
async function readArticleParts(collDir, slug) {
|
|
1608
1812
|
for (const ext of [".mdx", ".md"]) {
|
|
1609
1813
|
try {
|
|
1610
|
-
const parsed = parseArticle(await
|
|
1814
|
+
const parsed = parseArticle(await import_promises10.default.readFile(import_node_path9.default.join(collDir, `${slug}${ext}`), "utf8"));
|
|
1611
1815
|
return { tags: parsed.tags, body: parsed.body };
|
|
1612
1816
|
} catch {
|
|
1613
1817
|
}
|
|
@@ -1641,7 +1845,7 @@ async function buildSearchIndex(collection) {
|
|
|
1641
1845
|
for (const e of entries) {
|
|
1642
1846
|
try {
|
|
1643
1847
|
const collEntry = await findCollection(e.name);
|
|
1644
|
-
const collDir =
|
|
1848
|
+
const collDir = import_node_path9.default.resolve(collEntry?.path ?? e.path);
|
|
1645
1849
|
await flatten(e.name, collDir, await listArticleTree(e.name), out);
|
|
1646
1850
|
} catch {
|
|
1647
1851
|
}
|
|
@@ -1650,15 +1854,15 @@ async function buildSearchIndex(collection) {
|
|
|
1650
1854
|
}
|
|
1651
1855
|
|
|
1652
1856
|
// src/doctor.ts
|
|
1653
|
-
var
|
|
1857
|
+
var import_promises12 = __toESM(require("fs/promises"), 1);
|
|
1654
1858
|
init_paths();
|
|
1655
1859
|
init_registry();
|
|
1656
1860
|
init_collections();
|
|
1657
1861
|
|
|
1658
1862
|
// src/plugins/manager.ts
|
|
1659
|
-
var
|
|
1863
|
+
var import_promises11 = __toESM(require("fs/promises"), 1);
|
|
1660
1864
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
1661
|
-
var
|
|
1865
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1662
1866
|
init_paths();
|
|
1663
1867
|
init_errors();
|
|
1664
1868
|
init_collections();
|
|
@@ -1669,7 +1873,7 @@ init_host();
|
|
|
1669
1873
|
var import_node_child_process4 = require("child_process");
|
|
1670
1874
|
var import_node_util3 = require("util");
|
|
1671
1875
|
var import_node_fs = require("fs");
|
|
1672
|
-
var
|
|
1876
|
+
var import_node_path10 = require("path");
|
|
1673
1877
|
var import_node_url = require("url");
|
|
1674
1878
|
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
|
|
1675
1879
|
async function runProcess(file, args, opts) {
|
|
@@ -1685,7 +1889,7 @@ async function runProcess(file, args, opts) {
|
|
|
1685
1889
|
// src/plugins/manager.ts
|
|
1686
1890
|
async function readPackageJson(file) {
|
|
1687
1891
|
try {
|
|
1688
|
-
return JSON.parse(await
|
|
1892
|
+
return JSON.parse(await import_promises11.default.readFile(file, "utf8"));
|
|
1689
1893
|
} catch {
|
|
1690
1894
|
return null;
|
|
1691
1895
|
}
|
|
@@ -1701,7 +1905,7 @@ function binOf(manifest) {
|
|
|
1701
1905
|
async function pluginManifestDirs(nodeModules) {
|
|
1702
1906
|
let entries;
|
|
1703
1907
|
try {
|
|
1704
|
-
entries = await
|
|
1908
|
+
entries = await import_promises11.default.readdir(nodeModules);
|
|
1705
1909
|
} catch {
|
|
1706
1910
|
return [];
|
|
1707
1911
|
}
|
|
@@ -1711,44 +1915,44 @@ async function pluginManifestDirs(nodeModules) {
|
|
|
1711
1915
|
if (entry.startsWith("@")) {
|
|
1712
1916
|
let scoped;
|
|
1713
1917
|
try {
|
|
1714
|
-
scoped = await
|
|
1918
|
+
scoped = await import_promises11.default.readdir(import_node_path11.default.join(nodeModules, entry));
|
|
1715
1919
|
} catch {
|
|
1716
1920
|
continue;
|
|
1717
1921
|
}
|
|
1718
1922
|
for (const pkg of scoped) {
|
|
1719
1923
|
if (pkg.startsWith(".")) continue;
|
|
1720
|
-
dirs.push(
|
|
1924
|
+
dirs.push(import_node_path11.default.join(nodeModules, entry, pkg));
|
|
1721
1925
|
}
|
|
1722
1926
|
} else {
|
|
1723
|
-
dirs.push(
|
|
1927
|
+
dirs.push(import_node_path11.default.join(nodeModules, entry));
|
|
1724
1928
|
}
|
|
1725
1929
|
}
|
|
1726
1930
|
return dirs;
|
|
1727
1931
|
}
|
|
1728
1932
|
async function ensurePluginRoot() {
|
|
1729
1933
|
const dir = pluginsDir();
|
|
1730
|
-
await
|
|
1731
|
-
const pkgPath =
|
|
1934
|
+
await import_promises11.default.mkdir(dir, { recursive: true });
|
|
1935
|
+
const pkgPath = import_node_path11.default.join(dir, "package.json");
|
|
1732
1936
|
if (await readPackageJson(pkgPath) === null) {
|
|
1733
1937
|
const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
|
|
1734
|
-
await
|
|
1938
|
+
await import_promises11.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
1735
1939
|
`, "utf8");
|
|
1736
1940
|
}
|
|
1737
1941
|
return dir;
|
|
1738
1942
|
}
|
|
1739
1943
|
async function asSearchPackage(pkgDir) {
|
|
1740
|
-
const manifest = await readPackageJson(
|
|
1944
|
+
const manifest = await readPackageJson(import_node_path11.default.join(pkgDir, "package.json"));
|
|
1741
1945
|
if (!manifest?.name || manifest.diffwiki?.kind !== "search") return null;
|
|
1742
1946
|
const bin = binOf(manifest);
|
|
1743
1947
|
if (!bin) return null;
|
|
1744
|
-
return { name: manifest.name, version: manifest.version, binAbs:
|
|
1948
|
+
return { name: manifest.name, version: manifest.version, binAbs: import_node_path11.default.resolve(pkgDir, bin) };
|
|
1745
1949
|
}
|
|
1746
1950
|
async function packageNameFromSpec(spec) {
|
|
1747
1951
|
const looksLikePath = spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~") || spec.startsWith("file:");
|
|
1748
1952
|
if (looksLikePath) {
|
|
1749
1953
|
const raw = spec.startsWith("file:") ? spec.slice("file:".length) : spec;
|
|
1750
|
-
const abs =
|
|
1751
|
-
return (await readPackageJson(
|
|
1954
|
+
const abs = import_node_path11.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os2.default.homedir()));
|
|
1955
|
+
return (await readPackageJson(import_node_path11.default.join(abs, "package.json")))?.name ?? null;
|
|
1752
1956
|
}
|
|
1753
1957
|
if (/^(git\+|git:|https?:|ssh:)/.test(spec)) return null;
|
|
1754
1958
|
if (spec.startsWith("@")) {
|
|
@@ -1759,10 +1963,10 @@ async function packageNameFromSpec(spec) {
|
|
|
1759
1963
|
return at <= 0 ? spec : spec.slice(0, at);
|
|
1760
1964
|
}
|
|
1761
1965
|
async function resolveSearchPackage(dir, name) {
|
|
1762
|
-
return asSearchPackage(
|
|
1966
|
+
return asSearchPackage(import_node_path11.default.join(dir, "node_modules", ...name.split("/")));
|
|
1763
1967
|
}
|
|
1764
1968
|
async function discoverSearchPackage(dir) {
|
|
1765
|
-
const nodeModules =
|
|
1969
|
+
const nodeModules = import_node_path11.default.join(dir, "node_modules");
|
|
1766
1970
|
for (const pkgDir of await pluginManifestDirs(nodeModules)) {
|
|
1767
1971
|
const found = await asSearchPackage(pkgDir);
|
|
1768
1972
|
if (found) return found;
|
|
@@ -1894,7 +2098,7 @@ init_host();
|
|
|
1894
2098
|
init_types();
|
|
1895
2099
|
async function exists(target) {
|
|
1896
2100
|
try {
|
|
1897
|
-
await
|
|
2101
|
+
await import_promises12.default.access(target);
|
|
1898
2102
|
return true;
|
|
1899
2103
|
} catch {
|
|
1900
2104
|
return false;
|
|
@@ -2047,6 +2251,7 @@ init_registry2();
|
|
|
2047
2251
|
addTags,
|
|
2048
2252
|
buildArticleTree,
|
|
2049
2253
|
buildCollections,
|
|
2254
|
+
buildLinkGraph,
|
|
2050
2255
|
buildNavTree,
|
|
2051
2256
|
buildSearchIndex,
|
|
2052
2257
|
collectionDir,
|
|
@@ -2061,6 +2266,7 @@ init_registry2();
|
|
|
2061
2266
|
emitPluginEvent,
|
|
2062
2267
|
ensureGitRepo,
|
|
2063
2268
|
ensurePluginRoot,
|
|
2269
|
+
fileTimestamps,
|
|
2064
2270
|
findCollection,
|
|
2065
2271
|
findCollectionById,
|
|
2066
2272
|
findEnabledPlugin,
|
|
@@ -2070,6 +2276,7 @@ init_registry2();
|
|
|
2070
2276
|
gitToplevel,
|
|
2071
2277
|
initRepoWiki,
|
|
2072
2278
|
installPlugin,
|
|
2279
|
+
invalidateLinkGraph,
|
|
2073
2280
|
isLinkedWorktree,
|
|
2074
2281
|
listArticleTree,
|
|
2075
2282
|
listAvailablePlugins,
|
|
@@ -2078,6 +2285,7 @@ init_registry2();
|
|
|
2078
2285
|
listPlugins,
|
|
2079
2286
|
listSearchModes,
|
|
2080
2287
|
loadSearchProvider,
|
|
2288
|
+
localGraph,
|
|
2081
2289
|
nativeSearch,
|
|
2082
2290
|
parseAddTarget,
|
|
2083
2291
|
parseArticle,
|