diffwiki-core 0.7.0 → 0.8.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 +155 -33
- package/dist/index.d.cts +43 -2
- package/dist/index.d.ts +43 -2
- package/dist/index.js +154 -33
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -582,6 +582,7 @@ __export(index_exports, {
|
|
|
582
582
|
buildSearchIndex: () => buildSearchIndex,
|
|
583
583
|
collectionDir: () => collectionDir,
|
|
584
584
|
collectionGitStatus: () => collectionGitStatus,
|
|
585
|
+
collectionMeta: () => collectionMeta,
|
|
585
586
|
collectionsDir: () => collectionsDir,
|
|
586
587
|
configPath: () => configPath,
|
|
587
588
|
createArticle: () => createArticle,
|
|
@@ -729,22 +730,39 @@ var YAML_ENGINE = {
|
|
|
729
730
|
stringify: (data) => (0, import_yaml.stringify)(data)
|
|
730
731
|
};
|
|
731
732
|
var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
|
|
733
|
+
function coerceAudience(value) {
|
|
734
|
+
return value === "private" ? "private" : "public";
|
|
735
|
+
}
|
|
736
|
+
function coerceStatus(value) {
|
|
737
|
+
return value === "draft" ? "draft" : "published";
|
|
738
|
+
}
|
|
732
739
|
function parseFallback(raw) {
|
|
733
740
|
const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
|
|
734
741
|
const fenceMatch = fenceRe.exec(raw);
|
|
735
742
|
let title = "";
|
|
743
|
+
let audience = "public";
|
|
744
|
+
let status = "published";
|
|
736
745
|
let body;
|
|
746
|
+
const clean = (s) => s.trim().replace(/^["']|["']$/g, "");
|
|
737
747
|
if (fenceMatch) {
|
|
738
748
|
const inner = fenceMatch[1];
|
|
739
749
|
const titleMatch = /^title:\s*(.+)$/m.exec(inner);
|
|
740
750
|
if (titleMatch) {
|
|
741
|
-
title = titleMatch[1]
|
|
751
|
+
title = clean(titleMatch[1]);
|
|
752
|
+
}
|
|
753
|
+
const audienceMatch = /^audience:\s*(.+)$/m.exec(inner);
|
|
754
|
+
if (audienceMatch) {
|
|
755
|
+
audience = coerceAudience(clean(audienceMatch[1]));
|
|
756
|
+
}
|
|
757
|
+
const statusMatch = /^status:\s*(.+)$/m.exec(inner);
|
|
758
|
+
if (statusMatch) {
|
|
759
|
+
status = coerceStatus(clean(statusMatch[1]));
|
|
742
760
|
}
|
|
743
761
|
body = raw.slice(fenceMatch[0].length);
|
|
744
762
|
} else {
|
|
745
763
|
body = raw;
|
|
746
764
|
}
|
|
747
|
-
return { title, tags: [], body: body.replace(/^\n+/, "") };
|
|
765
|
+
return { title, tags: [], audience, status, body: body.replace(/^\n+/, "") };
|
|
748
766
|
}
|
|
749
767
|
function parseArticle(raw) {
|
|
750
768
|
try {
|
|
@@ -752,13 +770,17 @@ function parseArticle(raw) {
|
|
|
752
770
|
const title = typeof data.title === "string" ? data.title : "";
|
|
753
771
|
const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
|
|
754
772
|
const created = typeof data.created === "string" ? data.created : void 0;
|
|
755
|
-
|
|
773
|
+
const audience = coerceAudience(data.audience);
|
|
774
|
+
const status = coerceStatus(data.status);
|
|
775
|
+
return { title, tags, created, audience, status, body: content.replace(/^\n+/, "") };
|
|
756
776
|
} catch {
|
|
757
777
|
return parseFallback(raw);
|
|
758
778
|
}
|
|
759
779
|
}
|
|
760
780
|
function serializeArticle(article) {
|
|
761
781
|
const front = { title: article.title, tags: article.tags };
|
|
782
|
+
if (article.audience === "private") front.audience = "private";
|
|
783
|
+
if (article.status === "draft") front.status = "draft";
|
|
762
784
|
return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
|
|
763
785
|
}
|
|
764
786
|
|
|
@@ -839,7 +861,17 @@ async function loadArticle(target) {
|
|
|
839
861
|
async function updateArticleBody(target, body) {
|
|
840
862
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
841
863
|
const parsed = parseArticle(raw);
|
|
842
|
-
await import_promises5.default.writeFile(
|
|
864
|
+
await import_promises5.default.writeFile(
|
|
865
|
+
filePath,
|
|
866
|
+
serializeArticle({
|
|
867
|
+
title: parsed.title,
|
|
868
|
+
tags: parsed.tags,
|
|
869
|
+
audience: parsed.audience,
|
|
870
|
+
status: parsed.status,
|
|
871
|
+
body
|
|
872
|
+
}),
|
|
873
|
+
"utf8"
|
|
874
|
+
);
|
|
843
875
|
void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
|
|
844
876
|
return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
|
|
845
877
|
}
|
|
@@ -847,7 +879,17 @@ async function mutateTags(target, fn) {
|
|
|
847
879
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
848
880
|
const parsed = parseArticle(raw);
|
|
849
881
|
const tags = fn(parsed.tags);
|
|
850
|
-
await import_promises5.default.writeFile(
|
|
882
|
+
await import_promises5.default.writeFile(
|
|
883
|
+
filePath,
|
|
884
|
+
serializeArticle({
|
|
885
|
+
title: parsed.title,
|
|
886
|
+
tags,
|
|
887
|
+
audience: parsed.audience,
|
|
888
|
+
status: parsed.status,
|
|
889
|
+
body: parsed.body
|
|
890
|
+
}),
|
|
891
|
+
"utf8"
|
|
892
|
+
);
|
|
851
893
|
void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
|
|
852
894
|
return { title: parsed.title, tags, path: filePath, collection };
|
|
853
895
|
}
|
|
@@ -1168,20 +1210,30 @@ function pickIndexFile(names) {
|
|
|
1168
1210
|
};
|
|
1169
1211
|
return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
|
|
1170
1212
|
}
|
|
1171
|
-
|
|
1213
|
+
var CHEAP_META_BYTES = 8192;
|
|
1214
|
+
async function cheapMeta(absPath) {
|
|
1172
1215
|
let handle;
|
|
1173
1216
|
try {
|
|
1174
1217
|
handle = await import_promises8.default.open(absPath, "r");
|
|
1175
|
-
const buf = Buffer.alloc(
|
|
1176
|
-
const { bytesRead } = await handle.read(buf, 0,
|
|
1218
|
+
const buf = Buffer.alloc(CHEAP_META_BYTES);
|
|
1219
|
+
const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
|
|
1177
1220
|
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
1178
|
-
const
|
|
1179
|
-
if (
|
|
1221
|
+
const fence = /^---[ \t]*\n([\s\S]*?)(?:\n---[ \t]*\n|$)/.exec(snippet);
|
|
1222
|
+
if (!fence) return {};
|
|
1223
|
+
const inner = fence[1];
|
|
1224
|
+
const clean = (s) => s.trim().replace(/^["']|["']$/g, "");
|
|
1225
|
+
const titleM = /^title:\s*(.+)$/m.exec(inner);
|
|
1226
|
+
const audienceM = /^audience:\s*(.+)$/m.exec(inner);
|
|
1227
|
+
const statusM = /^status:\s*(.+)$/m.exec(inner);
|
|
1228
|
+
const title = titleM ? clean(titleM[1]) : void 0;
|
|
1229
|
+
const audience = audienceM && clean(audienceM[1]) === "private" ? "private" : void 0;
|
|
1230
|
+
const status = statusM && clean(statusM[1]) === "draft" ? "draft" : void 0;
|
|
1231
|
+
return { title, audience, status };
|
|
1180
1232
|
} catch {
|
|
1181
1233
|
} finally {
|
|
1182
1234
|
await handle?.close();
|
|
1183
1235
|
}
|
|
1184
|
-
return
|
|
1236
|
+
return {};
|
|
1185
1237
|
}
|
|
1186
1238
|
async function enrichTitles(nodes, collDir) {
|
|
1187
1239
|
return Promise.all(
|
|
@@ -1189,21 +1241,41 @@ async function enrichTitles(nodes, collDir) {
|
|
|
1189
1241
|
if (node.children) {
|
|
1190
1242
|
const children = await enrichTitles(node.children, collDir);
|
|
1191
1243
|
let title = node.title;
|
|
1244
|
+
let audience = node.audience;
|
|
1245
|
+
let status = node.status;
|
|
1192
1246
|
if (node.slug) {
|
|
1193
1247
|
const dir = import_node_path6.default.join(collDir, node.slug);
|
|
1194
1248
|
const idx = pickIndexFile(await readdirSafe(dir));
|
|
1195
|
-
if (idx)
|
|
1249
|
+
if (idx) {
|
|
1250
|
+
const meta = await cheapMeta(import_node_path6.default.join(dir, idx));
|
|
1251
|
+
title = meta.title ?? title;
|
|
1252
|
+
audience = meta.audience ?? audience;
|
|
1253
|
+
status = meta.status ?? status;
|
|
1254
|
+
}
|
|
1196
1255
|
}
|
|
1197
|
-
return { ...node, title, children };
|
|
1256
|
+
return { ...node, title, audience, status, children };
|
|
1198
1257
|
}
|
|
1199
1258
|
for (const ext of [".mdx", ".md"]) {
|
|
1200
|
-
const
|
|
1201
|
-
if (title
|
|
1259
|
+
const meta = await cheapMeta(import_node_path6.default.join(collDir, `${node.slug}${ext}`));
|
|
1260
|
+
if (meta.title || meta.audience || meta.status) {
|
|
1261
|
+
return {
|
|
1262
|
+
...node,
|
|
1263
|
+
title: meta.title ?? node.title,
|
|
1264
|
+
audience: meta.audience ?? node.audience,
|
|
1265
|
+
status: meta.status ?? node.status
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1202
1268
|
}
|
|
1203
1269
|
return node;
|
|
1204
1270
|
})
|
|
1205
1271
|
);
|
|
1206
1272
|
}
|
|
1273
|
+
async function collectionMeta(collDir) {
|
|
1274
|
+
const idx = pickIndexFile(await readdirSafe(collDir));
|
|
1275
|
+
if (!idx) return {};
|
|
1276
|
+
const meta = await cheapMeta(import_node_path6.default.join(collDir, idx));
|
|
1277
|
+
return { audience: meta.audience, status: meta.status };
|
|
1278
|
+
}
|
|
1207
1279
|
async function listArticleTree(coll) {
|
|
1208
1280
|
const entry = await findCollection(coll);
|
|
1209
1281
|
if (!entry) throw new CollectionNotFoundError(coll);
|
|
@@ -1245,7 +1317,17 @@ async function readArticle(coll, slug) {
|
|
|
1245
1317
|
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
1246
1318
|
const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
|
|
1247
1319
|
const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
|
|
1248
|
-
return {
|
|
1320
|
+
return {
|
|
1321
|
+
coll,
|
|
1322
|
+
slug: safeSlug,
|
|
1323
|
+
title,
|
|
1324
|
+
tags: parsed.tags,
|
|
1325
|
+
audience: parsed.audience,
|
|
1326
|
+
status: parsed.status,
|
|
1327
|
+
body: parsed.body,
|
|
1328
|
+
path: abs,
|
|
1329
|
+
isIndex
|
|
1330
|
+
};
|
|
1249
1331
|
}
|
|
1250
1332
|
|
|
1251
1333
|
// src/render.ts
|
|
@@ -1278,10 +1360,15 @@ function rehypeRewriteLinks(ctx) {
|
|
|
1278
1360
|
if (pathPart === "") return;
|
|
1279
1361
|
const joined = import_node_path7.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
|
|
1280
1362
|
const normalized = import_node_path7.default.posix.normalize(joined);
|
|
1363
|
+
const toRoute = (p) => p.replace(/\.(mdx?)$/i, "").replace(/(^|\/)(index|readme)$/i, "$1").replace(/\/+$/, "").replace(/^\/+/, "");
|
|
1364
|
+
if (normalized.startsWith("../")) {
|
|
1365
|
+
const sibling = normalized.slice(3);
|
|
1366
|
+
if (sibling === "" || sibling.startsWith("..")) return;
|
|
1367
|
+
node.properties = { ...node.properties, href: `/${toRoute(sibling)}${suffix}` };
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1281
1370
|
if (normalized.startsWith("..")) return;
|
|
1282
|
-
|
|
1283
|
-
resolved = resolved.replace(/(^|\/)(index|readme)$/i, "$1");
|
|
1284
|
-
resolved = resolved.replace(/\/+$/, "").replace(/^\/+/, "");
|
|
1371
|
+
const resolved = toRoute(normalized);
|
|
1285
1372
|
node.properties = {
|
|
1286
1373
|
...node.properties,
|
|
1287
1374
|
href: `/${ctx.coll}${resolved ? `/${resolved}` : ""}${suffix}`
|
|
@@ -1496,13 +1583,22 @@ function localGraph(graph, focusId, include, siblingCap = 12) {
|
|
|
1496
1583
|
init_errors();
|
|
1497
1584
|
var isStatic = () => process.env.DIFFWIKI_STATIC === "1";
|
|
1498
1585
|
function toTreeNodes(coll, nodes) {
|
|
1499
|
-
|
|
1586
|
+
const visible = isStatic() ? nodes.filter((n) => n.audience !== "private" && n.status !== "draft") : nodes;
|
|
1587
|
+
return visible.map(
|
|
1500
1588
|
(n) => n.children ? {
|
|
1501
1589
|
name: n.name,
|
|
1502
1590
|
title: n.title ?? n.name,
|
|
1503
1591
|
path: n.slug ? `/${coll}/${n.slug}` : void 0,
|
|
1592
|
+
...n.audience ? { audience: n.audience } : {},
|
|
1593
|
+
...n.status ? { status: n.status } : {},
|
|
1504
1594
|
children: toTreeNodes(coll, n.children)
|
|
1505
|
-
} : {
|
|
1595
|
+
} : {
|
|
1596
|
+
name: n.name,
|
|
1597
|
+
title: n.title ?? n.name,
|
|
1598
|
+
path: `/${coll}/${n.slug}`,
|
|
1599
|
+
...n.audience ? { audience: n.audience } : {},
|
|
1600
|
+
...n.status ? { status: n.status } : {}
|
|
1601
|
+
}
|
|
1506
1602
|
);
|
|
1507
1603
|
}
|
|
1508
1604
|
async function gitInfo(e) {
|
|
@@ -1515,23 +1611,40 @@ async function gitInfo(e) {
|
|
|
1515
1611
|
}
|
|
1516
1612
|
async function buildCollections() {
|
|
1517
1613
|
const entries = await listCollections();
|
|
1518
|
-
|
|
1519
|
-
entries.map(async (e) =>
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1614
|
+
const infos = await Promise.all(
|
|
1615
|
+
entries.map(async (e) => {
|
|
1616
|
+
const { audience, status } = await collectionMeta(e.path);
|
|
1617
|
+
return {
|
|
1618
|
+
name: e.name,
|
|
1619
|
+
type: e.type,
|
|
1620
|
+
path: e.path,
|
|
1621
|
+
...await gitInfo(e),
|
|
1622
|
+
...audience ? { audience } : {},
|
|
1623
|
+
...status ? { status } : {}
|
|
1624
|
+
};
|
|
1625
|
+
})
|
|
1525
1626
|
);
|
|
1627
|
+
return isStatic() ? infos.filter((c) => c.audience !== "private" && c.status !== "draft") : infos;
|
|
1526
1628
|
}
|
|
1527
1629
|
async function buildNavTree() {
|
|
1528
1630
|
const entries = await listCollections();
|
|
1529
|
-
|
|
1530
|
-
entries.map(async (e) =>
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1631
|
+
const trees = await Promise.all(
|
|
1632
|
+
entries.map(async (e) => {
|
|
1633
|
+
const { audience, status } = await collectionMeta(e.path);
|
|
1634
|
+
return {
|
|
1635
|
+
collection: {
|
|
1636
|
+
name: e.name,
|
|
1637
|
+
type: e.type,
|
|
1638
|
+
path: e.path,
|
|
1639
|
+
...await gitInfo(e),
|
|
1640
|
+
...audience ? { audience } : {},
|
|
1641
|
+
...status ? { status } : {}
|
|
1642
|
+
},
|
|
1643
|
+
nodes: toTreeNodes(e.name, await listArticleTree(e.name))
|
|
1644
|
+
};
|
|
1645
|
+
})
|
|
1534
1646
|
);
|
|
1647
|
+
return isStatic() ? trees.filter((t) => t.collection.audience !== "private" && t.collection.status !== "draft") : trees;
|
|
1535
1648
|
}
|
|
1536
1649
|
function findNode(nodes, slug) {
|
|
1537
1650
|
for (const n of nodes) {
|
|
@@ -1559,6 +1672,8 @@ async function resolvePage(coll, slug, opts) {
|
|
|
1559
1672
|
slug: art.slug,
|
|
1560
1673
|
title: art.title,
|
|
1561
1674
|
tags: art.tags,
|
|
1675
|
+
...art.audience === "private" ? { audience: art.audience } : {},
|
|
1676
|
+
...art.status === "draft" ? { status: art.status } : {},
|
|
1562
1677
|
html,
|
|
1563
1678
|
toc,
|
|
1564
1679
|
path: `/${art.coll}/${art.slug}`,
|
|
@@ -1808,6 +1923,7 @@ async function query(term, opts) {
|
|
|
1808
1923
|
var import_promises10 = __toESM(require("fs/promises"), 1);
|
|
1809
1924
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
1810
1925
|
init_collections();
|
|
1926
|
+
var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
|
|
1811
1927
|
async function readArticleParts(collDir, slug) {
|
|
1812
1928
|
for (const ext of [".mdx", ".md"]) {
|
|
1813
1929
|
try {
|
|
@@ -1820,6 +1936,7 @@ async function readArticleParts(collDir, slug) {
|
|
|
1820
1936
|
}
|
|
1821
1937
|
async function flatten(coll, collDir, nodes, out) {
|
|
1822
1938
|
for (const n of nodes) {
|
|
1939
|
+
if (isStatic2() && (n.audience === "private" || n.status === "draft")) continue;
|
|
1823
1940
|
if (n.slug && !n.children) {
|
|
1824
1941
|
const title = n.title ?? n.name;
|
|
1825
1942
|
const { tags, body } = await readArticleParts(collDir, n.slug);
|
|
@@ -1846,6 +1963,10 @@ async function buildSearchIndex(collection) {
|
|
|
1846
1963
|
try {
|
|
1847
1964
|
const collEntry = await findCollection(e.name);
|
|
1848
1965
|
const collDir = import_node_path9.default.resolve(collEntry?.path ?? e.path);
|
|
1966
|
+
if (isStatic2()) {
|
|
1967
|
+
const cm = await collectionMeta(collDir);
|
|
1968
|
+
if (cm.audience === "private" || cm.status === "draft") continue;
|
|
1969
|
+
}
|
|
1849
1970
|
await flatten(e.name, collDir, await listArticleTree(e.name), out);
|
|
1850
1971
|
} catch {
|
|
1851
1972
|
}
|
|
@@ -2256,6 +2377,7 @@ init_registry2();
|
|
|
2256
2377
|
buildSearchIndex,
|
|
2257
2378
|
collectionDir,
|
|
2258
2379
|
collectionGitStatus,
|
|
2380
|
+
collectionMeta,
|
|
2259
2381
|
collectionsDir,
|
|
2260
2382
|
configPath,
|
|
2261
2383
|
createArticle,
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,10 @@ import { SearchDoc } from './bm25.cjs';
|
|
|
2
2
|
export { RankableFields, rankDocs, tokenize } from './bm25.cjs';
|
|
3
3
|
import { LogLevel, Logger } from '@logtape/logtape';
|
|
4
4
|
|
|
5
|
+
/** Audience axis — who an article is for. Private is never published. Missing/invalid → 'public'. */
|
|
6
|
+
type Audience = 'public' | 'private';
|
|
7
|
+
/** Lifecycle axis — readiness. Draft is not yet published. Missing/invalid → 'published'. */
|
|
8
|
+
type PublishStatus = 'draft' | 'published';
|
|
5
9
|
type CollectionType = 'global' | 'repo' | 'external';
|
|
6
10
|
interface CollectionEntry {
|
|
7
11
|
name: string;
|
|
@@ -47,6 +51,10 @@ interface ArticleNode {
|
|
|
47
51
|
* leaves (the article) and on branches (the folder, navigable to its index).
|
|
48
52
|
*/
|
|
49
53
|
slug?: string;
|
|
54
|
+
/** Audience, set only when 'private' (omitted for 'public'). */
|
|
55
|
+
audience?: Audience;
|
|
56
|
+
/** Lifecycle status, set only when 'draft' (omitted for 'published'). */
|
|
57
|
+
status?: PublishStatus;
|
|
50
58
|
/** Branch only: nested nodes. */
|
|
51
59
|
children?: ArticleNode[];
|
|
52
60
|
}
|
|
@@ -56,6 +64,10 @@ interface ArticleContent {
|
|
|
56
64
|
slug: string;
|
|
57
65
|
title: string;
|
|
58
66
|
tags: string[];
|
|
67
|
+
/** Audience from frontmatter ('public' when absent). */
|
|
68
|
+
audience: Audience;
|
|
69
|
+
/** Lifecycle status from frontmatter ('published' when absent). */
|
|
70
|
+
status: PublishStatus;
|
|
59
71
|
/** Frontmatter-stripped body (md/mdx). */
|
|
60
72
|
body: string;
|
|
61
73
|
/** Absolute path to the file on disk. */
|
|
@@ -160,6 +172,10 @@ interface TreeNode {
|
|
|
160
172
|
name: string;
|
|
161
173
|
title?: string;
|
|
162
174
|
path?: string;
|
|
175
|
+
/** Audience; present only when 'private' (drives the sidebar lock). */
|
|
176
|
+
audience?: Audience;
|
|
177
|
+
/** Lifecycle status; present only when 'draft' (drives the sidebar DRAFT tag). */
|
|
178
|
+
status?: PublishStatus;
|
|
163
179
|
children?: TreeNode[];
|
|
164
180
|
/** Set only on top-level collection rows: git branch/behind badge data. */
|
|
165
181
|
meta?: NodeMeta;
|
|
@@ -170,6 +186,10 @@ interface CollectionInfo {
|
|
|
170
186
|
path: string;
|
|
171
187
|
branch?: string;
|
|
172
188
|
behind?: number;
|
|
189
|
+
/** Audience from the collection's root index page; present only when 'private'. */
|
|
190
|
+
audience?: Audience;
|
|
191
|
+
/** Lifecycle status from the collection's root index page; present only when 'draft'. */
|
|
192
|
+
status?: PublishStatus;
|
|
173
193
|
}
|
|
174
194
|
interface CollectionTree {
|
|
175
195
|
collection: CollectionInfo;
|
|
@@ -185,6 +205,10 @@ interface DocData {
|
|
|
185
205
|
slug: string;
|
|
186
206
|
title: string;
|
|
187
207
|
tags: string[];
|
|
208
|
+
/** Audience; present only when 'private'. */
|
|
209
|
+
audience?: Audience;
|
|
210
|
+
/** Lifecycle status; present only when 'draft'. */
|
|
211
|
+
status?: PublishStatus;
|
|
188
212
|
html: string;
|
|
189
213
|
toc: TocItem[];
|
|
190
214
|
path: string;
|
|
@@ -307,11 +331,15 @@ interface ParsedArticle {
|
|
|
307
331
|
title: string;
|
|
308
332
|
tags: string[];
|
|
309
333
|
created?: string;
|
|
334
|
+
audience: Audience;
|
|
335
|
+
status: PublishStatus;
|
|
310
336
|
body: string;
|
|
311
337
|
}
|
|
312
338
|
interface SerializableArticle {
|
|
313
339
|
title: string;
|
|
314
340
|
tags: string[];
|
|
341
|
+
audience?: Audience;
|
|
342
|
+
status?: PublishStatus;
|
|
315
343
|
body: string;
|
|
316
344
|
}
|
|
317
345
|
declare function parseArticle(raw: string): ParsedArticle;
|
|
@@ -436,7 +464,10 @@ interface FileTimestamps {
|
|
|
436
464
|
*/
|
|
437
465
|
declare function fileTimestamps(absPath: string): Promise<FileTimestamps>;
|
|
438
466
|
|
|
439
|
-
/** Map core article nodes to UI nav nodes, assigning each its route path.
|
|
467
|
+
/** Map core article nodes to UI nav nodes, assigning each its route path.
|
|
468
|
+
* In static builds, private (audience) or draft (status) articles are dropped
|
|
469
|
+
* entirely (no nav row, and — since routes + per-doc JSON derive from this tree
|
|
470
|
+
* — no route/JSON). */
|
|
440
471
|
declare function toTreeNodes(coll: string, nodes: ArticleNode[]): TreeNode[];
|
|
441
472
|
declare function buildCollections(): Promise<CollectionInfo[]>;
|
|
442
473
|
declare function buildNavTree(): Promise<CollectionTree[]>;
|
|
@@ -640,6 +671,16 @@ declare function resolveLogLevel(env?: NodeJS.ProcessEnv): DiffwikiLogLevel;
|
|
|
640
671
|
* is the relative path without extension. Dirs-first, then alpha (case-insensitive).
|
|
641
672
|
*/
|
|
642
673
|
declare function buildArticleTree(relPaths: string[]): ArticleNode[];
|
|
674
|
+
/**
|
|
675
|
+
* Read a collection's (or any directory's) audience + status from its index page
|
|
676
|
+
* (index.md / README). Each axis is present only at its non-default value
|
|
677
|
+
* (audience:'private' / status:'draft'), else absent. Cheap partial read — used
|
|
678
|
+
* to mark/skip whole collections the same way leaf articles are.
|
|
679
|
+
*/
|
|
680
|
+
declare function collectionMeta(collDir: string): Promise<{
|
|
681
|
+
audience?: Audience;
|
|
682
|
+
status?: PublishStatus;
|
|
683
|
+
}>;
|
|
643
684
|
/**
|
|
644
685
|
* List the article tree of a collection. Walks the collection's registered
|
|
645
686
|
* `entry.path` — so collections living OUTSIDE `~/.diffwiki/collections`
|
|
@@ -890,4 +931,4 @@ declare function findEnabledPlugin(kind: PluginKind, name?: string): Promise<Plu
|
|
|
890
931
|
/** Find a plugin record by name (regardless of enabled state). */
|
|
891
932
|
declare function findPluginRecord(name: string): Promise<PluginRecord | undefined>;
|
|
892
933
|
|
|
893
|
-
export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type FileTimestamps, type GraphEdge, type GraphNode, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, type LinkGraph, type ListingEntry, type LocalGraph, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderContext, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildLinkGraph, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, invalidateLinkGraph, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, localGraph, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
|
|
934
|
+
export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, type Audience, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type FileTimestamps, type GraphEdge, type GraphNode, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, type LinkGraph, type ListingEntry, type LocalGraph, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type PublishStatus, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderContext, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildLinkGraph, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionMeta, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, invalidateLinkGraph, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, localGraph, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { SearchDoc } from './bm25.js';
|
|
|
2
2
|
export { RankableFields, rankDocs, tokenize } from './bm25.js';
|
|
3
3
|
import { LogLevel, Logger } from '@logtape/logtape';
|
|
4
4
|
|
|
5
|
+
/** Audience axis — who an article is for. Private is never published. Missing/invalid → 'public'. */
|
|
6
|
+
type Audience = 'public' | 'private';
|
|
7
|
+
/** Lifecycle axis — readiness. Draft is not yet published. Missing/invalid → 'published'. */
|
|
8
|
+
type PublishStatus = 'draft' | 'published';
|
|
5
9
|
type CollectionType = 'global' | 'repo' | 'external';
|
|
6
10
|
interface CollectionEntry {
|
|
7
11
|
name: string;
|
|
@@ -47,6 +51,10 @@ interface ArticleNode {
|
|
|
47
51
|
* leaves (the article) and on branches (the folder, navigable to its index).
|
|
48
52
|
*/
|
|
49
53
|
slug?: string;
|
|
54
|
+
/** Audience, set only when 'private' (omitted for 'public'). */
|
|
55
|
+
audience?: Audience;
|
|
56
|
+
/** Lifecycle status, set only when 'draft' (omitted for 'published'). */
|
|
57
|
+
status?: PublishStatus;
|
|
50
58
|
/** Branch only: nested nodes. */
|
|
51
59
|
children?: ArticleNode[];
|
|
52
60
|
}
|
|
@@ -56,6 +64,10 @@ interface ArticleContent {
|
|
|
56
64
|
slug: string;
|
|
57
65
|
title: string;
|
|
58
66
|
tags: string[];
|
|
67
|
+
/** Audience from frontmatter ('public' when absent). */
|
|
68
|
+
audience: Audience;
|
|
69
|
+
/** Lifecycle status from frontmatter ('published' when absent). */
|
|
70
|
+
status: PublishStatus;
|
|
59
71
|
/** Frontmatter-stripped body (md/mdx). */
|
|
60
72
|
body: string;
|
|
61
73
|
/** Absolute path to the file on disk. */
|
|
@@ -160,6 +172,10 @@ interface TreeNode {
|
|
|
160
172
|
name: string;
|
|
161
173
|
title?: string;
|
|
162
174
|
path?: string;
|
|
175
|
+
/** Audience; present only when 'private' (drives the sidebar lock). */
|
|
176
|
+
audience?: Audience;
|
|
177
|
+
/** Lifecycle status; present only when 'draft' (drives the sidebar DRAFT tag). */
|
|
178
|
+
status?: PublishStatus;
|
|
163
179
|
children?: TreeNode[];
|
|
164
180
|
/** Set only on top-level collection rows: git branch/behind badge data. */
|
|
165
181
|
meta?: NodeMeta;
|
|
@@ -170,6 +186,10 @@ interface CollectionInfo {
|
|
|
170
186
|
path: string;
|
|
171
187
|
branch?: string;
|
|
172
188
|
behind?: number;
|
|
189
|
+
/** Audience from the collection's root index page; present only when 'private'. */
|
|
190
|
+
audience?: Audience;
|
|
191
|
+
/** Lifecycle status from the collection's root index page; present only when 'draft'. */
|
|
192
|
+
status?: PublishStatus;
|
|
173
193
|
}
|
|
174
194
|
interface CollectionTree {
|
|
175
195
|
collection: CollectionInfo;
|
|
@@ -185,6 +205,10 @@ interface DocData {
|
|
|
185
205
|
slug: string;
|
|
186
206
|
title: string;
|
|
187
207
|
tags: string[];
|
|
208
|
+
/** Audience; present only when 'private'. */
|
|
209
|
+
audience?: Audience;
|
|
210
|
+
/** Lifecycle status; present only when 'draft'. */
|
|
211
|
+
status?: PublishStatus;
|
|
188
212
|
html: string;
|
|
189
213
|
toc: TocItem[];
|
|
190
214
|
path: string;
|
|
@@ -307,11 +331,15 @@ interface ParsedArticle {
|
|
|
307
331
|
title: string;
|
|
308
332
|
tags: string[];
|
|
309
333
|
created?: string;
|
|
334
|
+
audience: Audience;
|
|
335
|
+
status: PublishStatus;
|
|
310
336
|
body: string;
|
|
311
337
|
}
|
|
312
338
|
interface SerializableArticle {
|
|
313
339
|
title: string;
|
|
314
340
|
tags: string[];
|
|
341
|
+
audience?: Audience;
|
|
342
|
+
status?: PublishStatus;
|
|
315
343
|
body: string;
|
|
316
344
|
}
|
|
317
345
|
declare function parseArticle(raw: string): ParsedArticle;
|
|
@@ -436,7 +464,10 @@ interface FileTimestamps {
|
|
|
436
464
|
*/
|
|
437
465
|
declare function fileTimestamps(absPath: string): Promise<FileTimestamps>;
|
|
438
466
|
|
|
439
|
-
/** Map core article nodes to UI nav nodes, assigning each its route path.
|
|
467
|
+
/** Map core article nodes to UI nav nodes, assigning each its route path.
|
|
468
|
+
* In static builds, private (audience) or draft (status) articles are dropped
|
|
469
|
+
* entirely (no nav row, and — since routes + per-doc JSON derive from this tree
|
|
470
|
+
* — no route/JSON). */
|
|
440
471
|
declare function toTreeNodes(coll: string, nodes: ArticleNode[]): TreeNode[];
|
|
441
472
|
declare function buildCollections(): Promise<CollectionInfo[]>;
|
|
442
473
|
declare function buildNavTree(): Promise<CollectionTree[]>;
|
|
@@ -640,6 +671,16 @@ declare function resolveLogLevel(env?: NodeJS.ProcessEnv): DiffwikiLogLevel;
|
|
|
640
671
|
* is the relative path without extension. Dirs-first, then alpha (case-insensitive).
|
|
641
672
|
*/
|
|
642
673
|
declare function buildArticleTree(relPaths: string[]): ArticleNode[];
|
|
674
|
+
/**
|
|
675
|
+
* Read a collection's (or any directory's) audience + status from its index page
|
|
676
|
+
* (index.md / README). Each axis is present only at its non-default value
|
|
677
|
+
* (audience:'private' / status:'draft'), else absent. Cheap partial read — used
|
|
678
|
+
* to mark/skip whole collections the same way leaf articles are.
|
|
679
|
+
*/
|
|
680
|
+
declare function collectionMeta(collDir: string): Promise<{
|
|
681
|
+
audience?: Audience;
|
|
682
|
+
status?: PublishStatus;
|
|
683
|
+
}>;
|
|
643
684
|
/**
|
|
644
685
|
* List the article tree of a collection. Walks the collection's registered
|
|
645
686
|
* `entry.path` — so collections living OUTSIDE `~/.diffwiki/collections`
|
|
@@ -890,4 +931,4 @@ declare function findEnabledPlugin(kind: PluginKind, name?: string): Promise<Plu
|
|
|
890
931
|
/** Find a plugin record by name (regardless of enabled state). */
|
|
891
932
|
declare function findPluginRecord(name: string): Promise<PluginRecord | undefined>;
|
|
892
933
|
|
|
893
|
-
export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type FileTimestamps, type GraphEdge, type GraphNode, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, type LinkGraph, type ListingEntry, type LocalGraph, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderContext, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildLinkGraph, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, invalidateLinkGraph, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, localGraph, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
|
|
934
|
+
export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, type Audience, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type FileTimestamps, type GraphEdge, type GraphNode, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, type LinkGraph, type ListingEntry, type LocalGraph, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type PublishStatus, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderContext, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildLinkGraph, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionMeta, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, invalidateLinkGraph, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, localGraph, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
|
package/dist/index.js
CHANGED
|
@@ -109,22 +109,39 @@ var YAML_ENGINE = {
|
|
|
109
109
|
stringify: (data) => stringifyYaml(data)
|
|
110
110
|
};
|
|
111
111
|
var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
|
|
112
|
+
function coerceAudience(value) {
|
|
113
|
+
return value === "private" ? "private" : "public";
|
|
114
|
+
}
|
|
115
|
+
function coerceStatus(value) {
|
|
116
|
+
return value === "draft" ? "draft" : "published";
|
|
117
|
+
}
|
|
112
118
|
function parseFallback(raw) {
|
|
113
119
|
const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
|
|
114
120
|
const fenceMatch = fenceRe.exec(raw);
|
|
115
121
|
let title = "";
|
|
122
|
+
let audience = "public";
|
|
123
|
+
let status = "published";
|
|
116
124
|
let body;
|
|
125
|
+
const clean = (s) => s.trim().replace(/^["']|["']$/g, "");
|
|
117
126
|
if (fenceMatch) {
|
|
118
127
|
const inner = fenceMatch[1];
|
|
119
128
|
const titleMatch = /^title:\s*(.+)$/m.exec(inner);
|
|
120
129
|
if (titleMatch) {
|
|
121
|
-
title = titleMatch[1]
|
|
130
|
+
title = clean(titleMatch[1]);
|
|
131
|
+
}
|
|
132
|
+
const audienceMatch = /^audience:\s*(.+)$/m.exec(inner);
|
|
133
|
+
if (audienceMatch) {
|
|
134
|
+
audience = coerceAudience(clean(audienceMatch[1]));
|
|
135
|
+
}
|
|
136
|
+
const statusMatch = /^status:\s*(.+)$/m.exec(inner);
|
|
137
|
+
if (statusMatch) {
|
|
138
|
+
status = coerceStatus(clean(statusMatch[1]));
|
|
122
139
|
}
|
|
123
140
|
body = raw.slice(fenceMatch[0].length);
|
|
124
141
|
} else {
|
|
125
142
|
body = raw;
|
|
126
143
|
}
|
|
127
|
-
return { title, tags: [], body: body.replace(/^\n+/, "") };
|
|
144
|
+
return { title, tags: [], audience, status, body: body.replace(/^\n+/, "") };
|
|
128
145
|
}
|
|
129
146
|
function parseArticle(raw) {
|
|
130
147
|
try {
|
|
@@ -132,13 +149,17 @@ function parseArticle(raw) {
|
|
|
132
149
|
const title = typeof data.title === "string" ? data.title : "";
|
|
133
150
|
const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
|
|
134
151
|
const created = typeof data.created === "string" ? data.created : void 0;
|
|
135
|
-
|
|
152
|
+
const audience = coerceAudience(data.audience);
|
|
153
|
+
const status = coerceStatus(data.status);
|
|
154
|
+
return { title, tags, created, audience, status, body: content.replace(/^\n+/, "") };
|
|
136
155
|
} catch {
|
|
137
156
|
return parseFallback(raw);
|
|
138
157
|
}
|
|
139
158
|
}
|
|
140
159
|
function serializeArticle(article) {
|
|
141
160
|
const front = { title: article.title, tags: article.tags };
|
|
161
|
+
if (article.audience === "private") front.audience = "private";
|
|
162
|
+
if (article.status === "draft") front.status = "draft";
|
|
142
163
|
return matter.stringify(article.body, front, MATTER_OPTS);
|
|
143
164
|
}
|
|
144
165
|
|
|
@@ -214,7 +235,17 @@ async function loadArticle(target) {
|
|
|
214
235
|
async function updateArticleBody(target, body) {
|
|
215
236
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
216
237
|
const parsed = parseArticle(raw);
|
|
217
|
-
await fs2.writeFile(
|
|
238
|
+
await fs2.writeFile(
|
|
239
|
+
filePath,
|
|
240
|
+
serializeArticle({
|
|
241
|
+
title: parsed.title,
|
|
242
|
+
tags: parsed.tags,
|
|
243
|
+
audience: parsed.audience,
|
|
244
|
+
status: parsed.status,
|
|
245
|
+
body
|
|
246
|
+
}),
|
|
247
|
+
"utf8"
|
|
248
|
+
);
|
|
218
249
|
void fireHook("article-updated", collection, path.relative(dir, filePath));
|
|
219
250
|
return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
|
|
220
251
|
}
|
|
@@ -222,7 +253,17 @@ async function mutateTags(target, fn) {
|
|
|
222
253
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
223
254
|
const parsed = parseArticle(raw);
|
|
224
255
|
const tags = fn(parsed.tags);
|
|
225
|
-
await fs2.writeFile(
|
|
256
|
+
await fs2.writeFile(
|
|
257
|
+
filePath,
|
|
258
|
+
serializeArticle({
|
|
259
|
+
title: parsed.title,
|
|
260
|
+
tags,
|
|
261
|
+
audience: parsed.audience,
|
|
262
|
+
status: parsed.status,
|
|
263
|
+
body: parsed.body
|
|
264
|
+
}),
|
|
265
|
+
"utf8"
|
|
266
|
+
);
|
|
226
267
|
void fireHook("article-updated", collection, path.relative(dir, filePath));
|
|
227
268
|
return { title: parsed.title, tags, path: filePath, collection };
|
|
228
269
|
}
|
|
@@ -534,20 +575,30 @@ function pickIndexFile(names) {
|
|
|
534
575
|
};
|
|
535
576
|
return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
|
|
536
577
|
}
|
|
537
|
-
|
|
578
|
+
var CHEAP_META_BYTES = 8192;
|
|
579
|
+
async function cheapMeta(absPath) {
|
|
538
580
|
let handle;
|
|
539
581
|
try {
|
|
540
582
|
handle = await fs5.open(absPath, "r");
|
|
541
|
-
const buf = Buffer.alloc(
|
|
542
|
-
const { bytesRead } = await handle.read(buf, 0,
|
|
583
|
+
const buf = Buffer.alloc(CHEAP_META_BYTES);
|
|
584
|
+
const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
|
|
543
585
|
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
586
|
+
const fence = /^---[ \t]*\n([\s\S]*?)(?:\n---[ \t]*\n|$)/.exec(snippet);
|
|
587
|
+
if (!fence) return {};
|
|
588
|
+
const inner = fence[1];
|
|
589
|
+
const clean = (s) => s.trim().replace(/^["']|["']$/g, "");
|
|
590
|
+
const titleM = /^title:\s*(.+)$/m.exec(inner);
|
|
591
|
+
const audienceM = /^audience:\s*(.+)$/m.exec(inner);
|
|
592
|
+
const statusM = /^status:\s*(.+)$/m.exec(inner);
|
|
593
|
+
const title = titleM ? clean(titleM[1]) : void 0;
|
|
594
|
+
const audience = audienceM && clean(audienceM[1]) === "private" ? "private" : void 0;
|
|
595
|
+
const status = statusM && clean(statusM[1]) === "draft" ? "draft" : void 0;
|
|
596
|
+
return { title, audience, status };
|
|
546
597
|
} catch {
|
|
547
598
|
} finally {
|
|
548
599
|
await handle?.close();
|
|
549
600
|
}
|
|
550
|
-
return
|
|
601
|
+
return {};
|
|
551
602
|
}
|
|
552
603
|
async function enrichTitles(nodes, collDir) {
|
|
553
604
|
return Promise.all(
|
|
@@ -555,21 +606,41 @@ async function enrichTitles(nodes, collDir) {
|
|
|
555
606
|
if (node.children) {
|
|
556
607
|
const children = await enrichTitles(node.children, collDir);
|
|
557
608
|
let title = node.title;
|
|
609
|
+
let audience = node.audience;
|
|
610
|
+
let status = node.status;
|
|
558
611
|
if (node.slug) {
|
|
559
612
|
const dir = path4.join(collDir, node.slug);
|
|
560
613
|
const idx = pickIndexFile(await readdirSafe(dir));
|
|
561
|
-
if (idx)
|
|
614
|
+
if (idx) {
|
|
615
|
+
const meta = await cheapMeta(path4.join(dir, idx));
|
|
616
|
+
title = meta.title ?? title;
|
|
617
|
+
audience = meta.audience ?? audience;
|
|
618
|
+
status = meta.status ?? status;
|
|
619
|
+
}
|
|
562
620
|
}
|
|
563
|
-
return { ...node, title, children };
|
|
621
|
+
return { ...node, title, audience, status, children };
|
|
564
622
|
}
|
|
565
623
|
for (const ext of [".mdx", ".md"]) {
|
|
566
|
-
const
|
|
567
|
-
if (title
|
|
624
|
+
const meta = await cheapMeta(path4.join(collDir, `${node.slug}${ext}`));
|
|
625
|
+
if (meta.title || meta.audience || meta.status) {
|
|
626
|
+
return {
|
|
627
|
+
...node,
|
|
628
|
+
title: meta.title ?? node.title,
|
|
629
|
+
audience: meta.audience ?? node.audience,
|
|
630
|
+
status: meta.status ?? node.status
|
|
631
|
+
};
|
|
632
|
+
}
|
|
568
633
|
}
|
|
569
634
|
return node;
|
|
570
635
|
})
|
|
571
636
|
);
|
|
572
637
|
}
|
|
638
|
+
async function collectionMeta(collDir) {
|
|
639
|
+
const idx = pickIndexFile(await readdirSafe(collDir));
|
|
640
|
+
if (!idx) return {};
|
|
641
|
+
const meta = await cheapMeta(path4.join(collDir, idx));
|
|
642
|
+
return { audience: meta.audience, status: meta.status };
|
|
643
|
+
}
|
|
573
644
|
async function listArticleTree(coll) {
|
|
574
645
|
const entry = await findCollection(coll);
|
|
575
646
|
if (!entry) throw new CollectionNotFoundError(coll);
|
|
@@ -611,7 +682,17 @@ async function readArticle(coll, slug) {
|
|
|
611
682
|
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
612
683
|
const parsed = parseArticle(await fs5.readFile(abs, "utf8"));
|
|
613
684
|
const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
|
|
614
|
-
return {
|
|
685
|
+
return {
|
|
686
|
+
coll,
|
|
687
|
+
slug: safeSlug,
|
|
688
|
+
title,
|
|
689
|
+
tags: parsed.tags,
|
|
690
|
+
audience: parsed.audience,
|
|
691
|
+
status: parsed.status,
|
|
692
|
+
body: parsed.body,
|
|
693
|
+
path: abs,
|
|
694
|
+
isIndex
|
|
695
|
+
};
|
|
615
696
|
}
|
|
616
697
|
|
|
617
698
|
// src/render.ts
|
|
@@ -644,10 +725,15 @@ function rehypeRewriteLinks(ctx) {
|
|
|
644
725
|
if (pathPart === "") return;
|
|
645
726
|
const joined = path5.posix.join(slugDir === "." ? "" : slugDir, pathPart);
|
|
646
727
|
const normalized = path5.posix.normalize(joined);
|
|
728
|
+
const toRoute = (p) => p.replace(/\.(mdx?)$/i, "").replace(/(^|\/)(index|readme)$/i, "$1").replace(/\/+$/, "").replace(/^\/+/, "");
|
|
729
|
+
if (normalized.startsWith("../")) {
|
|
730
|
+
const sibling = normalized.slice(3);
|
|
731
|
+
if (sibling === "" || sibling.startsWith("..")) return;
|
|
732
|
+
node.properties = { ...node.properties, href: `/${toRoute(sibling)}${suffix}` };
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
647
735
|
if (normalized.startsWith("..")) return;
|
|
648
|
-
|
|
649
|
-
resolved = resolved.replace(/(^|\/)(index|readme)$/i, "$1");
|
|
650
|
-
resolved = resolved.replace(/\/+$/, "").replace(/^\/+/, "");
|
|
736
|
+
const resolved = toRoute(normalized);
|
|
651
737
|
node.properties = {
|
|
652
738
|
...node.properties,
|
|
653
739
|
href: `/${ctx.coll}${resolved ? `/${resolved}` : ""}${suffix}`
|
|
@@ -859,13 +945,22 @@ function localGraph(graph, focusId, include, siblingCap = 12) {
|
|
|
859
945
|
// src/site.ts
|
|
860
946
|
var isStatic = () => process.env.DIFFWIKI_STATIC === "1";
|
|
861
947
|
function toTreeNodes(coll, nodes) {
|
|
862
|
-
|
|
948
|
+
const visible = isStatic() ? nodes.filter((n) => n.audience !== "private" && n.status !== "draft") : nodes;
|
|
949
|
+
return visible.map(
|
|
863
950
|
(n) => n.children ? {
|
|
864
951
|
name: n.name,
|
|
865
952
|
title: n.title ?? n.name,
|
|
866
953
|
path: n.slug ? `/${coll}/${n.slug}` : void 0,
|
|
954
|
+
...n.audience ? { audience: n.audience } : {},
|
|
955
|
+
...n.status ? { status: n.status } : {},
|
|
867
956
|
children: toTreeNodes(coll, n.children)
|
|
868
|
-
} : {
|
|
957
|
+
} : {
|
|
958
|
+
name: n.name,
|
|
959
|
+
title: n.title ?? n.name,
|
|
960
|
+
path: `/${coll}/${n.slug}`,
|
|
961
|
+
...n.audience ? { audience: n.audience } : {},
|
|
962
|
+
...n.status ? { status: n.status } : {}
|
|
963
|
+
}
|
|
869
964
|
);
|
|
870
965
|
}
|
|
871
966
|
async function gitInfo(e) {
|
|
@@ -878,23 +973,40 @@ async function gitInfo(e) {
|
|
|
878
973
|
}
|
|
879
974
|
async function buildCollections() {
|
|
880
975
|
const entries = await listCollections();
|
|
881
|
-
|
|
882
|
-
entries.map(async (e) =>
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
976
|
+
const infos = await Promise.all(
|
|
977
|
+
entries.map(async (e) => {
|
|
978
|
+
const { audience, status } = await collectionMeta(e.path);
|
|
979
|
+
return {
|
|
980
|
+
name: e.name,
|
|
981
|
+
type: e.type,
|
|
982
|
+
path: e.path,
|
|
983
|
+
...await gitInfo(e),
|
|
984
|
+
...audience ? { audience } : {},
|
|
985
|
+
...status ? { status } : {}
|
|
986
|
+
};
|
|
987
|
+
})
|
|
888
988
|
);
|
|
989
|
+
return isStatic() ? infos.filter((c) => c.audience !== "private" && c.status !== "draft") : infos;
|
|
889
990
|
}
|
|
890
991
|
async function buildNavTree() {
|
|
891
992
|
const entries = await listCollections();
|
|
892
|
-
|
|
893
|
-
entries.map(async (e) =>
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
993
|
+
const trees = await Promise.all(
|
|
994
|
+
entries.map(async (e) => {
|
|
995
|
+
const { audience, status } = await collectionMeta(e.path);
|
|
996
|
+
return {
|
|
997
|
+
collection: {
|
|
998
|
+
name: e.name,
|
|
999
|
+
type: e.type,
|
|
1000
|
+
path: e.path,
|
|
1001
|
+
...await gitInfo(e),
|
|
1002
|
+
...audience ? { audience } : {},
|
|
1003
|
+
...status ? { status } : {}
|
|
1004
|
+
},
|
|
1005
|
+
nodes: toTreeNodes(e.name, await listArticleTree(e.name))
|
|
1006
|
+
};
|
|
1007
|
+
})
|
|
897
1008
|
);
|
|
1009
|
+
return isStatic() ? trees.filter((t) => t.collection.audience !== "private" && t.collection.status !== "draft") : trees;
|
|
898
1010
|
}
|
|
899
1011
|
function findNode(nodes, slug) {
|
|
900
1012
|
for (const n of nodes) {
|
|
@@ -922,6 +1034,8 @@ async function resolvePage(coll, slug, opts) {
|
|
|
922
1034
|
slug: art.slug,
|
|
923
1035
|
title: art.title,
|
|
924
1036
|
tags: art.tags,
|
|
1037
|
+
...art.audience === "private" ? { audience: art.audience } : {},
|
|
1038
|
+
...art.status === "draft" ? { status: art.status } : {},
|
|
925
1039
|
html,
|
|
926
1040
|
toc,
|
|
927
1041
|
path: `/${art.coll}/${art.slug}`,
|
|
@@ -1106,6 +1220,7 @@ async function query(term, opts) {
|
|
|
1106
1220
|
// src/search.ts
|
|
1107
1221
|
import fs7 from "fs/promises";
|
|
1108
1222
|
import path7 from "path";
|
|
1223
|
+
var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
|
|
1109
1224
|
async function readArticleParts(collDir, slug) {
|
|
1110
1225
|
for (const ext of [".mdx", ".md"]) {
|
|
1111
1226
|
try {
|
|
@@ -1118,6 +1233,7 @@ async function readArticleParts(collDir, slug) {
|
|
|
1118
1233
|
}
|
|
1119
1234
|
async function flatten(coll, collDir, nodes, out) {
|
|
1120
1235
|
for (const n of nodes) {
|
|
1236
|
+
if (isStatic2() && (n.audience === "private" || n.status === "draft")) continue;
|
|
1121
1237
|
if (n.slug && !n.children) {
|
|
1122
1238
|
const title = n.title ?? n.name;
|
|
1123
1239
|
const { tags, body } = await readArticleParts(collDir, n.slug);
|
|
@@ -1144,6 +1260,10 @@ async function buildSearchIndex(collection) {
|
|
|
1144
1260
|
try {
|
|
1145
1261
|
const collEntry = await findCollection(e.name);
|
|
1146
1262
|
const collDir = path7.resolve(collEntry?.path ?? e.path);
|
|
1263
|
+
if (isStatic2()) {
|
|
1264
|
+
const cm = await collectionMeta(collDir);
|
|
1265
|
+
if (cm.audience === "private" || cm.status === "draft") continue;
|
|
1266
|
+
}
|
|
1147
1267
|
await flatten(e.name, collDir, await listArticleTree(e.name), out);
|
|
1148
1268
|
} catch {
|
|
1149
1269
|
}
|
|
@@ -1514,6 +1634,7 @@ export {
|
|
|
1514
1634
|
buildSearchIndex,
|
|
1515
1635
|
collectionDir,
|
|
1516
1636
|
collectionGitStatus,
|
|
1637
|
+
collectionMeta,
|
|
1517
1638
|
collectionsDir,
|
|
1518
1639
|
configPath,
|
|
1519
1640
|
createArticle,
|