diffwiki-core 0.2.0-rc.f44d17c → 0.3.0-rc.202607211217.f5290fa
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 +202 -59
- package/dist/index.d.cts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +199 -59
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
NoDefaultCollectionError: () => NoDefaultCollectionError,
|
|
39
39
|
REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
|
|
40
40
|
addTags: () => addTags,
|
|
41
|
+
buildArticleTree: () => buildArticleTree,
|
|
41
42
|
collectionDir: () => collectionDir,
|
|
42
43
|
collectionsDir: () => collectionsDir,
|
|
43
44
|
configPath: () => configPath,
|
|
@@ -47,11 +48,13 @@ __export(index_exports, {
|
|
|
47
48
|
findCollection: () => findCollection,
|
|
48
49
|
getConfigValue: () => getConfigValue,
|
|
49
50
|
initRepoWiki: () => initRepoWiki,
|
|
51
|
+
listArticleTree: () => listArticleTree,
|
|
50
52
|
listCollections: () => listCollections,
|
|
51
53
|
parseAddTarget: () => parseAddTarget,
|
|
52
54
|
parseArticle: () => parseArticle,
|
|
53
55
|
parsePathTarget: () => parsePathTarget,
|
|
54
56
|
query: () => query,
|
|
57
|
+
readArticle: () => readArticle,
|
|
55
58
|
readConfig: () => readConfig,
|
|
56
59
|
readRegistry: () => readRegistry,
|
|
57
60
|
readRepoConfig: () => readRepoConfig,
|
|
@@ -126,51 +129,28 @@ function collectionDir(name) {
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
// src/config.ts
|
|
129
|
-
var
|
|
130
|
-
function isMissing(err) {
|
|
131
|
-
return err?.code === "ENOENT";
|
|
132
|
-
}
|
|
133
|
-
async function readConfig() {
|
|
134
|
-
try {
|
|
135
|
-
return JSON.parse(await import_promises.default.readFile(configPath(), "utf8"));
|
|
136
|
-
} catch (err) {
|
|
137
|
-
if (isMissing(err)) return {};
|
|
138
|
-
throw err;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
async function writeConfig(config) {
|
|
142
|
-
await import_promises.default.mkdir(resolveHome(), { recursive: true });
|
|
143
|
-
await import_promises.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
144
|
-
`, "utf8");
|
|
145
|
-
}
|
|
146
|
-
async function setConfigValue(key, value) {
|
|
147
|
-
const config = await readConfig();
|
|
148
|
-
config[key] = value;
|
|
149
|
-
await writeConfig(config);
|
|
150
|
-
}
|
|
151
|
-
async function getConfigValue(key) {
|
|
152
|
-
const config = await readConfig();
|
|
153
|
-
const value = config[key];
|
|
154
|
-
return typeof value === "string" ? value : void 0;
|
|
155
|
-
}
|
|
132
|
+
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
156
133
|
|
|
157
|
-
// src/
|
|
134
|
+
// src/collections.ts
|
|
158
135
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
159
|
-
|
|
136
|
+
|
|
137
|
+
// src/registry.ts
|
|
138
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
139
|
+
function isMissing(err) {
|
|
160
140
|
return err?.code === "ENOENT";
|
|
161
141
|
}
|
|
162
142
|
async function readRegistry() {
|
|
163
143
|
try {
|
|
164
|
-
const parsed = JSON.parse(await
|
|
144
|
+
const parsed = JSON.parse(await import_promises.default.readFile(registryPath(), "utf8"));
|
|
165
145
|
return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
|
|
166
146
|
} catch (err) {
|
|
167
|
-
if (
|
|
147
|
+
if (isMissing(err)) return { version: 1, collections: [] };
|
|
168
148
|
throw err;
|
|
169
149
|
}
|
|
170
150
|
}
|
|
171
151
|
async function writeRegistry(registry) {
|
|
172
|
-
await
|
|
173
|
-
await
|
|
152
|
+
await import_promises.default.mkdir(resolveHome(), { recursive: true });
|
|
153
|
+
await import_promises.default.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
|
|
174
154
|
`, "utf8");
|
|
175
155
|
}
|
|
176
156
|
async function registerCollection(entry) {
|
|
@@ -189,29 +169,7 @@ async function unregisterCollection(name) {
|
|
|
189
169
|
await writeRegistry(registry);
|
|
190
170
|
}
|
|
191
171
|
|
|
192
|
-
// src/slugify.ts
|
|
193
|
-
function slugify(input) {
|
|
194
|
-
const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
|
|
195
|
-
return slug.length > 0 ? slug : "untitled";
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// src/frontmatter.ts
|
|
199
|
-
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
200
|
-
function parseArticle(raw) {
|
|
201
|
-
const { data, content } = (0, import_gray_matter.default)(raw);
|
|
202
|
-
const title = typeof data.title === "string" ? data.title : "";
|
|
203
|
-
const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
|
|
204
|
-
return { title, tags, body: content.replace(/^\n+/, "") };
|
|
205
|
-
}
|
|
206
|
-
function serializeArticle(article) {
|
|
207
|
-
const front = { title: article.title, tags: article.tags };
|
|
208
|
-
if (article.created) front.created = article.created;
|
|
209
|
-
if (article.updated) front.updated = article.updated;
|
|
210
|
-
return import_gray_matter.default.stringify(article.body, front);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
172
|
// src/collections.ts
|
|
214
|
-
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
215
173
|
async function listCollections() {
|
|
216
174
|
return (await readRegistry()).collections;
|
|
217
175
|
}
|
|
@@ -221,7 +179,7 @@ async function findCollection(name) {
|
|
|
221
179
|
async function createCollection(name) {
|
|
222
180
|
if (await findCollection(name)) throw new CollectionExistsError(name);
|
|
223
181
|
const dir = collectionDir(name);
|
|
224
|
-
await
|
|
182
|
+
await import_promises2.default.mkdir(dir, { recursive: true });
|
|
225
183
|
const entry = {
|
|
226
184
|
name,
|
|
227
185
|
type: "global",
|
|
@@ -236,11 +194,64 @@ async function removeCollection(name) {
|
|
|
236
194
|
if (!entry) throw new CollectionNotFoundError(name);
|
|
237
195
|
await unregisterCollection(name);
|
|
238
196
|
if (entry.type === "global") {
|
|
239
|
-
await
|
|
197
|
+
await import_promises2.default.rm(entry.path, { recursive: true, force: true });
|
|
240
198
|
}
|
|
241
199
|
return entry;
|
|
242
200
|
}
|
|
243
201
|
|
|
202
|
+
// src/config.ts
|
|
203
|
+
function isMissing2(err) {
|
|
204
|
+
return err?.code === "ENOENT";
|
|
205
|
+
}
|
|
206
|
+
async function readConfig() {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(await import_promises3.default.readFile(configPath(), "utf8"));
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (isMissing2(err)) return {};
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function writeConfig(config) {
|
|
215
|
+
await import_promises3.default.mkdir(resolveHome(), { recursive: true });
|
|
216
|
+
await import_promises3.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
217
|
+
`, "utf8");
|
|
218
|
+
}
|
|
219
|
+
async function setConfigValue(key, value) {
|
|
220
|
+
if (key === "defaultCollection" && !await findCollection(value)) {
|
|
221
|
+
throw new CollectionNotFoundError(value);
|
|
222
|
+
}
|
|
223
|
+
const config = await readConfig();
|
|
224
|
+
config[key] = value;
|
|
225
|
+
await writeConfig(config);
|
|
226
|
+
}
|
|
227
|
+
async function getConfigValue(key) {
|
|
228
|
+
const config = await readConfig();
|
|
229
|
+
const value = config[key];
|
|
230
|
+
return typeof value === "string" ? value : void 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/slugify.ts
|
|
234
|
+
function slugify(input) {
|
|
235
|
+
const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
|
|
236
|
+
return slug.length > 0 ? slug : "untitled";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/frontmatter.ts
|
|
240
|
+
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
241
|
+
function parseArticle(raw) {
|
|
242
|
+
const { data, content } = (0, import_gray_matter.default)(raw);
|
|
243
|
+
const title = typeof data.title === "string" ? data.title : "";
|
|
244
|
+
const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
|
|
245
|
+
const created = typeof data.created === "string" ? data.created : void 0;
|
|
246
|
+
return { title, tags, created, body: content.replace(/^\n+/, "") };
|
|
247
|
+
}
|
|
248
|
+
function serializeArticle(article) {
|
|
249
|
+
const front = { title: article.title, tags: article.tags };
|
|
250
|
+
if (article.created) front.created = article.created;
|
|
251
|
+
if (article.updated) front.updated = article.updated;
|
|
252
|
+
return import_gray_matter.default.stringify(article.body, front);
|
|
253
|
+
}
|
|
254
|
+
|
|
244
255
|
// src/articles.ts
|
|
245
256
|
var import_promises4 = __toESM(require("fs/promises"), 1);
|
|
246
257
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
@@ -275,7 +286,11 @@ async function resolveCollectionDir(name) {
|
|
|
275
286
|
return { name: coll, dir: entry.path };
|
|
276
287
|
}
|
|
277
288
|
function ensureMdPath(file) {
|
|
278
|
-
|
|
289
|
+
if (/\.mdx?$/.test(file)) return file;
|
|
290
|
+
const dir = import_node_path2.default.dirname(file);
|
|
291
|
+
const base = slugify(import_node_path2.default.basename(file));
|
|
292
|
+
const rel = dir === "." ? `${base}.md` : import_node_path2.default.join(dir, `${base}.md`);
|
|
293
|
+
return rel;
|
|
279
294
|
}
|
|
280
295
|
async function createArticle(opts) {
|
|
281
296
|
const { collection, title } = parseAddTarget(opts.target);
|
|
@@ -307,6 +322,7 @@ async function updateArticleBody(target, body) {
|
|
|
307
322
|
serializeArticle({
|
|
308
323
|
title: parsed.title,
|
|
309
324
|
tags: parsed.tags,
|
|
325
|
+
created: parsed.created,
|
|
310
326
|
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
311
327
|
body
|
|
312
328
|
}),
|
|
@@ -323,6 +339,7 @@ async function mutateTags(target, fn) {
|
|
|
323
339
|
serializeArticle({
|
|
324
340
|
title: parsed.title,
|
|
325
341
|
tags,
|
|
342
|
+
created: parsed.created,
|
|
326
343
|
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
327
344
|
body: parsed.body
|
|
328
345
|
}),
|
|
@@ -414,7 +431,8 @@ async function query(term, collection) {
|
|
|
414
431
|
let targets;
|
|
415
432
|
if (collection) {
|
|
416
433
|
const one = await findCollection(collection);
|
|
417
|
-
|
|
434
|
+
if (!one) throw new CollectionNotFoundError(collection);
|
|
435
|
+
targets = [one];
|
|
418
436
|
} else {
|
|
419
437
|
targets = await listCollections();
|
|
420
438
|
}
|
|
@@ -479,6 +497,128 @@ async function doctor() {
|
|
|
479
497
|
}
|
|
480
498
|
return out;
|
|
481
499
|
}
|
|
500
|
+
|
|
501
|
+
// src/browse.ts
|
|
502
|
+
var import_promises8 = __toESM(require("fs/promises"), 1);
|
|
503
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
504
|
+
function buildArticleTree(relPaths) {
|
|
505
|
+
const root = {};
|
|
506
|
+
for (const raw of relPaths) {
|
|
507
|
+
const rel = raw.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
508
|
+
if (!rel) continue;
|
|
509
|
+
const segs = rel.split("/");
|
|
510
|
+
let cur = root;
|
|
511
|
+
segs.forEach((seg, i) => {
|
|
512
|
+
if (i === segs.length - 1) {
|
|
513
|
+
if (!(seg in cur)) cur[seg] = null;
|
|
514
|
+
} else {
|
|
515
|
+
if (cur[seg] === null || !(seg in cur)) cur[seg] = {};
|
|
516
|
+
cur = cur[seg];
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
const stripExt = (f) => f.replace(/\.(mdx?)$/, "");
|
|
521
|
+
const walk = (trie, parents) => {
|
|
522
|
+
const entries = Object.entries(trie);
|
|
523
|
+
const branches = entries.filter(([, v]) => v !== null);
|
|
524
|
+
const leaves = entries.filter(([, v]) => v === null).map(([k]) => k);
|
|
525
|
+
branches.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
526
|
+
leaves.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
527
|
+
const branchNodes = branches.map(([seg, sub]) => ({
|
|
528
|
+
name: seg,
|
|
529
|
+
title: seg,
|
|
530
|
+
children: walk(sub, [...parents, seg])
|
|
531
|
+
}));
|
|
532
|
+
const leafNodes = leaves.map((file) => {
|
|
533
|
+
const nameNoExt = stripExt(file);
|
|
534
|
+
return { name: nameNoExt, title: nameNoExt, slug: [...parents, nameNoExt].join("/") };
|
|
535
|
+
});
|
|
536
|
+
return [...branchNodes, ...leafNodes];
|
|
537
|
+
};
|
|
538
|
+
return walk(root, []);
|
|
539
|
+
}
|
|
540
|
+
async function collectRelPaths(dir, rel = "") {
|
|
541
|
+
let dirents;
|
|
542
|
+
try {
|
|
543
|
+
dirents = await import_promises8.default.readdir(dir, { withFileTypes: true });
|
|
544
|
+
} catch {
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
const out = [];
|
|
548
|
+
for (const d of dirents) {
|
|
549
|
+
if (d.name.startsWith(".")) continue;
|
|
550
|
+
const childRel = rel ? `${rel}/${d.name}` : d.name;
|
|
551
|
+
if (d.isDirectory()) {
|
|
552
|
+
out.push(...await collectRelPaths(import_node_path5.default.join(dir, d.name), childRel));
|
|
553
|
+
} else if (/\.(mdx?)$/.test(d.name)) {
|
|
554
|
+
out.push(childRel);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
async function cheapTitle(absPath) {
|
|
560
|
+
let handle;
|
|
561
|
+
try {
|
|
562
|
+
handle = await import_promises8.default.open(absPath, "r");
|
|
563
|
+
const buf = Buffer.alloc(512);
|
|
564
|
+
const { bytesRead } = await handle.read(buf, 0, 512, 0);
|
|
565
|
+
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
566
|
+
const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
|
|
567
|
+
if (m) return m[1].trim().replace(/^["']|["']$/g, "");
|
|
568
|
+
} catch {
|
|
569
|
+
} finally {
|
|
570
|
+
await handle?.close();
|
|
571
|
+
}
|
|
572
|
+
return void 0;
|
|
573
|
+
}
|
|
574
|
+
async function enrichTitles(nodes, collDir) {
|
|
575
|
+
return Promise.all(
|
|
576
|
+
nodes.map(async (node) => {
|
|
577
|
+
if (node.children) {
|
|
578
|
+
return { ...node, children: await enrichTitles(node.children, collDir) };
|
|
579
|
+
}
|
|
580
|
+
for (const ext of [".mdx", ".md"]) {
|
|
581
|
+
const title = await cheapTitle(import_node_path5.default.join(collDir, `${node.slug}${ext}`));
|
|
582
|
+
if (title) return { ...node, title };
|
|
583
|
+
}
|
|
584
|
+
return node;
|
|
585
|
+
})
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
async function listArticleTree(coll) {
|
|
589
|
+
const entry = await findCollection(coll);
|
|
590
|
+
if (!entry) throw new CollectionNotFoundError(coll);
|
|
591
|
+
const rels = await collectRelPaths(entry.path);
|
|
592
|
+
return enrichTitles(buildArticleTree(rels), import_node_path5.default.resolve(entry.path));
|
|
593
|
+
}
|
|
594
|
+
async function readArticle(coll, slug) {
|
|
595
|
+
const entry = await findCollection(coll);
|
|
596
|
+
if (!entry) throw new CollectionNotFoundError(coll);
|
|
597
|
+
const collDir = import_node_path5.default.resolve(entry.path);
|
|
598
|
+
const normalized = slug.replace(/\\/g, "/");
|
|
599
|
+
if (normalized.split("/").some((s) => s === "..")) {
|
|
600
|
+
throw new InvalidTargetError(slug, "path traversal detected");
|
|
601
|
+
}
|
|
602
|
+
const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
603
|
+
if (!safeSlug) throw new InvalidTargetError(slug, "expected a non-empty article slug");
|
|
604
|
+
let abs;
|
|
605
|
+
for (const ext of [".mdx", ".md"]) {
|
|
606
|
+
const candidate = import_node_path5.default.resolve(collDir, `${safeSlug}${ext}`);
|
|
607
|
+
if (candidate !== collDir && !candidate.startsWith(collDir + import_node_path5.default.sep)) {
|
|
608
|
+
throw new InvalidTargetError(slug, "path traversal detected");
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
await import_promises8.default.access(candidate);
|
|
612
|
+
abs = candidate;
|
|
613
|
+
break;
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
618
|
+
const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
|
|
619
|
+
const title = parsed.title?.trim() || (safeSlug.split("/").pop() ?? safeSlug);
|
|
620
|
+
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
|
|
621
|
+
}
|
|
482
622
|
// Annotate the CommonJS export names for ESM import in node:
|
|
483
623
|
0 && (module.exports = {
|
|
484
624
|
ArticleNotFoundError,
|
|
@@ -489,6 +629,7 @@ async function doctor() {
|
|
|
489
629
|
NoDefaultCollectionError,
|
|
490
630
|
REPO_CONFIG_FILE,
|
|
491
631
|
addTags,
|
|
632
|
+
buildArticleTree,
|
|
492
633
|
collectionDir,
|
|
493
634
|
collectionsDir,
|
|
494
635
|
configPath,
|
|
@@ -498,11 +639,13 @@ async function doctor() {
|
|
|
498
639
|
findCollection,
|
|
499
640
|
getConfigValue,
|
|
500
641
|
initRepoWiki,
|
|
642
|
+
listArticleTree,
|
|
501
643
|
listCollections,
|
|
502
644
|
parseAddTarget,
|
|
503
645
|
parseArticle,
|
|
504
646
|
parsePathTarget,
|
|
505
647
|
query,
|
|
648
|
+
readArticle,
|
|
506
649
|
readConfig,
|
|
507
650
|
readRegistry,
|
|
508
651
|
readRepoConfig,
|
package/dist/index.d.cts
CHANGED
|
@@ -25,6 +25,28 @@ interface RepoConfig {
|
|
|
25
25
|
/** Path to the wiki directory, relative to the repo root. */
|
|
26
26
|
path: string;
|
|
27
27
|
}
|
|
28
|
+
/** A node in a collection's article tree. Branches have `children`; leaves have `slug`. */
|
|
29
|
+
interface ArticleNode {
|
|
30
|
+
/** Segment label — dir name for branches, filename-without-ext for leaves. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Display title — frontmatter `title` for leaves when available, else `name`. */
|
|
33
|
+
title?: string;
|
|
34
|
+
/** Leaf only: article path within the collection, without extension, e.g. "guide/setup". */
|
|
35
|
+
slug?: string;
|
|
36
|
+
/** Branch only: nested nodes. */
|
|
37
|
+
children?: ArticleNode[];
|
|
38
|
+
}
|
|
39
|
+
/** A resolved article's content, returned by `readArticle`. */
|
|
40
|
+
interface ArticleContent {
|
|
41
|
+
coll: string;
|
|
42
|
+
slug: string;
|
|
43
|
+
title: string;
|
|
44
|
+
tags: string[];
|
|
45
|
+
/** Frontmatter-stripped body (md/mdx). */
|
|
46
|
+
body: string;
|
|
47
|
+
/** Absolute path to the file on disk. */
|
|
48
|
+
path: string;
|
|
49
|
+
}
|
|
28
50
|
|
|
29
51
|
/** Base class for all expected, user-facing diffwiki failures. */
|
|
30
52
|
declare class DiffwikiError extends Error {
|
|
@@ -70,6 +92,7 @@ declare function slugify(input: string): string;
|
|
|
70
92
|
interface ParsedArticle {
|
|
71
93
|
title: string;
|
|
72
94
|
tags: string[];
|
|
95
|
+
created?: string;
|
|
73
96
|
body: string;
|
|
74
97
|
}
|
|
75
98
|
interface SerializableArticle {
|
|
@@ -141,4 +164,23 @@ interface Diagnostic {
|
|
|
141
164
|
}
|
|
142
165
|
declare function doctor(): Promise<Diagnostic[]>;
|
|
143
166
|
|
|
144
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Build a nested ArticleNode[] from a flat list of relative file paths.
|
|
169
|
+
* Pure — no I/O. Directories become branches; files become leaves whose `slug`
|
|
170
|
+
* is the relative path without extension. Dirs-first, then alpha (case-insensitive).
|
|
171
|
+
*/
|
|
172
|
+
declare function buildArticleTree(relPaths: string[]): ArticleNode[];
|
|
173
|
+
/**
|
|
174
|
+
* List the article tree of a collection. Walks the collection's registered
|
|
175
|
+
* `entry.path` — so collections living OUTSIDE `~/.diffwiki/collections`
|
|
176
|
+
* (e.g. in-repo wikis registered via `diffwiki init`) work unchanged.
|
|
177
|
+
*/
|
|
178
|
+
declare function listArticleTree(coll: string): Promise<ArticleNode[]>;
|
|
179
|
+
/**
|
|
180
|
+
* Read and resolve a single article by collection + slug (relative path without
|
|
181
|
+
* extension). Tries `.mdx` then `.md`, guards against path traversal, and splits
|
|
182
|
+
* frontmatter. Throws CollectionNotFoundError / ArticleNotFoundError / InvalidTargetError.
|
|
183
|
+
*/
|
|
184
|
+
declare function readArticle(coll: string, slug: string): Promise<ArticleContent>;
|
|
185
|
+
|
|
186
|
+
export { type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, type CollectionEntry, CollectionExistsError, CollectionNotFoundError, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type InitOptions, InvalidTargetError, NoDefaultCollectionError, type ParsedArticle, type QueryHit, REPO_CONFIG_FILE, type Registry, type RepoConfig, type SerializableArticle, addTags, buildArticleTree, collectionDir, collectionsDir, configPath, createArticle, createCollection, doctor, findCollection, getConfigValue, initRepoWiki, listArticleTree, listCollections, parseAddTarget, parseArticle, parsePathTarget, query, readArticle, readConfig, readRegistry, readRepoConfig, registerCollection, registryPath, removeArticle, removeCollection, removeTags, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,28 @@ interface RepoConfig {
|
|
|
25
25
|
/** Path to the wiki directory, relative to the repo root. */
|
|
26
26
|
path: string;
|
|
27
27
|
}
|
|
28
|
+
/** A node in a collection's article tree. Branches have `children`; leaves have `slug`. */
|
|
29
|
+
interface ArticleNode {
|
|
30
|
+
/** Segment label — dir name for branches, filename-without-ext for leaves. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Display title — frontmatter `title` for leaves when available, else `name`. */
|
|
33
|
+
title?: string;
|
|
34
|
+
/** Leaf only: article path within the collection, without extension, e.g. "guide/setup". */
|
|
35
|
+
slug?: string;
|
|
36
|
+
/** Branch only: nested nodes. */
|
|
37
|
+
children?: ArticleNode[];
|
|
38
|
+
}
|
|
39
|
+
/** A resolved article's content, returned by `readArticle`. */
|
|
40
|
+
interface ArticleContent {
|
|
41
|
+
coll: string;
|
|
42
|
+
slug: string;
|
|
43
|
+
title: string;
|
|
44
|
+
tags: string[];
|
|
45
|
+
/** Frontmatter-stripped body (md/mdx). */
|
|
46
|
+
body: string;
|
|
47
|
+
/** Absolute path to the file on disk. */
|
|
48
|
+
path: string;
|
|
49
|
+
}
|
|
28
50
|
|
|
29
51
|
/** Base class for all expected, user-facing diffwiki failures. */
|
|
30
52
|
declare class DiffwikiError extends Error {
|
|
@@ -70,6 +92,7 @@ declare function slugify(input: string): string;
|
|
|
70
92
|
interface ParsedArticle {
|
|
71
93
|
title: string;
|
|
72
94
|
tags: string[];
|
|
95
|
+
created?: string;
|
|
73
96
|
body: string;
|
|
74
97
|
}
|
|
75
98
|
interface SerializableArticle {
|
|
@@ -141,4 +164,23 @@ interface Diagnostic {
|
|
|
141
164
|
}
|
|
142
165
|
declare function doctor(): Promise<Diagnostic[]>;
|
|
143
166
|
|
|
144
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Build a nested ArticleNode[] from a flat list of relative file paths.
|
|
169
|
+
* Pure — no I/O. Directories become branches; files become leaves whose `slug`
|
|
170
|
+
* is the relative path without extension. Dirs-first, then alpha (case-insensitive).
|
|
171
|
+
*/
|
|
172
|
+
declare function buildArticleTree(relPaths: string[]): ArticleNode[];
|
|
173
|
+
/**
|
|
174
|
+
* List the article tree of a collection. Walks the collection's registered
|
|
175
|
+
* `entry.path` — so collections living OUTSIDE `~/.diffwiki/collections`
|
|
176
|
+
* (e.g. in-repo wikis registered via `diffwiki init`) work unchanged.
|
|
177
|
+
*/
|
|
178
|
+
declare function listArticleTree(coll: string): Promise<ArticleNode[]>;
|
|
179
|
+
/**
|
|
180
|
+
* Read and resolve a single article by collection + slug (relative path without
|
|
181
|
+
* extension). Tries `.mdx` then `.md`, guards against path traversal, and splits
|
|
182
|
+
* frontmatter. Throws CollectionNotFoundError / ArticleNotFoundError / InvalidTargetError.
|
|
183
|
+
*/
|
|
184
|
+
declare function readArticle(coll: string, slug: string): Promise<ArticleContent>;
|
|
185
|
+
|
|
186
|
+
export { type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, type CollectionEntry, CollectionExistsError, CollectionNotFoundError, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type InitOptions, InvalidTargetError, NoDefaultCollectionError, type ParsedArticle, type QueryHit, REPO_CONFIG_FILE, type Registry, type RepoConfig, type SerializableArticle, addTags, buildArticleTree, collectionDir, collectionsDir, configPath, createArticle, createCollection, doctor, findCollection, getConfigValue, initRepoWiki, listArticleTree, listCollections, parseAddTarget, parseArticle, parsePathTarget, query, readArticle, readConfig, readRegistry, readRepoConfig, registerCollection, registryPath, removeArticle, removeCollection, removeTags, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
|
package/dist/index.js
CHANGED
|
@@ -52,51 +52,28 @@ function collectionDir(name) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// src/config.ts
|
|
55
|
-
import
|
|
56
|
-
function isMissing(err) {
|
|
57
|
-
return err?.code === "ENOENT";
|
|
58
|
-
}
|
|
59
|
-
async function readConfig() {
|
|
60
|
-
try {
|
|
61
|
-
return JSON.parse(await fs.readFile(configPath(), "utf8"));
|
|
62
|
-
} catch (err) {
|
|
63
|
-
if (isMissing(err)) return {};
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
async function writeConfig(config) {
|
|
68
|
-
await fs.mkdir(resolveHome(), { recursive: true });
|
|
69
|
-
await fs.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
70
|
-
`, "utf8");
|
|
71
|
-
}
|
|
72
|
-
async function setConfigValue(key, value) {
|
|
73
|
-
const config = await readConfig();
|
|
74
|
-
config[key] = value;
|
|
75
|
-
await writeConfig(config);
|
|
76
|
-
}
|
|
77
|
-
async function getConfigValue(key) {
|
|
78
|
-
const config = await readConfig();
|
|
79
|
-
const value = config[key];
|
|
80
|
-
return typeof value === "string" ? value : void 0;
|
|
81
|
-
}
|
|
55
|
+
import fs3 from "fs/promises";
|
|
82
56
|
|
|
83
|
-
// src/
|
|
57
|
+
// src/collections.ts
|
|
84
58
|
import fs2 from "fs/promises";
|
|
85
|
-
|
|
59
|
+
|
|
60
|
+
// src/registry.ts
|
|
61
|
+
import fs from "fs/promises";
|
|
62
|
+
function isMissing(err) {
|
|
86
63
|
return err?.code === "ENOENT";
|
|
87
64
|
}
|
|
88
65
|
async function readRegistry() {
|
|
89
66
|
try {
|
|
90
|
-
const parsed = JSON.parse(await
|
|
67
|
+
const parsed = JSON.parse(await fs.readFile(registryPath(), "utf8"));
|
|
91
68
|
return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
|
|
92
69
|
} catch (err) {
|
|
93
|
-
if (
|
|
70
|
+
if (isMissing(err)) return { version: 1, collections: [] };
|
|
94
71
|
throw err;
|
|
95
72
|
}
|
|
96
73
|
}
|
|
97
74
|
async function writeRegistry(registry) {
|
|
98
|
-
await
|
|
99
|
-
await
|
|
75
|
+
await fs.mkdir(resolveHome(), { recursive: true });
|
|
76
|
+
await fs.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
|
|
100
77
|
`, "utf8");
|
|
101
78
|
}
|
|
102
79
|
async function registerCollection(entry) {
|
|
@@ -115,29 +92,7 @@ async function unregisterCollection(name) {
|
|
|
115
92
|
await writeRegistry(registry);
|
|
116
93
|
}
|
|
117
94
|
|
|
118
|
-
// src/slugify.ts
|
|
119
|
-
function slugify(input) {
|
|
120
|
-
const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
|
|
121
|
-
return slug.length > 0 ? slug : "untitled";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// src/frontmatter.ts
|
|
125
|
-
import matter from "gray-matter";
|
|
126
|
-
function parseArticle(raw) {
|
|
127
|
-
const { data, content } = matter(raw);
|
|
128
|
-
const title = typeof data.title === "string" ? data.title : "";
|
|
129
|
-
const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
|
|
130
|
-
return { title, tags, body: content.replace(/^\n+/, "") };
|
|
131
|
-
}
|
|
132
|
-
function serializeArticle(article) {
|
|
133
|
-
const front = { title: article.title, tags: article.tags };
|
|
134
|
-
if (article.created) front.created = article.created;
|
|
135
|
-
if (article.updated) front.updated = article.updated;
|
|
136
|
-
return matter.stringify(article.body, front);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
95
|
// src/collections.ts
|
|
140
|
-
import fs3 from "fs/promises";
|
|
141
96
|
async function listCollections() {
|
|
142
97
|
return (await readRegistry()).collections;
|
|
143
98
|
}
|
|
@@ -147,7 +102,7 @@ async function findCollection(name) {
|
|
|
147
102
|
async function createCollection(name) {
|
|
148
103
|
if (await findCollection(name)) throw new CollectionExistsError(name);
|
|
149
104
|
const dir = collectionDir(name);
|
|
150
|
-
await
|
|
105
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
151
106
|
const entry = {
|
|
152
107
|
name,
|
|
153
108
|
type: "global",
|
|
@@ -162,11 +117,64 @@ async function removeCollection(name) {
|
|
|
162
117
|
if (!entry) throw new CollectionNotFoundError(name);
|
|
163
118
|
await unregisterCollection(name);
|
|
164
119
|
if (entry.type === "global") {
|
|
165
|
-
await
|
|
120
|
+
await fs2.rm(entry.path, { recursive: true, force: true });
|
|
166
121
|
}
|
|
167
122
|
return entry;
|
|
168
123
|
}
|
|
169
124
|
|
|
125
|
+
// src/config.ts
|
|
126
|
+
function isMissing2(err) {
|
|
127
|
+
return err?.code === "ENOENT";
|
|
128
|
+
}
|
|
129
|
+
async function readConfig() {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(await fs3.readFile(configPath(), "utf8"));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (isMissing2(err)) return {};
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function writeConfig(config) {
|
|
138
|
+
await fs3.mkdir(resolveHome(), { recursive: true });
|
|
139
|
+
await fs3.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
140
|
+
`, "utf8");
|
|
141
|
+
}
|
|
142
|
+
async function setConfigValue(key, value) {
|
|
143
|
+
if (key === "defaultCollection" && !await findCollection(value)) {
|
|
144
|
+
throw new CollectionNotFoundError(value);
|
|
145
|
+
}
|
|
146
|
+
const config = await readConfig();
|
|
147
|
+
config[key] = value;
|
|
148
|
+
await writeConfig(config);
|
|
149
|
+
}
|
|
150
|
+
async function getConfigValue(key) {
|
|
151
|
+
const config = await readConfig();
|
|
152
|
+
const value = config[key];
|
|
153
|
+
return typeof value === "string" ? value : void 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/slugify.ts
|
|
157
|
+
function slugify(input) {
|
|
158
|
+
const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
|
|
159
|
+
return slug.length > 0 ? slug : "untitled";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/frontmatter.ts
|
|
163
|
+
import matter from "gray-matter";
|
|
164
|
+
function parseArticle(raw) {
|
|
165
|
+
const { data, content } = matter(raw);
|
|
166
|
+
const title = typeof data.title === "string" ? data.title : "";
|
|
167
|
+
const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
|
|
168
|
+
const created = typeof data.created === "string" ? data.created : void 0;
|
|
169
|
+
return { title, tags, created, body: content.replace(/^\n+/, "") };
|
|
170
|
+
}
|
|
171
|
+
function serializeArticle(article) {
|
|
172
|
+
const front = { title: article.title, tags: article.tags };
|
|
173
|
+
if (article.created) front.created = article.created;
|
|
174
|
+
if (article.updated) front.updated = article.updated;
|
|
175
|
+
return matter.stringify(article.body, front);
|
|
176
|
+
}
|
|
177
|
+
|
|
170
178
|
// src/articles.ts
|
|
171
179
|
import fs4 from "fs/promises";
|
|
172
180
|
import path2 from "path";
|
|
@@ -201,7 +209,11 @@ async function resolveCollectionDir(name) {
|
|
|
201
209
|
return { name: coll, dir: entry.path };
|
|
202
210
|
}
|
|
203
211
|
function ensureMdPath(file) {
|
|
204
|
-
|
|
212
|
+
if (/\.mdx?$/.test(file)) return file;
|
|
213
|
+
const dir = path2.dirname(file);
|
|
214
|
+
const base = slugify(path2.basename(file));
|
|
215
|
+
const rel = dir === "." ? `${base}.md` : path2.join(dir, `${base}.md`);
|
|
216
|
+
return rel;
|
|
205
217
|
}
|
|
206
218
|
async function createArticle(opts) {
|
|
207
219
|
const { collection, title } = parseAddTarget(opts.target);
|
|
@@ -233,6 +245,7 @@ async function updateArticleBody(target, body) {
|
|
|
233
245
|
serializeArticle({
|
|
234
246
|
title: parsed.title,
|
|
235
247
|
tags: parsed.tags,
|
|
248
|
+
created: parsed.created,
|
|
236
249
|
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
237
250
|
body
|
|
238
251
|
}),
|
|
@@ -249,6 +262,7 @@ async function mutateTags(target, fn) {
|
|
|
249
262
|
serializeArticle({
|
|
250
263
|
title: parsed.title,
|
|
251
264
|
tags,
|
|
265
|
+
created: parsed.created,
|
|
252
266
|
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
253
267
|
body: parsed.body
|
|
254
268
|
}),
|
|
@@ -340,7 +354,8 @@ async function query(term, collection) {
|
|
|
340
354
|
let targets;
|
|
341
355
|
if (collection) {
|
|
342
356
|
const one = await findCollection(collection);
|
|
343
|
-
|
|
357
|
+
if (!one) throw new CollectionNotFoundError(collection);
|
|
358
|
+
targets = [one];
|
|
344
359
|
} else {
|
|
345
360
|
targets = await listCollections();
|
|
346
361
|
}
|
|
@@ -405,6 +420,128 @@ async function doctor() {
|
|
|
405
420
|
}
|
|
406
421
|
return out;
|
|
407
422
|
}
|
|
423
|
+
|
|
424
|
+
// src/browse.ts
|
|
425
|
+
import fs8 from "fs/promises";
|
|
426
|
+
import path5 from "path";
|
|
427
|
+
function buildArticleTree(relPaths) {
|
|
428
|
+
const root = {};
|
|
429
|
+
for (const raw of relPaths) {
|
|
430
|
+
const rel = raw.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
431
|
+
if (!rel) continue;
|
|
432
|
+
const segs = rel.split("/");
|
|
433
|
+
let cur = root;
|
|
434
|
+
segs.forEach((seg, i) => {
|
|
435
|
+
if (i === segs.length - 1) {
|
|
436
|
+
if (!(seg in cur)) cur[seg] = null;
|
|
437
|
+
} else {
|
|
438
|
+
if (cur[seg] === null || !(seg in cur)) cur[seg] = {};
|
|
439
|
+
cur = cur[seg];
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const stripExt = (f) => f.replace(/\.(mdx?)$/, "");
|
|
444
|
+
const walk = (trie, parents) => {
|
|
445
|
+
const entries = Object.entries(trie);
|
|
446
|
+
const branches = entries.filter(([, v]) => v !== null);
|
|
447
|
+
const leaves = entries.filter(([, v]) => v === null).map(([k]) => k);
|
|
448
|
+
branches.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
449
|
+
leaves.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
450
|
+
const branchNodes = branches.map(([seg, sub]) => ({
|
|
451
|
+
name: seg,
|
|
452
|
+
title: seg,
|
|
453
|
+
children: walk(sub, [...parents, seg])
|
|
454
|
+
}));
|
|
455
|
+
const leafNodes = leaves.map((file) => {
|
|
456
|
+
const nameNoExt = stripExt(file);
|
|
457
|
+
return { name: nameNoExt, title: nameNoExt, slug: [...parents, nameNoExt].join("/") };
|
|
458
|
+
});
|
|
459
|
+
return [...branchNodes, ...leafNodes];
|
|
460
|
+
};
|
|
461
|
+
return walk(root, []);
|
|
462
|
+
}
|
|
463
|
+
async function collectRelPaths(dir, rel = "") {
|
|
464
|
+
let dirents;
|
|
465
|
+
try {
|
|
466
|
+
dirents = await fs8.readdir(dir, { withFileTypes: true });
|
|
467
|
+
} catch {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
const out = [];
|
|
471
|
+
for (const d of dirents) {
|
|
472
|
+
if (d.name.startsWith(".")) continue;
|
|
473
|
+
const childRel = rel ? `${rel}/${d.name}` : d.name;
|
|
474
|
+
if (d.isDirectory()) {
|
|
475
|
+
out.push(...await collectRelPaths(path5.join(dir, d.name), childRel));
|
|
476
|
+
} else if (/\.(mdx?)$/.test(d.name)) {
|
|
477
|
+
out.push(childRel);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
async function cheapTitle(absPath) {
|
|
483
|
+
let handle;
|
|
484
|
+
try {
|
|
485
|
+
handle = await fs8.open(absPath, "r");
|
|
486
|
+
const buf = Buffer.alloc(512);
|
|
487
|
+
const { bytesRead } = await handle.read(buf, 0, 512, 0);
|
|
488
|
+
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
489
|
+
const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
|
|
490
|
+
if (m) return m[1].trim().replace(/^["']|["']$/g, "");
|
|
491
|
+
} catch {
|
|
492
|
+
} finally {
|
|
493
|
+
await handle?.close();
|
|
494
|
+
}
|
|
495
|
+
return void 0;
|
|
496
|
+
}
|
|
497
|
+
async function enrichTitles(nodes, collDir) {
|
|
498
|
+
return Promise.all(
|
|
499
|
+
nodes.map(async (node) => {
|
|
500
|
+
if (node.children) {
|
|
501
|
+
return { ...node, children: await enrichTitles(node.children, collDir) };
|
|
502
|
+
}
|
|
503
|
+
for (const ext of [".mdx", ".md"]) {
|
|
504
|
+
const title = await cheapTitle(path5.join(collDir, `${node.slug}${ext}`));
|
|
505
|
+
if (title) return { ...node, title };
|
|
506
|
+
}
|
|
507
|
+
return node;
|
|
508
|
+
})
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
async function listArticleTree(coll) {
|
|
512
|
+
const entry = await findCollection(coll);
|
|
513
|
+
if (!entry) throw new CollectionNotFoundError(coll);
|
|
514
|
+
const rels = await collectRelPaths(entry.path);
|
|
515
|
+
return enrichTitles(buildArticleTree(rels), path5.resolve(entry.path));
|
|
516
|
+
}
|
|
517
|
+
async function readArticle(coll, slug) {
|
|
518
|
+
const entry = await findCollection(coll);
|
|
519
|
+
if (!entry) throw new CollectionNotFoundError(coll);
|
|
520
|
+
const collDir = path5.resolve(entry.path);
|
|
521
|
+
const normalized = slug.replace(/\\/g, "/");
|
|
522
|
+
if (normalized.split("/").some((s) => s === "..")) {
|
|
523
|
+
throw new InvalidTargetError(slug, "path traversal detected");
|
|
524
|
+
}
|
|
525
|
+
const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
526
|
+
if (!safeSlug) throw new InvalidTargetError(slug, "expected a non-empty article slug");
|
|
527
|
+
let abs;
|
|
528
|
+
for (const ext of [".mdx", ".md"]) {
|
|
529
|
+
const candidate = path5.resolve(collDir, `${safeSlug}${ext}`);
|
|
530
|
+
if (candidate !== collDir && !candidate.startsWith(collDir + path5.sep)) {
|
|
531
|
+
throw new InvalidTargetError(slug, "path traversal detected");
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
await fs8.access(candidate);
|
|
535
|
+
abs = candidate;
|
|
536
|
+
break;
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
541
|
+
const parsed = parseArticle(await fs8.readFile(abs, "utf8"));
|
|
542
|
+
const title = parsed.title?.trim() || (safeSlug.split("/").pop() ?? safeSlug);
|
|
543
|
+
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
|
|
544
|
+
}
|
|
408
545
|
export {
|
|
409
546
|
ArticleNotFoundError,
|
|
410
547
|
CollectionExistsError,
|
|
@@ -414,6 +551,7 @@ export {
|
|
|
414
551
|
NoDefaultCollectionError,
|
|
415
552
|
REPO_CONFIG_FILE,
|
|
416
553
|
addTags,
|
|
554
|
+
buildArticleTree,
|
|
417
555
|
collectionDir,
|
|
418
556
|
collectionsDir,
|
|
419
557
|
configPath,
|
|
@@ -423,11 +561,13 @@ export {
|
|
|
423
561
|
findCollection,
|
|
424
562
|
getConfigValue,
|
|
425
563
|
initRepoWiki,
|
|
564
|
+
listArticleTree,
|
|
426
565
|
listCollections,
|
|
427
566
|
parseAddTarget,
|
|
428
567
|
parseArticle,
|
|
429
568
|
parsePathTarget,
|
|
430
569
|
query,
|
|
570
|
+
readArticle,
|
|
431
571
|
readConfig,
|
|
432
572
|
readRegistry,
|
|
433
573
|
readRepoConfig,
|