diffwiki-core 0.5.0-rc.202607230329.fb70592 → 0.5.0

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
@@ -40,7 +40,7 @@ var init_types = __esm({
40
40
  });
41
41
 
42
42
  // src/errors.ts
43
- var DiffwikiError, CollectionExistsError, CollectionNotFoundError, NoDefaultCollectionError, ArticleNotFoundError, InvalidTargetError, PluginError, WorktreeNotAllowedError, CollectionAlreadyAddedError;
43
+ var DiffwikiError, CollectionExistsError, CollectionNotFoundError, NoDefaultCollectionError, ArticleNotFoundError, InvalidTargetError, PluginError;
44
44
  var init_errors = __esm({
45
45
  "src/errors.ts"() {
46
46
  "use strict";
@@ -77,16 +77,6 @@ var init_errors = __esm({
77
77
  };
78
78
  PluginError = class extends DiffwikiError {
79
79
  };
80
- WorktreeNotAllowedError = class extends DiffwikiError {
81
- constructor(root) {
82
- super(`refusing to add a linked worktree \u2014 add the source repository instead (${root})`);
83
- }
84
- };
85
- CollectionAlreadyAddedError = class extends DiffwikiError {
86
- constructor(name) {
87
- super(`this repository is already added as collection "${name}"`);
88
- }
89
- };
90
80
  }
91
81
  });
92
82
 
@@ -514,9 +504,6 @@ async function listCollections() {
514
504
  async function findCollection(name) {
515
505
  return (await readRegistry()).collections.find((c) => c.name === name);
516
506
  }
517
- async function findCollectionById(id) {
518
- return (await readRegistry()).collections.find((c) => c.id === id);
519
- }
520
507
  async function createCollection(name) {
521
508
  if (await findCollection(name)) throw new CollectionExistsError(name);
522
509
  const dir = collectionDir(name);
@@ -556,7 +543,6 @@ var init_collections = __esm({
556
543
  var index_exports = {};
557
544
  __export(index_exports, {
558
545
  ArticleNotFoundError: () => ArticleNotFoundError,
559
- CollectionAlreadyAddedError: () => CollectionAlreadyAddedError,
560
546
  CollectionExistsError: () => CollectionExistsError,
561
547
  CollectionNotFoundError: () => CollectionNotFoundError,
562
548
  DiffwikiError: () => DiffwikiError,
@@ -565,35 +551,24 @@ __export(index_exports, {
565
551
  NoDefaultCollectionError: () => NoDefaultCollectionError,
566
552
  PluginError: () => PluginError,
567
553
  REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
568
- WorktreeNotAllowedError: () => WorktreeNotAllowedError,
569
- addExternalCollection: () => addExternalCollection,
570
554
  addTags: () => addTags,
571
555
  buildArticleTree: () => buildArticleTree,
572
- buildCollections: () => buildCollections,
573
- buildNavTree: () => buildNavTree,
574
- buildSearchIndex: () => buildSearchIndex,
575
556
  collectionDir: () => collectionDir,
576
- collectionGitStatus: () => collectionGitStatus,
577
557
  collectionsDir: () => collectionsDir,
578
558
  configPath: () => configPath,
579
559
  createArticle: () => createArticle,
580
560
  createCollection: () => createCollection,
581
- currentBranch: () => currentBranch,
582
561
  deregisterPlugin: () => deregisterPlugin,
583
562
  doctor: () => doctor,
584
563
  emitPluginEvent: () => emitPluginEvent,
585
- ensureGitRepo: () => ensureGitRepo,
586
564
  ensurePluginRoot: () => ensurePluginRoot,
587
565
  findCollection: () => findCollection,
588
- findCollectionById: () => findCollectionById,
589
566
  findEnabledPlugin: () => findEnabledPlugin,
590
567
  findPluginRecord: () => findPluginRecord,
591
568
  getConfigValue: () => getConfigValue,
592
569
  getLogger: () => getLogger,
593
- gitToplevel: () => gitToplevel,
594
570
  initRepoWiki: () => initRepoWiki,
595
571
  installPlugin: () => installPlugin,
596
- isLinkedWorktree: () => isLinkedWorktree,
597
572
  listArticleTree: () => listArticleTree,
598
573
  listAvailablePlugins: () => listAvailablePlugins,
599
574
  listCollections: () => listCollections,
@@ -608,7 +583,6 @@ __export(index_exports, {
608
583
  pluginsDir: () => pluginsDir,
609
584
  pluginsRegistryPath: () => pluginsRegistryPath,
610
585
  query: () => query,
611
- rankDocs: () => rankDocs,
612
586
  readArticle: () => readArticle,
613
587
  readConfig: () => readConfig,
614
588
  readPluginRegistry: () => readPluginRegistry,
@@ -622,24 +596,17 @@ __export(index_exports, {
622
596
  removePlugin: () => removePlugin,
623
597
  removeTags: () => removeTags,
624
598
  renderArticle: () => renderArticle,
625
- repoId: () => repoId,
626
- repoRemoteName: () => repoRemoteName,
627
- resolveCwdCollection: () => resolveCwdCollection,
628
599
  resolveDefaultSearchMode: () => resolveDefaultSearchMode,
629
600
  resolveHome: () => resolveHome,
630
601
  resolveLogLevel: () => resolveLogLevel,
631
- resolvePage: () => resolvePage,
632
602
  search: () => search,
633
603
  serializeArticle: () => serializeArticle,
634
604
  setConfigValue: () => setConfigValue,
635
605
  setTags: () => setTags,
636
606
  slugify: () => slugify,
637
- toTreeNodes: () => toTreeNodes,
638
- tokenize: () => tokenize,
639
607
  unregisterCollection: () => unregisterCollection,
640
608
  updateArticleBody: () => updateArticleBody,
641
609
  upsertPlugin: () => upsertPlugin,
642
- upstreamStatus: () => upstreamStatus,
643
610
  writeConfig: () => writeConfig,
644
611
  writePluginRegistry: () => writePluginRegistry,
645
612
  writeRegistry: () => writeRegistry
@@ -881,89 +848,29 @@ async function removeArticle(target) {
881
848
 
882
849
  // src/repo.ts
883
850
  var import_promises6 = __toESM(require("fs/promises"), 1);
884
- var import_node_path5 = __toESM(require("path"), 1);
885
- var import_node_child_process3 = require("child_process");
886
- var import_node_util2 = require("util");
887
- var import_yaml2 = require("yaml");
888
- init_collections();
889
- init_registry();
890
-
891
- // src/git.ts
892
851
  var import_node_path4 = __toESM(require("path"), 1);
893
852
  var import_node_child_process2 = require("child_process");
894
853
  var import_node_util = require("util");
854
+ var import_yaml2 = require("yaml");
855
+ init_collections();
856
+ init_registry();
895
857
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
896
- async function git(cwd, args) {
897
- try {
898
- const { stdout } = await execFileAsync("git", args, { cwd });
899
- return stdout.trim();
900
- } catch {
901
- return void 0;
902
- }
903
- }
904
- async function gitToplevel(cwd) {
905
- return git(cwd, ["rev-parse", "--show-toplevel"]);
906
- }
907
- async function isLinkedWorktree(cwd) {
908
- const gitDir = await git(cwd, ["rev-parse", "--absolute-git-dir"]);
909
- const commonDir = await git(cwd, ["rev-parse", "--git-common-dir"]);
910
- if (!gitDir || !commonDir) return false;
911
- const commonAbs = import_node_path4.default.isAbsolute(commonDir) ? commonDir : import_node_path4.default.resolve(cwd, commonDir);
912
- return import_node_path4.default.resolve(gitDir) !== import_node_path4.default.resolve(commonAbs);
913
- }
914
- async function repoRemoteName(cwd) {
915
- const url = await git(cwd, ["remote", "get-url", "origin"]);
916
- if (!url) return void 0;
917
- const base = url.replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
918
- const name = base.replace(/\.git$/, "");
919
- return name.length > 0 ? name : void 0;
920
- }
921
- async function repoId(cwd) {
922
- const roots = await git(cwd, ["rev-list", "--max-parents=0", "HEAD"]);
923
- if (roots) return `commit:${roots.split("\n")[0]}`;
924
- const url = await git(cwd, ["remote", "get-url", "origin"]);
925
- if (url) return `remote:${url.replace(/\.git$/, "").replace(/\/+$/, "")}`;
926
- return void 0;
927
- }
928
- async function currentBranch(cwd) {
929
- const name = await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
930
- return name && name !== "HEAD" ? name : void 0;
931
- }
932
- async function upstreamStatus(cwd) {
933
- const branch = await currentBranch(cwd);
934
- const counts = await git(cwd, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"]);
935
- if (!counts) return { branch, ahead: 0, behind: 0, hasUpstream: false };
936
- const [behind, ahead] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
937
- return { branch, ahead, behind, hasUpstream: true };
938
- }
939
- async function ensureGitRepo(cwd) {
940
- const top = await gitToplevel(cwd);
941
- if (top) return top;
942
- await execFileAsync("git", ["init"], { cwd });
943
- const after = await gitToplevel(cwd);
944
- if (!after) throw new Error("git init did not produce a repository");
945
- return after;
946
- }
947
-
948
- // src/repo.ts
949
- init_errors();
950
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
951
858
  var REPO_CONFIG_FILE = "diffwiki.yaml";
952
859
  async function detectRepoName(cwd) {
953
860
  try {
954
- const { stdout } = await execFileAsync2("git", ["rev-parse", "--show-toplevel"], { cwd });
955
- return import_node_path5.default.basename(stdout.trim());
861
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
862
+ return import_node_path4.default.basename(stdout.trim());
956
863
  } catch {
957
- return import_node_path5.default.basename(import_node_path5.default.resolve(cwd));
864
+ return import_node_path4.default.basename(import_node_path4.default.resolve(cwd));
958
865
  }
959
866
  }
960
867
  async function readRepoConfig(cwd) {
961
868
  try {
962
- const raw = await import_promises6.default.readFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), "utf8");
869
+ const raw = await import_promises6.default.readFile(import_node_path4.default.join(cwd, REPO_CONFIG_FILE), "utf8");
963
870
  const parsed = (0, import_yaml2.parse)(raw) ?? {};
964
871
  if (!parsed.path) return void 0;
965
872
  return {
966
- collection: parsed.collection ?? import_node_path5.default.basename(import_node_path5.default.resolve(cwd)),
873
+ collection: parsed.collection ?? import_node_path4.default.basename(import_node_path4.default.resolve(cwd)),
967
874
  path: parsed.path
968
875
  };
969
876
  } catch (err) {
@@ -972,9 +879,9 @@ async function readRepoConfig(cwd) {
972
879
  }
973
880
  }
974
881
  async function initRepoWiki(opts) {
975
- const cwd = import_node_path5.default.resolve(opts.cwd);
882
+ const cwd = import_node_path4.default.resolve(opts.cwd);
976
883
  const wikiPath = opts.wikiPath ?? "wiki";
977
- const absWiki = import_node_path5.default.join(cwd, wikiPath);
884
+ const absWiki = import_node_path4.default.join(cwd, wikiPath);
978
885
  let name = opts.collection ?? await detectRepoName(cwd);
979
886
  const existing = await findCollection(name);
980
887
  if (existing && existing.path !== absWiki) {
@@ -991,7 +898,7 @@ async function initRepoWiki(opts) {
991
898
  }
992
899
  await import_promises6.default.mkdir(absWiki, { recursive: true });
993
900
  const config = { collection: name, path: wikiPath };
994
- await import_promises6.default.writeFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
901
+ await import_promises6.default.writeFile(import_node_path4.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
995
902
  const entry = {
996
903
  name,
997
904
  type: "repo",
@@ -1001,497 +908,152 @@ async function initRepoWiki(opts) {
1001
908
  await registerCollection(entry);
1002
909
  return entry;
1003
910
  }
1004
- async function addExternalCollection(opts) {
1005
- const cwd = import_node_path5.default.resolve(opts.cwd);
1006
- const root = await ensureGitRepo(cwd);
1007
- if (await isLinkedWorktree(cwd)) throw new WorktreeNotAllowedError(root);
1008
- const id = await repoId(root) ?? `path:${root}`;
1009
- const already = await findCollectionById(id);
1010
- if (already) throw new CollectionAlreadyAddedError(already.name);
1011
- const docsRel = opts.docsDir ?? "docs";
1012
- const contentDir = import_node_path5.default.join(root, docsRel);
1013
- let name = await repoRemoteName(root) ?? import_node_path5.default.basename(root);
1014
- if (await findCollection(name)) {
1015
- let n = 2;
1016
- while (await findCollection(`${name}-${n}`)) n += 1;
1017
- name = `${name}-${n}`;
1018
- }
1019
- const entry = {
1020
- name,
1021
- type: "external",
1022
- path: contentDir,
1023
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1024
- id,
1025
- repoRoot: root,
1026
- docsDir: docsRel
1027
- };
1028
- await registerCollection(entry);
1029
- return entry;
1030
- }
1031
- async function resolveCwdCollection(cwd) {
1032
- const abs = import_node_path5.default.resolve(cwd);
1033
- const root = await gitToplevel(abs);
1034
- if (root) {
1035
- const id = await repoId(root) ?? `path:${root}`;
1036
- const byId = await findCollectionById(id);
1037
- if (byId) return byId;
1038
- }
1039
- const within = (base) => abs === base || abs.startsWith(base + import_node_path5.default.sep);
1040
- const all = await listCollections();
1041
- return all.find((c) => within(c.path) || c.repoRoot !== void 0 && within(c.repoRoot));
1042
- }
1043
- async function collectionGitStatus(entry) {
1044
- if (entry.type !== "external" || !entry.repoRoot) return void 0;
1045
- const top = await gitToplevel(entry.repoRoot);
1046
- if (!top) return void 0;
1047
- const s = await upstreamStatus(entry.repoRoot);
1048
- return { branch: s.branch, ahead: s.ahead, behind: s.behind, hasUpstream: s.hasUpstream };
1049
- }
1050
-
1051
- // src/site.ts
1052
- init_collections();
1053
911
 
1054
- // src/browse.ts
912
+ // src/query.ts
1055
913
  var import_promises7 = __toESM(require("fs/promises"), 1);
1056
- var import_node_path6 = __toESM(require("path"), 1);
914
+ var import_node_path5 = __toESM(require("path"), 1);
1057
915
  init_collections();
1058
916
  init_errors();
1059
- var INDEX_RE = /^(index|readme)\.mdx?$/i;
1060
- function buildArticleTree(relPaths) {
1061
- const root = {};
1062
- for (const raw of relPaths) {
1063
- const rel = raw.replace(/\\/g, "/").replace(/^\/+/, "");
1064
- if (!rel) continue;
1065
- const segs = rel.split("/");
1066
- let cur = root;
1067
- segs.forEach((seg, i) => {
1068
- if (i === segs.length - 1) {
1069
- if (!(seg in cur)) cur[seg] = null;
1070
- } else {
1071
- if (cur[seg] === null || !(seg in cur)) cur[seg] = {};
1072
- cur = cur[seg];
1073
- }
1074
- });
1075
- }
1076
- const stripExt = (f) => f.replace(/\.(mdx?)$/, "");
1077
- const walk = (trie, parents) => {
1078
- const entries = Object.entries(trie);
1079
- const branches = entries.filter(([, v]) => v !== null);
1080
- const leaves = entries.filter(([, v]) => v === null).map(([k]) => k).filter((f) => !INDEX_RE.test(f));
1081
- branches.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
1082
- leaves.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1083
- const branchNodes = branches.map(([seg, sub]) => ({
1084
- name: seg,
1085
- title: seg,
1086
- // Folders are navigable to their index page (an authored index.md/README,
1087
- // else a generated listing).
1088
- slug: [...parents, seg].join("/"),
1089
- children: walk(sub, [...parents, seg])
1090
- }));
1091
- const leafNodes = leaves.map((file) => {
1092
- const nameNoExt = stripExt(file);
1093
- return { name: nameNoExt, title: nameNoExt, slug: [...parents, nameNoExt].join("/") };
1094
- });
1095
- return [...branchNodes, ...leafNodes];
1096
- };
1097
- return walk(root, []);
1098
- }
1099
- async function collectRelPaths(dir, rel = "") {
1100
- let dirents;
917
+ init_logger();
918
+ init_host();
919
+ init_registry2();
920
+ init_types();
921
+ var log = getLogger("query");
922
+ async function collectMarkdownFiles(dir) {
923
+ let entries;
1101
924
  try {
1102
- dirents = await import_promises7.default.readdir(dir, { withFileTypes: true });
925
+ entries = await import_promises7.default.readdir(dir, { withFileTypes: true });
1103
926
  } catch {
1104
927
  return [];
1105
928
  }
1106
- const out = [];
1107
- for (const d of dirents) {
1108
- if (d.name.startsWith(".")) continue;
1109
- const childRel = rel ? `${rel}/${d.name}` : d.name;
1110
- if (d.isDirectory()) {
1111
- out.push(...await collectRelPaths(import_node_path6.default.join(dir, d.name), childRel));
1112
- } else if (/\.(mdx?)$/.test(d.name)) {
1113
- out.push(childRel);
929
+ const files = [];
930
+ for (const entry of entries) {
931
+ const full = import_node_path5.default.join(dir, entry.name);
932
+ if (entry.isDirectory()) {
933
+ files.push(...await collectMarkdownFiles(full));
934
+ } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
935
+ files.push(full);
1114
936
  }
1115
937
  }
1116
- return out;
938
+ return files;
1117
939
  }
1118
- async function readdirSafe(dir) {
1119
- try {
1120
- return await import_promises7.default.readdir(dir);
1121
- } catch {
1122
- return [];
1123
- }
940
+ var K1 = 1.5;
941
+ var B = 0.75;
942
+ var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
943
+ function tokenize(text) {
944
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
1124
945
  }
1125
- function pickIndexFile(names) {
1126
- const rank = (n) => {
1127
- const lower = n.toLowerCase();
1128
- const base = lower.startsWith("index") ? 0 : 1;
1129
- const ext = lower.endsWith(".mdx") ? 0 : 1;
1130
- return base * 2 + ext;
1131
- };
1132
- return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
946
+ function termFreqs(tokens) {
947
+ const freqs = /* @__PURE__ */ new Map();
948
+ for (const t of tokens) {
949
+ freqs.set(t, (freqs.get(t) ?? 0) + 1);
950
+ }
951
+ return freqs;
1133
952
  }
1134
- async function cheapTitle(absPath) {
1135
- let handle;
1136
- try {
1137
- handle = await import_promises7.default.open(absPath, "r");
1138
- const buf = Buffer.alloc(512);
1139
- const { bytesRead } = await handle.read(buf, 0, 512, 0);
1140
- const snippet = buf.subarray(0, bytesRead).toString("utf8");
1141
- const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
1142
- if (m) return m[1].trim().replace(/^["']|["']$/g, "");
1143
- } catch {
1144
- } finally {
1145
- await handle?.close();
953
+ function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
954
+ const tf = termFreqs(fieldTokens);
955
+ const dl = fieldTokens.length;
956
+ const avgDl = avgFieldLen || 1;
957
+ let score = 0;
958
+ for (const term of queryTerms) {
959
+ const freq = tf.get(term) ?? 0;
960
+ if (freq === 0) continue;
961
+ const termIdf = idf.get(term) ?? 0;
962
+ const numerator = freq * (K1 + 1);
963
+ const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
964
+ score += termIdf * (numerator / denominator);
1146
965
  }
1147
- return void 0;
966
+ return score;
1148
967
  }
1149
- async function enrichTitles(nodes, collDir) {
1150
- return Promise.all(
1151
- nodes.map(async (node) => {
1152
- if (node.children) {
1153
- const children = await enrichTitles(node.children, collDir);
1154
- let title = node.title;
1155
- if (node.slug) {
1156
- const dir = import_node_path6.default.join(collDir, node.slug);
1157
- const idx = pickIndexFile(await readdirSafe(dir));
1158
- if (idx) title = await cheapTitle(import_node_path6.default.join(dir, idx)) ?? title;
1159
- }
1160
- return { ...node, title, children };
1161
- }
1162
- for (const ext of [".mdx", ".md"]) {
1163
- const title = await cheapTitle(import_node_path6.default.join(collDir, `${node.slug}${ext}`));
1164
- if (title) return { ...node, title };
968
+ async function nativeSearch(term, collections, collection) {
969
+ let targets = collections;
970
+ if (collection) {
971
+ const one = collections.find((c) => c.name === collection);
972
+ if (!one) throw new CollectionNotFoundError(collection);
973
+ targets = [one];
974
+ }
975
+ const docs = [];
976
+ for (const entry of targets) {
977
+ for (const full of await collectMarkdownFiles(entry.path)) {
978
+ const fallbackTitle = import_node_path5.default.basename(full).replace(/\.mdx?$/, "");
979
+ let title = fallbackTitle;
980
+ let tags = [];
981
+ let body = "";
982
+ try {
983
+ const parsed = parseArticle(await import_promises7.default.readFile(full, "utf8"));
984
+ if (parsed.title) title = parsed.title;
985
+ tags = parsed.tags;
986
+ body = parsed.body;
987
+ } catch {
1165
988
  }
1166
- return node;
1167
- })
1168
- );
1169
- }
1170
- async function listArticleTree(coll) {
1171
- const entry = await findCollection(coll);
1172
- if (!entry) throw new CollectionNotFoundError(coll);
1173
- const rels = await collectRelPaths(entry.path);
1174
- return enrichTitles(buildArticleTree(rels), import_node_path6.default.resolve(entry.path));
1175
- }
1176
- async function readArticle(coll, slug) {
1177
- const entry = await findCollection(coll);
1178
- if (!entry) throw new CollectionNotFoundError(coll);
1179
- const collDir = import_node_path6.default.resolve(entry.path);
1180
- const normalized = slug.replace(/\\/g, "/");
1181
- if (normalized.split("/").some((s) => s === "..")) {
1182
- throw new InvalidTargetError(slug, "path traversal detected");
989
+ docs.push({
990
+ collectionName: entry.name,
991
+ filePath: full,
992
+ title,
993
+ tags,
994
+ fields: {
995
+ title: tokenize(title),
996
+ tags: tokenize(tags.join(" ")),
997
+ collection: tokenize(entry.name),
998
+ body: tokenize(body)
999
+ }
1000
+ });
1001
+ }
1183
1002
  }
1184
- const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
1185
- const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path6.default.sep);
1186
- let abs;
1187
- for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
1188
- const candidate = import_node_path6.default.resolve(collDir, rel);
1189
- if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1190
- try {
1191
- await import_promises7.default.access(candidate);
1192
- abs = candidate;
1193
- break;
1194
- } catch {
1003
+ if (!term.trim()) {
1004
+ return docs.map((d) => ({
1005
+ collection: d.collectionName,
1006
+ path: d.filePath,
1007
+ title: d.title,
1008
+ tags: d.tags
1009
+ }));
1010
+ }
1011
+ const queryTerms = tokenize(term);
1012
+ if (queryTerms.length === 0) {
1013
+ return docs.map((d) => ({
1014
+ collection: d.collectionName,
1015
+ path: d.filePath,
1016
+ title: d.title,
1017
+ tags: d.tags
1018
+ }));
1019
+ }
1020
+ const docTokenSets = docs.map(
1021
+ (doc) => /* @__PURE__ */ new Set([...doc.fields.title, ...doc.fields.tags, ...doc.fields.collection, ...doc.fields.body])
1022
+ );
1023
+ const N = docs.length;
1024
+ const idf = /* @__PURE__ */ new Map();
1025
+ for (const qt of queryTerms) {
1026
+ let docFreq = 0;
1027
+ for (const tokenSet of docTokenSets) {
1028
+ if (tokenSet.has(qt)) docFreq++;
1195
1029
  }
1030
+ idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
1196
1031
  }
1197
- if (!abs) {
1198
- const dir = import_node_path6.default.resolve(collDir, safeSlug);
1199
- if (inJail(dir)) {
1200
- const idx = pickIndexFile(await readdirSafe(dir));
1201
- if (idx) abs = import_node_path6.default.join(dir, idx);
1032
+ const avgLens = {
1033
+ title: docs.reduce((sum, d) => sum + d.fields.title.length, 0) / (N || 1),
1034
+ tags: docs.reduce((sum, d) => sum + d.fields.tags.length, 0) / (N || 1),
1035
+ collection: docs.reduce((sum, d) => sum + d.fields.collection.length, 0) / (N || 1),
1036
+ body: docs.reduce((sum, d) => sum + d.fields.body.length, 0) / (N || 1)
1037
+ };
1038
+ const scored = [];
1039
+ for (const doc of docs) {
1040
+ const titleScore = bm25FieldScore(queryTerms, doc.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title;
1041
+ const tagsScore = bm25FieldScore(queryTerms, doc.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags;
1042
+ const collScore = bm25FieldScore(queryTerms, doc.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection;
1043
+ const bodyScore = bm25FieldScore(queryTerms, doc.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
1044
+ const total = titleScore + tagsScore + collScore + bodyScore;
1045
+ if (total > 0) {
1046
+ scored.push({ doc, score: total });
1202
1047
  }
1203
1048
  }
1204
- if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1205
- const parsed = parseArticle(await import_promises7.default.readFile(abs, "utf8"));
1206
- const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1207
- return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
1208
- }
1209
-
1210
- // src/render.ts
1211
- var import_unified = require("unified");
1212
- var import_remark_parse = __toESM(require("remark-parse"), 1);
1213
- var import_remark_gfm = __toESM(require("remark-gfm"), 1);
1214
- var import_remark_rehype = __toESM(require("remark-rehype"), 1);
1215
- var import_rehype_slug = __toESM(require("rehype-slug"), 1);
1216
- var import_rehype = __toESM(require("@shikijs/rehype"), 1);
1217
- var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
1218
- var import_unist_util_visit = require("unist-util-visit");
1219
- var import_hast_util_to_string = require("hast-util-to-string");
1220
- var LANG_ALIASES = {
1221
- "c#": "csharp",
1222
- cs: "csharp",
1223
- "c++": "cpp",
1224
- cxx: "cpp",
1225
- hs: "haskell",
1226
- ex: "elixir"
1227
- };
1228
- function rehypeCollectToc(toc) {
1229
- return (tree) => {
1230
- (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1231
- if (node.tagName !== "h2" && node.tagName !== "h3") return;
1232
- const id = node.properties?.id;
1233
- if (!id) return;
1234
- toc.push({ slug: String(id), text: (0, import_hast_util_to_string.toString)(node), depth: node.tagName === "h2" ? 2 : 3 });
1235
- });
1236
- };
1237
- }
1238
- function rehypeStripLeadingH1() {
1239
- return (tree) => {
1240
- let removed = false;
1241
- (0, import_unist_util_visit.visit)(tree, "element", (node, index, parent) => {
1242
- if (!removed && node.tagName === "h1" && parent && index != null) {
1243
- parent.children.splice(index, 1);
1244
- removed = true;
1245
- return [import_unist_util_visit.SKIP, index];
1246
- }
1247
- });
1248
- };
1249
- }
1250
- function rehypeMarkMermaid() {
1251
- return (tree) => {
1252
- (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1253
- if (node.tagName !== "pre") return;
1254
- const code = node.children.find((c) => c.type === "element" && c.tagName === "code");
1255
- const cls = code?.properties?.className ?? [];
1256
- if (!code || !cls.includes("language-mermaid")) return;
1257
- const source = (0, import_hast_util_to_string.toString)(code);
1258
- node.properties = { className: ["mermaid"] };
1259
- node.children = [{ type: "text", value: source }];
1260
- });
1261
- };
1262
- }
1263
- async function renderArticle(body) {
1264
- const toc = [];
1265
- 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(import_rehype.default, {
1266
- themes: { light: "github-light", dark: "github-dark" },
1267
- defaultColor: false,
1268
- langAlias: LANG_ALIASES,
1269
- fallbackLanguage: "plaintext"
1270
- }).use(import_rehype_stringify.default).process(body);
1271
- return { html: String(file), toc };
1272
- }
1273
-
1274
- // src/site.ts
1275
- init_errors();
1276
- var isStatic = () => process.env.DIFFWIKI_STATIC === "1";
1277
- function toTreeNodes(coll, nodes) {
1278
- return nodes.map(
1279
- (n) => n.children ? {
1280
- name: n.name,
1281
- title: n.title ?? n.name,
1282
- path: n.slug ? `/${coll}/${n.slug}` : void 0,
1283
- children: toTreeNodes(coll, n.children)
1284
- } : { name: n.name, title: n.title ?? n.name, path: `/${coll}/${n.slug}` }
1285
- );
1286
- }
1287
- async function gitInfo(e) {
1288
- if (isStatic()) {
1289
- if (e.type === "external") console.warn(`[diffwiki] static build: omitting branch info for "${e.name}"`);
1290
- return {};
1291
- }
1292
- const s = await collectionGitStatus(e);
1293
- return s ? { branch: s.branch, behind: s.hasUpstream ? s.behind : void 0 } : {};
1294
- }
1295
- async function buildCollections() {
1296
- const entries = await listCollections();
1297
- return Promise.all(
1298
- entries.map(async (e) => ({
1299
- name: e.name,
1300
- type: e.type,
1301
- path: e.path,
1302
- ...await gitInfo(e)
1303
- }))
1304
- );
1305
- }
1306
- async function buildNavTree() {
1307
- const entries = await listCollections();
1308
- return Promise.all(
1309
- entries.map(async (e) => ({
1310
- collection: { name: e.name, type: e.type, path: e.path, ...await gitInfo(e) },
1311
- nodes: toTreeNodes(e.name, await listArticleTree(e.name))
1312
- }))
1313
- );
1314
- }
1315
- function findNode(nodes, slug) {
1316
- for (const n of nodes) {
1317
- if (n.slug === slug) return n;
1318
- if (n.children) {
1319
- const found = findNode(n.children, slug);
1320
- if (found) return found;
1321
- }
1322
- }
1323
- return void 0;
1324
- }
1325
- async function resolvePage(coll, slug) {
1326
- try {
1327
- const art = await readArticle(coll, slug);
1328
- const { html, toc } = await renderArticle(art.body);
1329
- return {
1330
- kind: "doc",
1331
- coll: art.coll,
1332
- slug: art.slug,
1333
- title: art.title,
1334
- tags: art.tags,
1335
- html,
1336
- toc,
1337
- path: `/${art.coll}/${art.slug}`
1338
- };
1339
- } catch (err) {
1340
- if (err instanceof CollectionNotFoundError) return null;
1341
- if (!(err instanceof ArticleNotFoundError)) throw err;
1342
- let nodes;
1343
- try {
1344
- nodes = await listArticleTree(coll);
1345
- } catch (e) {
1346
- if (e instanceof CollectionNotFoundError) return null;
1347
- throw e;
1348
- }
1349
- const folder = slug ? findNode(nodes, slug) : { title: void 0, children: nodes };
1350
- if (!folder?.children) return null;
1351
- const entries = folder.children.map((n) => ({
1352
- name: n.name,
1353
- title: n.title ?? n.name,
1354
- path: `/${coll}/${n.slug ?? n.name}`,
1355
- isDir: Array.isArray(n.children)
1356
- }));
1357
- return {
1358
- kind: "listing",
1359
- coll,
1360
- slug,
1361
- title: folder.title ?? (slug ? slug.split("/").pop() ?? slug : coll),
1362
- entries
1363
- };
1364
- }
1365
- }
1366
-
1367
- // src/query.ts
1368
- var import_promises8 = __toESM(require("fs/promises"), 1);
1369
- var import_node_path7 = __toESM(require("path"), 1);
1370
- init_collections();
1371
- init_errors();
1372
- init_logger();
1373
- init_host();
1374
- init_registry2();
1375
- init_types();
1376
-
1377
- // src/bm25.ts
1378
- var K1 = 1.5;
1379
- var B = 0.75;
1380
- var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
1381
- function tokenize(text) {
1382
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
1383
- }
1384
- function termFreqs(tokens) {
1385
- const freqs = /* @__PURE__ */ new Map();
1386
- for (const t of tokens) freqs.set(t, (freqs.get(t) ?? 0) + 1);
1387
- return freqs;
1388
- }
1389
- function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
1390
- const tf = termFreqs(fieldTokens);
1391
- const dl = fieldTokens.length;
1392
- const avgDl = avgFieldLen || 1;
1393
- let score = 0;
1394
- for (const term of queryTerms) {
1395
- const freq = tf.get(term) ?? 0;
1396
- if (freq === 0) continue;
1397
- const termIdf = idf.get(term) ?? 0;
1398
- const numerator = freq * (K1 + 1);
1399
- const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
1400
- score += termIdf * (numerator / denominator);
1401
- }
1402
- return score;
1403
- }
1404
- function rankDocs(term, docs) {
1405
- const queryTerms = tokenize(term);
1406
- if (queryTerms.length === 0) return docs.map((d) => ({ ...d }));
1407
- const N = docs.length;
1408
- const docTokenSets = docs.map(
1409
- (d) => /* @__PURE__ */ new Set([...d.fields.title, ...d.fields.tags, ...d.fields.collection, ...d.fields.body])
1410
- );
1411
- const idf = /* @__PURE__ */ new Map();
1412
- for (const qt of queryTerms) {
1413
- let docFreq = 0;
1414
- for (const set of docTokenSets) if (set.has(qt)) docFreq++;
1415
- idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
1416
- }
1417
- const avg = (sel) => docs.reduce((s, d) => s + sel(d), 0) / (N || 1);
1418
- const avgLens = {
1419
- title: avg((d) => d.fields.title.length),
1420
- tags: avg((d) => d.fields.tags.length),
1421
- collection: avg((d) => d.fields.collection.length),
1422
- body: avg((d) => d.fields.body.length)
1423
- };
1424
- const scored = [];
1425
- for (const d of docs) {
1426
- const total = bm25FieldScore(queryTerms, d.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title + bm25FieldScore(queryTerms, d.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags + bm25FieldScore(queryTerms, d.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection + bm25FieldScore(queryTerms, d.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
1427
- if (total > 0) scored.push({ ...d, score: total });
1428
- }
1429
- scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
1430
- return scored;
1431
- }
1432
-
1433
- // src/query.ts
1434
- var log = getLogger("query");
1435
- async function collectMarkdownFiles(dir) {
1436
- let entries;
1437
- try {
1438
- entries = await import_promises8.default.readdir(dir, { withFileTypes: true });
1439
- } catch {
1440
- return [];
1441
- }
1442
- const files = [];
1443
- for (const entry of entries) {
1444
- const full = import_node_path7.default.join(dir, entry.name);
1445
- if (entry.isDirectory()) {
1446
- files.push(...await collectMarkdownFiles(full));
1447
- } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
1448
- files.push(full);
1449
- }
1450
- }
1451
- return files;
1452
- }
1453
- async function nativeSearch(term, collections, collection) {
1454
- let targets = collections;
1455
- if (collection) {
1456
- const one = collections.find((c) => c.name === collection);
1457
- if (!one) throw new CollectionNotFoundError(collection);
1458
- targets = [one];
1459
- }
1460
- const docs = [];
1461
- for (const entry of targets) {
1462
- for (const full of await collectMarkdownFiles(entry.path)) {
1463
- const fallbackTitle = import_node_path7.default.basename(full).replace(/\.mdx?$/, "");
1464
- let title = fallbackTitle;
1465
- let tags = [];
1466
- let body = "";
1467
- try {
1468
- const parsed = parseArticle(await import_promises8.default.readFile(full, "utf8"));
1469
- if (parsed.title) title = parsed.title;
1470
- tags = parsed.tags;
1471
- body = parsed.body;
1472
- } catch {
1473
- }
1474
- docs.push({
1475
- collectionName: entry.name,
1476
- filePath: full,
1477
- title,
1478
- tags,
1479
- fields: {
1480
- title: tokenize(title),
1481
- tags: tokenize(tags.join(" ")),
1482
- collection: tokenize(entry.name),
1483
- body: tokenize(body)
1484
- }
1485
- });
1486
- }
1487
- }
1488
- return rankDocs(term, docs).map((d) => ({
1489
- collection: d.collectionName,
1490
- path: d.filePath,
1491
- title: d.title,
1492
- tags: d.tags,
1493
- ...d.score === void 0 ? {} : { score: d.score }
1494
- }));
1049
+ scored.sort((a, b) => b.score - a.score);
1050
+ return scored.map((s) => ({
1051
+ collection: s.doc.collectionName,
1052
+ path: s.doc.filePath,
1053
+ title: s.doc.title,
1054
+ tags: s.doc.tags,
1055
+ score: s.score
1056
+ }));
1495
1057
  }
1496
1058
  async function resolveDefaultEngine() {
1497
1059
  const config = await readConfig();
@@ -1547,64 +1109,15 @@ async function query(term, opts) {
1547
1109
  return (await search(term, opts)).hits;
1548
1110
  }
1549
1111
 
1550
- // src/search.ts
1551
- var import_promises9 = __toESM(require("fs/promises"), 1);
1552
- var import_node_path8 = __toESM(require("path"), 1);
1553
- init_collections();
1554
- async function readArticleParts(collDir, slug) {
1555
- for (const ext of [".mdx", ".md"]) {
1556
- try {
1557
- const parsed = parseArticle(await import_promises9.default.readFile(import_node_path8.default.join(collDir, `${slug}${ext}`), "utf8"));
1558
- return { tags: parsed.tags, body: parsed.body };
1559
- } catch {
1560
- }
1561
- }
1562
- return { tags: [], body: "" };
1563
- }
1564
- async function flatten(coll, collDir, nodes, out) {
1565
- for (const n of nodes) {
1566
- if (n.slug && !n.children) {
1567
- const title = n.title ?? n.name;
1568
- const { tags, body } = await readArticleParts(collDir, n.slug);
1569
- out.push({
1570
- collection: coll,
1571
- slug: n.slug,
1572
- title,
1573
- tags,
1574
- fields: {
1575
- title: tokenize(title),
1576
- tags: tokenize(tags.join(" ")),
1577
- collection: tokenize(coll),
1578
- body: tokenize(body)
1579
- }
1580
- });
1581
- }
1582
- if (n.children) await flatten(coll, collDir, n.children, out);
1583
- }
1584
- }
1585
- async function buildSearchIndex(collection) {
1586
- const entries = collection ? (await listCollections()).filter((c) => c.name === collection) : await listCollections();
1587
- const out = [];
1588
- for (const e of entries) {
1589
- try {
1590
- const collEntry = await findCollection(e.name);
1591
- const collDir = import_node_path8.default.resolve(collEntry?.path ?? e.path);
1592
- await flatten(e.name, collDir, await listArticleTree(e.name), out);
1593
- } catch {
1594
- }
1595
- }
1596
- return out;
1597
- }
1598
-
1599
1112
  // src/doctor.ts
1600
- var import_promises11 = __toESM(require("fs/promises"), 1);
1113
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1601
1114
  init_paths();
1602
1115
  init_registry();
1603
1116
  init_collections();
1604
1117
 
1605
1118
  // src/plugins/manager.ts
1606
- var import_promises10 = __toESM(require("fs/promises"), 1);
1607
- var import_node_path10 = __toESM(require("path"), 1);
1119
+ var import_promises8 = __toESM(require("fs/promises"), 1);
1120
+ var import_node_path7 = __toESM(require("path"), 1);
1608
1121
  init_paths();
1609
1122
  init_errors();
1610
1123
  init_collections();
@@ -1612,14 +1125,14 @@ init_registry2();
1612
1125
  init_host();
1613
1126
 
1614
1127
  // src/plugins/sdk.ts
1615
- var import_node_child_process4 = require("child_process");
1616
- var import_node_util3 = require("util");
1128
+ var import_node_child_process3 = require("child_process");
1129
+ var import_node_util2 = require("util");
1617
1130
  var import_node_fs = require("fs");
1618
- var import_node_path9 = require("path");
1131
+ var import_node_path6 = require("path");
1619
1132
  var import_node_url = require("url");
1620
- var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
1133
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
1621
1134
  async function runProcess(file, args, opts) {
1622
- const { stdout, stderr } = await execFileAsync3(file, args, {
1135
+ const { stdout, stderr } = await execFileAsync2(file, args, {
1623
1136
  env: opts?.env,
1624
1137
  timeout: opts?.timeout,
1625
1138
  // Large: qmd emits multi-MB JSON search payloads that overflow the default 1 MB.
@@ -1631,7 +1144,7 @@ async function runProcess(file, args, opts) {
1631
1144
  // src/plugins/manager.ts
1632
1145
  async function readPackageJson(file) {
1633
1146
  try {
1634
- return JSON.parse(await import_promises10.default.readFile(file, "utf8"));
1147
+ return JSON.parse(await import_promises8.default.readFile(file, "utf8"));
1635
1148
  } catch {
1636
1149
  return null;
1637
1150
  }
@@ -1647,7 +1160,7 @@ function binOf(manifest) {
1647
1160
  async function pluginManifestDirs(nodeModules) {
1648
1161
  let entries;
1649
1162
  try {
1650
- entries = await import_promises10.default.readdir(nodeModules);
1163
+ entries = await import_promises8.default.readdir(nodeModules);
1651
1164
  } catch {
1652
1165
  return [];
1653
1166
  }
@@ -1657,39 +1170,39 @@ async function pluginManifestDirs(nodeModules) {
1657
1170
  if (entry.startsWith("@")) {
1658
1171
  let scoped;
1659
1172
  try {
1660
- scoped = await import_promises10.default.readdir(import_node_path10.default.join(nodeModules, entry));
1173
+ scoped = await import_promises8.default.readdir(import_node_path7.default.join(nodeModules, entry));
1661
1174
  } catch {
1662
1175
  continue;
1663
1176
  }
1664
1177
  for (const pkg of scoped) {
1665
1178
  if (pkg.startsWith(".")) continue;
1666
- dirs.push(import_node_path10.default.join(nodeModules, entry, pkg));
1179
+ dirs.push(import_node_path7.default.join(nodeModules, entry, pkg));
1667
1180
  }
1668
1181
  } else {
1669
- dirs.push(import_node_path10.default.join(nodeModules, entry));
1182
+ dirs.push(import_node_path7.default.join(nodeModules, entry));
1670
1183
  }
1671
1184
  }
1672
1185
  return dirs;
1673
1186
  }
1674
1187
  async function ensurePluginRoot() {
1675
1188
  const dir = pluginsDir();
1676
- await import_promises10.default.mkdir(dir, { recursive: true });
1677
- const pkgPath = import_node_path10.default.join(dir, "package.json");
1189
+ await import_promises8.default.mkdir(dir, { recursive: true });
1190
+ const pkgPath = import_node_path7.default.join(dir, "package.json");
1678
1191
  if (await readPackageJson(pkgPath) === null) {
1679
1192
  const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
1680
- await import_promises10.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
1193
+ await import_promises8.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
1681
1194
  `, "utf8");
1682
1195
  }
1683
1196
  return dir;
1684
1197
  }
1685
1198
  async function discoverSearchPackage(dir) {
1686
- const nodeModules = import_node_path10.default.join(dir, "node_modules");
1199
+ const nodeModules = import_node_path7.default.join(dir, "node_modules");
1687
1200
  for (const pkgDir of await pluginManifestDirs(nodeModules)) {
1688
- const manifest = await readPackageJson(import_node_path10.default.join(pkgDir, "package.json"));
1201
+ const manifest = await readPackageJson(import_node_path7.default.join(pkgDir, "package.json"));
1689
1202
  if (!manifest?.name || manifest.diffwiki?.kind !== "search") continue;
1690
1203
  const bin = binOf(manifest);
1691
1204
  if (!bin) continue;
1692
- return { name: manifest.name, version: manifest.version, binAbs: import_node_path10.default.resolve(pkgDir, bin) };
1205
+ return { name: manifest.name, version: manifest.version, binAbs: import_node_path7.default.resolve(pkgDir, bin) };
1693
1206
  }
1694
1207
  return null;
1695
1208
  }
@@ -1812,7 +1325,7 @@ init_host();
1812
1325
  init_types();
1813
1326
  async function exists(target) {
1814
1327
  try {
1815
- await import_promises11.default.access(target);
1328
+ await import_promises9.default.access(target);
1816
1329
  return true;
1817
1330
  } catch {
1818
1331
  return false;
@@ -1920,6 +1433,226 @@ async function appendPluginDiagnostics(out, config) {
1920
1433
  // src/index.ts
1921
1434
  init_logger();
1922
1435
 
1436
+ // src/browse.ts
1437
+ var import_promises10 = __toESM(require("fs/promises"), 1);
1438
+ var import_node_path8 = __toESM(require("path"), 1);
1439
+ init_collections();
1440
+ init_errors();
1441
+ var INDEX_RE = /^(index|readme)\.mdx?$/i;
1442
+ function buildArticleTree(relPaths) {
1443
+ const root = {};
1444
+ for (const raw of relPaths) {
1445
+ const rel = raw.replace(/\\/g, "/").replace(/^\/+/, "");
1446
+ if (!rel) continue;
1447
+ const segs = rel.split("/");
1448
+ let cur = root;
1449
+ segs.forEach((seg, i) => {
1450
+ if (i === segs.length - 1) {
1451
+ if (!(seg in cur)) cur[seg] = null;
1452
+ } else {
1453
+ if (cur[seg] === null || !(seg in cur)) cur[seg] = {};
1454
+ cur = cur[seg];
1455
+ }
1456
+ });
1457
+ }
1458
+ const stripExt = (f) => f.replace(/\.(mdx?)$/, "");
1459
+ const walk = (trie, parents) => {
1460
+ const entries = Object.entries(trie);
1461
+ const branches = entries.filter(([, v]) => v !== null);
1462
+ const leaves = entries.filter(([, v]) => v === null).map(([k]) => k).filter((f) => !INDEX_RE.test(f));
1463
+ branches.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
1464
+ leaves.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1465
+ const branchNodes = branches.map(([seg, sub]) => ({
1466
+ name: seg,
1467
+ title: seg,
1468
+ // Folders are navigable to their index page (an authored index.md/README,
1469
+ // else a generated listing).
1470
+ slug: [...parents, seg].join("/"),
1471
+ children: walk(sub, [...parents, seg])
1472
+ }));
1473
+ const leafNodes = leaves.map((file) => {
1474
+ const nameNoExt = stripExt(file);
1475
+ return { name: nameNoExt, title: nameNoExt, slug: [...parents, nameNoExt].join("/") };
1476
+ });
1477
+ return [...branchNodes, ...leafNodes];
1478
+ };
1479
+ return walk(root, []);
1480
+ }
1481
+ async function collectRelPaths(dir, rel = "") {
1482
+ let dirents;
1483
+ try {
1484
+ dirents = await import_promises10.default.readdir(dir, { withFileTypes: true });
1485
+ } catch {
1486
+ return [];
1487
+ }
1488
+ const out = [];
1489
+ for (const d of dirents) {
1490
+ if (d.name.startsWith(".")) continue;
1491
+ const childRel = rel ? `${rel}/${d.name}` : d.name;
1492
+ if (d.isDirectory()) {
1493
+ out.push(...await collectRelPaths(import_node_path8.default.join(dir, d.name), childRel));
1494
+ } else if (/\.(mdx?)$/.test(d.name)) {
1495
+ out.push(childRel);
1496
+ }
1497
+ }
1498
+ return out;
1499
+ }
1500
+ async function readdirSafe(dir) {
1501
+ try {
1502
+ return await import_promises10.default.readdir(dir);
1503
+ } catch {
1504
+ return [];
1505
+ }
1506
+ }
1507
+ function pickIndexFile(names) {
1508
+ const rank = (n) => {
1509
+ const lower = n.toLowerCase();
1510
+ const base = lower.startsWith("index") ? 0 : 1;
1511
+ const ext = lower.endsWith(".mdx") ? 0 : 1;
1512
+ return base * 2 + ext;
1513
+ };
1514
+ return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
1515
+ }
1516
+ async function cheapTitle(absPath) {
1517
+ let handle;
1518
+ try {
1519
+ handle = await import_promises10.default.open(absPath, "r");
1520
+ const buf = Buffer.alloc(512);
1521
+ const { bytesRead } = await handle.read(buf, 0, 512, 0);
1522
+ const snippet = buf.subarray(0, bytesRead).toString("utf8");
1523
+ const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
1524
+ if (m) return m[1].trim().replace(/^["']|["']$/g, "");
1525
+ } catch {
1526
+ } finally {
1527
+ await handle?.close();
1528
+ }
1529
+ return void 0;
1530
+ }
1531
+ async function enrichTitles(nodes, collDir) {
1532
+ return Promise.all(
1533
+ nodes.map(async (node) => {
1534
+ if (node.children) {
1535
+ const children = await enrichTitles(node.children, collDir);
1536
+ let title = node.title;
1537
+ if (node.slug) {
1538
+ const dir = import_node_path8.default.join(collDir, node.slug);
1539
+ const idx = pickIndexFile(await readdirSafe(dir));
1540
+ if (idx) title = await cheapTitle(import_node_path8.default.join(dir, idx)) ?? title;
1541
+ }
1542
+ return { ...node, title, children };
1543
+ }
1544
+ for (const ext of [".mdx", ".md"]) {
1545
+ const title = await cheapTitle(import_node_path8.default.join(collDir, `${node.slug}${ext}`));
1546
+ if (title) return { ...node, title };
1547
+ }
1548
+ return node;
1549
+ })
1550
+ );
1551
+ }
1552
+ async function listArticleTree(coll) {
1553
+ const entry = await findCollection(coll);
1554
+ if (!entry) throw new CollectionNotFoundError(coll);
1555
+ const rels = await collectRelPaths(entry.path);
1556
+ return enrichTitles(buildArticleTree(rels), import_node_path8.default.resolve(entry.path));
1557
+ }
1558
+ async function readArticle(coll, slug) {
1559
+ const entry = await findCollection(coll);
1560
+ if (!entry) throw new CollectionNotFoundError(coll);
1561
+ const collDir = import_node_path8.default.resolve(entry.path);
1562
+ const normalized = slug.replace(/\\/g, "/");
1563
+ if (normalized.split("/").some((s) => s === "..")) {
1564
+ throw new InvalidTargetError(slug, "path traversal detected");
1565
+ }
1566
+ const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
1567
+ const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path8.default.sep);
1568
+ let abs;
1569
+ for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
1570
+ const candidate = import_node_path8.default.resolve(collDir, rel);
1571
+ if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1572
+ try {
1573
+ await import_promises10.default.access(candidate);
1574
+ abs = candidate;
1575
+ break;
1576
+ } catch {
1577
+ }
1578
+ }
1579
+ if (!abs) {
1580
+ const dir = import_node_path8.default.resolve(collDir, safeSlug);
1581
+ if (inJail(dir)) {
1582
+ const idx = pickIndexFile(await readdirSafe(dir));
1583
+ if (idx) abs = import_node_path8.default.join(dir, idx);
1584
+ }
1585
+ }
1586
+ if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1587
+ const parsed = parseArticle(await import_promises10.default.readFile(abs, "utf8"));
1588
+ const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1589
+ return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
1590
+ }
1591
+
1592
+ // src/render.ts
1593
+ var import_unified = require("unified");
1594
+ var import_remark_parse = __toESM(require("remark-parse"), 1);
1595
+ var import_remark_gfm = __toESM(require("remark-gfm"), 1);
1596
+ var import_remark_rehype = __toESM(require("remark-rehype"), 1);
1597
+ var import_rehype_slug = __toESM(require("rehype-slug"), 1);
1598
+ var import_rehype = __toESM(require("@shikijs/rehype"), 1);
1599
+ var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
1600
+ var import_unist_util_visit = require("unist-util-visit");
1601
+ var import_hast_util_to_string = require("hast-util-to-string");
1602
+ var LANG_ALIASES = {
1603
+ "c#": "csharp",
1604
+ cs: "csharp",
1605
+ "c++": "cpp",
1606
+ cxx: "cpp",
1607
+ hs: "haskell",
1608
+ ex: "elixir"
1609
+ };
1610
+ function rehypeCollectToc(toc) {
1611
+ return (tree) => {
1612
+ (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1613
+ if (node.tagName !== "h2" && node.tagName !== "h3") return;
1614
+ const id = node.properties?.id;
1615
+ if (!id) return;
1616
+ toc.push({ slug: String(id), text: (0, import_hast_util_to_string.toString)(node), depth: node.tagName === "h2" ? 2 : 3 });
1617
+ });
1618
+ };
1619
+ }
1620
+ function rehypeStripLeadingH1() {
1621
+ return (tree) => {
1622
+ let removed = false;
1623
+ (0, import_unist_util_visit.visit)(tree, "element", (node, index, parent) => {
1624
+ if (!removed && node.tagName === "h1" && parent && index != null) {
1625
+ parent.children.splice(index, 1);
1626
+ removed = true;
1627
+ return [import_unist_util_visit.SKIP, index];
1628
+ }
1629
+ });
1630
+ };
1631
+ }
1632
+ function rehypeMarkMermaid() {
1633
+ return (tree) => {
1634
+ (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1635
+ if (node.tagName !== "pre") return;
1636
+ const code = node.children.find((c) => c.type === "element" && c.tagName === "code");
1637
+ const cls = code?.properties?.className ?? [];
1638
+ if (!code || !cls.includes("language-mermaid")) return;
1639
+ const source = (0, import_hast_util_to_string.toString)(code);
1640
+ node.properties = { className: ["mermaid"] };
1641
+ node.children = [{ type: "text", value: source }];
1642
+ });
1643
+ };
1644
+ }
1645
+ async function renderArticle(body) {
1646
+ const toc = [];
1647
+ 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(import_rehype.default, {
1648
+ themes: { light: "github-light", dark: "github-dark" },
1649
+ defaultColor: false,
1650
+ langAlias: LANG_ALIASES,
1651
+ fallbackLanguage: "plaintext"
1652
+ }).use(import_rehype_stringify.default).process(body);
1653
+ return { html: String(file), toc };
1654
+ }
1655
+
1923
1656
  // src/plugins/index.ts
1924
1657
  init_host();
1925
1658
  init_events();
@@ -1950,7 +1683,6 @@ init_registry2();
1950
1683
  // Annotate the CommonJS export names for ESM import in node:
1951
1684
  0 && (module.exports = {
1952
1685
  ArticleNotFoundError,
1953
- CollectionAlreadyAddedError,
1954
1686
  CollectionExistsError,
1955
1687
  CollectionNotFoundError,
1956
1688
  DiffwikiError,
@@ -1959,35 +1691,24 @@ init_registry2();
1959
1691
  NoDefaultCollectionError,
1960
1692
  PluginError,
1961
1693
  REPO_CONFIG_FILE,
1962
- WorktreeNotAllowedError,
1963
- addExternalCollection,
1964
1694
  addTags,
1965
1695
  buildArticleTree,
1966
- buildCollections,
1967
- buildNavTree,
1968
- buildSearchIndex,
1969
1696
  collectionDir,
1970
- collectionGitStatus,
1971
1697
  collectionsDir,
1972
1698
  configPath,
1973
1699
  createArticle,
1974
1700
  createCollection,
1975
- currentBranch,
1976
1701
  deregisterPlugin,
1977
1702
  doctor,
1978
1703
  emitPluginEvent,
1979
- ensureGitRepo,
1980
1704
  ensurePluginRoot,
1981
1705
  findCollection,
1982
- findCollectionById,
1983
1706
  findEnabledPlugin,
1984
1707
  findPluginRecord,
1985
1708
  getConfigValue,
1986
1709
  getLogger,
1987
- gitToplevel,
1988
1710
  initRepoWiki,
1989
1711
  installPlugin,
1990
- isLinkedWorktree,
1991
1712
  listArticleTree,
1992
1713
  listAvailablePlugins,
1993
1714
  listCollections,
@@ -2002,7 +1723,6 @@ init_registry2();
2002
1723
  pluginsDir,
2003
1724
  pluginsRegistryPath,
2004
1725
  query,
2005
- rankDocs,
2006
1726
  readArticle,
2007
1727
  readConfig,
2008
1728
  readPluginRegistry,
@@ -2016,24 +1736,17 @@ init_registry2();
2016
1736
  removePlugin,
2017
1737
  removeTags,
2018
1738
  renderArticle,
2019
- repoId,
2020
- repoRemoteName,
2021
- resolveCwdCollection,
2022
1739
  resolveDefaultSearchMode,
2023
1740
  resolveHome,
2024
1741
  resolveLogLevel,
2025
- resolvePage,
2026
1742
  search,
2027
1743
  serializeArticle,
2028
1744
  setConfigValue,
2029
1745
  setTags,
2030
1746
  slugify,
2031
- toTreeNodes,
2032
- tokenize,
2033
1747
  unregisterCollection,
2034
1748
  updateArticleBody,
2035
1749
  upsertPlugin,
2036
- upstreamStatus,
2037
1750
  writeConfig,
2038
1751
  writePluginRegistry,
2039
1752
  writeRegistry