diffwiki-core 0.3.0-rc.202607220233.d11ed2a → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -38,7 +38,6 @@ __export(index_exports, {
38
38
  NoDefaultCollectionError: () => NoDefaultCollectionError,
39
39
  REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
40
40
  addTags: () => addTags,
41
- buildArticleTree: () => buildArticleTree,
42
41
  collectionDir: () => collectionDir,
43
42
  collectionsDir: () => collectionsDir,
44
43
  configPath: () => configPath,
@@ -48,13 +47,11 @@ __export(index_exports, {
48
47
  findCollection: () => findCollection,
49
48
  getConfigValue: () => getConfigValue,
50
49
  initRepoWiki: () => initRepoWiki,
51
- listArticleTree: () => listArticleTree,
52
50
  listCollections: () => listCollections,
53
51
  parseAddTarget: () => parseAddTarget,
54
52
  parseArticle: () => parseArticle,
55
53
  parsePathTarget: () => parsePathTarget,
56
54
  query: () => query,
57
- readArticle: () => readArticle,
58
55
  readConfig: () => readConfig,
59
56
  readRegistry: () => readRegistry,
60
57
  readRepoConfig: () => readRepoConfig,
@@ -63,7 +60,6 @@ __export(index_exports, {
63
60
  removeArticle: () => removeArticle,
64
61
  removeCollection: () => removeCollection,
65
62
  removeTags: () => removeTags,
66
- renderArticle: () => renderArticle,
67
63
  resolveHome: () => resolveHome,
68
64
  serializeArticle: () => serializeArticle,
69
65
  setConfigValue: () => setConfigValue,
@@ -239,17 +235,8 @@ function slugify(input) {
239
235
 
240
236
  // src/frontmatter.ts
241
237
  var import_gray_matter = __toESM(require("gray-matter"), 1);
242
- var import_yaml = require("yaml");
243
- var YAML_ENGINE = {
244
- parse: (input) => {
245
- const data = (0, import_yaml.parse)(input);
246
- return data && typeof data === "object" ? data : {};
247
- },
248
- stringify: (data) => (0, import_yaml.stringify)(data)
249
- };
250
- var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
251
238
  function parseArticle(raw) {
252
- const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
239
+ const { data, content } = (0, import_gray_matter.default)(raw);
253
240
  const title = typeof data.title === "string" ? data.title : "";
254
241
  const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
255
242
  const created = typeof data.created === "string" ? data.created : void 0;
@@ -259,7 +246,7 @@ function serializeArticle(article) {
259
246
  const front = { title: article.title, tags: article.tags };
260
247
  if (article.created) front.created = article.created;
261
248
  if (article.updated) front.updated = article.updated;
262
- return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
249
+ return import_gray_matter.default.stringify(article.body, front);
263
250
  }
264
251
 
265
252
  // src/articles.ts
@@ -378,7 +365,7 @@ var import_promises5 = __toESM(require("fs/promises"), 1);
378
365
  var import_node_path3 = __toESM(require("path"), 1);
379
366
  var import_node_child_process = require("child_process");
380
367
  var import_node_util = require("util");
381
- var import_yaml2 = require("yaml");
368
+ var import_yaml = require("yaml");
382
369
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
383
370
  var REPO_CONFIG_FILE = "diffwiki.yaml";
384
371
  async function detectRepoName(cwd) {
@@ -392,7 +379,7 @@ async function detectRepoName(cwd) {
392
379
  async function readRepoConfig(cwd) {
393
380
  try {
394
381
  const raw = await import_promises5.default.readFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), "utf8");
395
- const parsed = (0, import_yaml2.parse)(raw) ?? {};
382
+ const parsed = (0, import_yaml.parse)(raw) ?? {};
396
383
  if (!parsed.path) return void 0;
397
384
  return {
398
385
  collection: parsed.collection ?? import_node_path3.default.basename(import_node_path3.default.resolve(cwd)),
@@ -423,7 +410,7 @@ async function initRepoWiki(opts) {
423
410
  }
424
411
  await import_promises5.default.mkdir(absWiki, { recursive: true });
425
412
  const config = { collection: name, path: wikiPath };
426
- await import_promises5.default.writeFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
413
+ await import_promises5.default.writeFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml.stringify)(config), "utf8");
427
414
  const entry = {
428
415
  name,
429
416
  type: "repo",
@@ -507,224 +494,6 @@ async function doctor() {
507
494
  }
508
495
  return out;
509
496
  }
510
-
511
- // src/browse.ts
512
- var import_promises8 = __toESM(require("fs/promises"), 1);
513
- var import_node_path5 = __toESM(require("path"), 1);
514
- var INDEX_RE = /^(index|readme)\.mdx?$/i;
515
- function buildArticleTree(relPaths) {
516
- const root = {};
517
- for (const raw of relPaths) {
518
- const rel = raw.replace(/\\/g, "/").replace(/^\/+/, "");
519
- if (!rel) continue;
520
- const segs = rel.split("/");
521
- let cur = root;
522
- segs.forEach((seg, i) => {
523
- if (i === segs.length - 1) {
524
- if (!(seg in cur)) cur[seg] = null;
525
- } else {
526
- if (cur[seg] === null || !(seg in cur)) cur[seg] = {};
527
- cur = cur[seg];
528
- }
529
- });
530
- }
531
- const stripExt = (f) => f.replace(/\.(mdx?)$/, "");
532
- const walk = (trie, parents) => {
533
- const entries = Object.entries(trie);
534
- const branches = entries.filter(([, v]) => v !== null);
535
- const leaves = entries.filter(([, v]) => v === null).map(([k]) => k).filter((f) => !INDEX_RE.test(f));
536
- branches.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
537
- leaves.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
538
- const branchNodes = branches.map(([seg, sub]) => ({
539
- name: seg,
540
- title: seg,
541
- // Folders are navigable to their index page (an authored index.md/README,
542
- // else a generated listing).
543
- slug: [...parents, seg].join("/"),
544
- children: walk(sub, [...parents, seg])
545
- }));
546
- const leafNodes = leaves.map((file) => {
547
- const nameNoExt = stripExt(file);
548
- return { name: nameNoExt, title: nameNoExt, slug: [...parents, nameNoExt].join("/") };
549
- });
550
- return [...branchNodes, ...leafNodes];
551
- };
552
- return walk(root, []);
553
- }
554
- async function collectRelPaths(dir, rel = "") {
555
- let dirents;
556
- try {
557
- dirents = await import_promises8.default.readdir(dir, { withFileTypes: true });
558
- } catch {
559
- return [];
560
- }
561
- const out = [];
562
- for (const d of dirents) {
563
- if (d.name.startsWith(".")) continue;
564
- const childRel = rel ? `${rel}/${d.name}` : d.name;
565
- if (d.isDirectory()) {
566
- out.push(...await collectRelPaths(import_node_path5.default.join(dir, d.name), childRel));
567
- } else if (/\.(mdx?)$/.test(d.name)) {
568
- out.push(childRel);
569
- }
570
- }
571
- return out;
572
- }
573
- async function readdirSafe(dir) {
574
- try {
575
- return await import_promises8.default.readdir(dir);
576
- } catch {
577
- return [];
578
- }
579
- }
580
- function pickIndexFile(names) {
581
- const rank = (n) => {
582
- const lower = n.toLowerCase();
583
- const base = lower.startsWith("index") ? 0 : 1;
584
- const ext = lower.endsWith(".mdx") ? 0 : 1;
585
- return base * 2 + ext;
586
- };
587
- return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
588
- }
589
- async function cheapTitle(absPath) {
590
- let handle;
591
- try {
592
- handle = await import_promises8.default.open(absPath, "r");
593
- const buf = Buffer.alloc(512);
594
- const { bytesRead } = await handle.read(buf, 0, 512, 0);
595
- const snippet = buf.subarray(0, bytesRead).toString("utf8");
596
- const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
597
- if (m) return m[1].trim().replace(/^["']|["']$/g, "");
598
- } catch {
599
- } finally {
600
- await handle?.close();
601
- }
602
- return void 0;
603
- }
604
- async function enrichTitles(nodes, collDir) {
605
- return Promise.all(
606
- nodes.map(async (node) => {
607
- if (node.children) {
608
- const children = await enrichTitles(node.children, collDir);
609
- let title = node.title;
610
- if (node.slug) {
611
- const dir = import_node_path5.default.join(collDir, node.slug);
612
- const idx = pickIndexFile(await readdirSafe(dir));
613
- if (idx) title = await cheapTitle(import_node_path5.default.join(dir, idx)) ?? title;
614
- }
615
- return { ...node, title, children };
616
- }
617
- for (const ext of [".mdx", ".md"]) {
618
- const title = await cheapTitle(import_node_path5.default.join(collDir, `${node.slug}${ext}`));
619
- if (title) return { ...node, title };
620
- }
621
- return node;
622
- })
623
- );
624
- }
625
- async function listArticleTree(coll) {
626
- const entry = await findCollection(coll);
627
- if (!entry) throw new CollectionNotFoundError(coll);
628
- const rels = await collectRelPaths(entry.path);
629
- return enrichTitles(buildArticleTree(rels), import_node_path5.default.resolve(entry.path));
630
- }
631
- async function readArticle(coll, slug) {
632
- const entry = await findCollection(coll);
633
- if (!entry) throw new CollectionNotFoundError(coll);
634
- const collDir = import_node_path5.default.resolve(entry.path);
635
- const normalized = slug.replace(/\\/g, "/");
636
- if (normalized.split("/").some((s) => s === "..")) {
637
- throw new InvalidTargetError(slug, "path traversal detected");
638
- }
639
- const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
640
- const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path5.default.sep);
641
- let abs;
642
- for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
643
- const candidate = import_node_path5.default.resolve(collDir, rel);
644
- if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
645
- try {
646
- await import_promises8.default.access(candidate);
647
- abs = candidate;
648
- break;
649
- } catch {
650
- }
651
- }
652
- if (!abs) {
653
- const dir = import_node_path5.default.resolve(collDir, safeSlug);
654
- if (inJail(dir)) {
655
- const idx = pickIndexFile(await readdirSafe(dir));
656
- if (idx) abs = import_node_path5.default.join(dir, idx);
657
- }
658
- }
659
- if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
660
- const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
661
- const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
662
- return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
663
- }
664
-
665
- // src/render.ts
666
- var import_unified = require("unified");
667
- var import_remark_parse = __toESM(require("remark-parse"), 1);
668
- var import_remark_gfm = __toESM(require("remark-gfm"), 1);
669
- var import_remark_rehype = __toESM(require("remark-rehype"), 1);
670
- var import_rehype_slug = __toESM(require("rehype-slug"), 1);
671
- var import_rehype = __toESM(require("@shikijs/rehype"), 1);
672
- var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
673
- var import_unist_util_visit = require("unist-util-visit");
674
- var import_hast_util_to_string = require("hast-util-to-string");
675
- var LANG_ALIASES = {
676
- "c#": "csharp",
677
- cs: "csharp",
678
- "c++": "cpp",
679
- cxx: "cpp",
680
- hs: "haskell",
681
- ex: "elixir"
682
- };
683
- function rehypeCollectToc(toc) {
684
- return (tree) => {
685
- (0, import_unist_util_visit.visit)(tree, "element", (node) => {
686
- if (node.tagName !== "h2" && node.tagName !== "h3") return;
687
- const id = node.properties?.id;
688
- if (!id) return;
689
- toc.push({ slug: String(id), text: (0, import_hast_util_to_string.toString)(node), depth: node.tagName === "h2" ? 2 : 3 });
690
- });
691
- };
692
- }
693
- function rehypeStripLeadingH1() {
694
- return (tree) => {
695
- let removed = false;
696
- (0, import_unist_util_visit.visit)(tree, "element", (node, index, parent) => {
697
- if (!removed && node.tagName === "h1" && parent && index != null) {
698
- parent.children.splice(index, 1);
699
- removed = true;
700
- return [import_unist_util_visit.SKIP, index];
701
- }
702
- });
703
- };
704
- }
705
- function rehypeMarkMermaid() {
706
- return (tree) => {
707
- (0, import_unist_util_visit.visit)(tree, "element", (node) => {
708
- if (node.tagName !== "pre") return;
709
- const code = node.children.find((c) => c.type === "element" && c.tagName === "code");
710
- const cls = code?.properties?.className ?? [];
711
- if (!code || !cls.includes("language-mermaid")) return;
712
- const source = (0, import_hast_util_to_string.toString)(code);
713
- node.properties = { className: ["mermaid"] };
714
- node.children = [{ type: "text", value: source }];
715
- });
716
- };
717
- }
718
- async function renderArticle(body) {
719
- const toc = [];
720
- const file = await (0, import_unified.unified)().use(import_remark_parse.default).use(import_remark_gfm.default).use(import_remark_rehype.default).use(import_rehype_slug.default).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(import_rehype.default, {
721
- themes: { light: "github-light", dark: "github-dark" },
722
- defaultColor: false,
723
- langAlias: LANG_ALIASES,
724
- fallbackLanguage: "plaintext"
725
- }).use(import_rehype_stringify.default).process(body);
726
- return { html: String(file), toc };
727
- }
728
497
  // Annotate the CommonJS export names for ESM import in node:
729
498
  0 && (module.exports = {
730
499
  ArticleNotFoundError,
@@ -735,7 +504,6 @@ async function renderArticle(body) {
735
504
  NoDefaultCollectionError,
736
505
  REPO_CONFIG_FILE,
737
506
  addTags,
738
- buildArticleTree,
739
507
  collectionDir,
740
508
  collectionsDir,
741
509
  configPath,
@@ -745,13 +513,11 @@ async function renderArticle(body) {
745
513
  findCollection,
746
514
  getConfigValue,
747
515
  initRepoWiki,
748
- listArticleTree,
749
516
  listCollections,
750
517
  parseAddTarget,
751
518
  parseArticle,
752
519
  parsePathTarget,
753
520
  query,
754
- readArticle,
755
521
  readConfig,
756
522
  readRegistry,
757
523
  readRepoConfig,
@@ -760,7 +526,6 @@ async function renderArticle(body) {
760
526
  removeArticle,
761
527
  removeCollection,
762
528
  removeTags,
763
- renderArticle,
764
529
  resolveHome,
765
530
  serializeArticle,
766
531
  setConfigValue,
package/dist/index.d.cts CHANGED
@@ -25,31 +25,6 @@ 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` (leaf) or the folder's index title, else `name`. */
33
- title?: string;
34
- /**
35
- * Path within the collection, without extension, e.g. "guide/setup". Set on
36
- * leaves (the article) and on branches (the folder, navigable to its index).
37
- */
38
- slug?: string;
39
- /** Branch only: nested nodes. */
40
- children?: ArticleNode[];
41
- }
42
- /** A resolved article's content, returned by `readArticle`. */
43
- interface ArticleContent {
44
- coll: string;
45
- slug: string;
46
- title: string;
47
- tags: string[];
48
- /** Frontmatter-stripped body (md/mdx). */
49
- body: string;
50
- /** Absolute path to the file on disk. */
51
- path: string;
52
- }
53
28
 
54
29
  /** Base class for all expected, user-facing diffwiki failures. */
55
30
  declare class DiffwikiError extends Error {
@@ -167,45 +142,4 @@ interface Diagnostic {
167
142
  }
168
143
  declare function doctor(): Promise<Diagnostic[]>;
169
144
 
170
- /**
171
- * Build a nested ArticleNode[] from a flat list of relative file paths.
172
- * Pure — no I/O. Directories become branches; files become leaves whose `slug`
173
- * is the relative path without extension. Dirs-first, then alpha (case-insensitive).
174
- */
175
- declare function buildArticleTree(relPaths: string[]): ArticleNode[];
176
- /**
177
- * List the article tree of a collection. Walks the collection's registered
178
- * `entry.path` — so collections living OUTSIDE `~/.diffwiki/collections`
179
- * (e.g. in-repo wikis registered via `diffwiki init`) work unchanged.
180
- */
181
- declare function listArticleTree(coll: string): Promise<ArticleNode[]>;
182
- /**
183
- * Read and resolve a single article by collection + slug (relative path without
184
- * extension). Tries `.mdx` then `.md`, guards against path traversal, and splits
185
- * frontmatter. Throws CollectionNotFoundError / ArticleNotFoundError / InvalidTargetError.
186
- */
187
- declare function readArticle(coll: string, slug: string): Promise<ArticleContent>;
188
-
189
- /** A heading extracted for the table of contents (h2/h3). */
190
- interface TocItem {
191
- slug: string;
192
- text: string;
193
- depth: number;
194
- }
195
- /** The result of rendering an article body to HTML. */
196
- interface RenderedArticle {
197
- /** Fully-rendered, Shiki-highlighted HTML — mounted directly on the client (no eval). */
198
- html: string;
199
- /** h2/h3 headings collected during render, in document order. */
200
- toc: TocItem[];
201
- }
202
- /**
203
- * Render an article body (markdown) to Shiki-highlighted HTML plus its table of
204
- * contents. Runs entirely server-side; the client mounts the HTML directly with
205
- * no `eval`/`new Function`, so it works under a strict CSP. Code is highlighted
206
- * here (dual github-light/github-dark via CSS variables); mermaid fences are
207
- * marked for client rendering.
208
- */
209
- declare function renderArticle(body: string): Promise<RenderedArticle>;
210
-
211
- 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 RenderedArticle, type RepoConfig, type SerializableArticle, type TocItem, 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, renderArticle, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
145
+ export { type Article, 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, collectionDir, collectionsDir, configPath, createArticle, createCollection, doctor, findCollection, getConfigValue, initRepoWiki, listCollections, parseAddTarget, parseArticle, parsePathTarget, query, readConfig, readRegistry, readRepoConfig, registerCollection, registryPath, removeArticle, removeCollection, removeTags, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
package/dist/index.d.ts CHANGED
@@ -25,31 +25,6 @@ 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` (leaf) or the folder's index title, else `name`. */
33
- title?: string;
34
- /**
35
- * Path within the collection, without extension, e.g. "guide/setup". Set on
36
- * leaves (the article) and on branches (the folder, navigable to its index).
37
- */
38
- slug?: string;
39
- /** Branch only: nested nodes. */
40
- children?: ArticleNode[];
41
- }
42
- /** A resolved article's content, returned by `readArticle`. */
43
- interface ArticleContent {
44
- coll: string;
45
- slug: string;
46
- title: string;
47
- tags: string[];
48
- /** Frontmatter-stripped body (md/mdx). */
49
- body: string;
50
- /** Absolute path to the file on disk. */
51
- path: string;
52
- }
53
28
 
54
29
  /** Base class for all expected, user-facing diffwiki failures. */
55
30
  declare class DiffwikiError extends Error {
@@ -167,45 +142,4 @@ interface Diagnostic {
167
142
  }
168
143
  declare function doctor(): Promise<Diagnostic[]>;
169
144
 
170
- /**
171
- * Build a nested ArticleNode[] from a flat list of relative file paths.
172
- * Pure — no I/O. Directories become branches; files become leaves whose `slug`
173
- * is the relative path without extension. Dirs-first, then alpha (case-insensitive).
174
- */
175
- declare function buildArticleTree(relPaths: string[]): ArticleNode[];
176
- /**
177
- * List the article tree of a collection. Walks the collection's registered
178
- * `entry.path` — so collections living OUTSIDE `~/.diffwiki/collections`
179
- * (e.g. in-repo wikis registered via `diffwiki init`) work unchanged.
180
- */
181
- declare function listArticleTree(coll: string): Promise<ArticleNode[]>;
182
- /**
183
- * Read and resolve a single article by collection + slug (relative path without
184
- * extension). Tries `.mdx` then `.md`, guards against path traversal, and splits
185
- * frontmatter. Throws CollectionNotFoundError / ArticleNotFoundError / InvalidTargetError.
186
- */
187
- declare function readArticle(coll: string, slug: string): Promise<ArticleContent>;
188
-
189
- /** A heading extracted for the table of contents (h2/h3). */
190
- interface TocItem {
191
- slug: string;
192
- text: string;
193
- depth: number;
194
- }
195
- /** The result of rendering an article body to HTML. */
196
- interface RenderedArticle {
197
- /** Fully-rendered, Shiki-highlighted HTML — mounted directly on the client (no eval). */
198
- html: string;
199
- /** h2/h3 headings collected during render, in document order. */
200
- toc: TocItem[];
201
- }
202
- /**
203
- * Render an article body (markdown) to Shiki-highlighted HTML plus its table of
204
- * contents. Runs entirely server-side; the client mounts the HTML directly with
205
- * no `eval`/`new Function`, so it works under a strict CSP. Code is highlighted
206
- * here (dual github-light/github-dark via CSS variables); mermaid fences are
207
- * marked for client rendering.
208
- */
209
- declare function renderArticle(body: string): Promise<RenderedArticle>;
210
-
211
- 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 RenderedArticle, type RepoConfig, type SerializableArticle, type TocItem, 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, renderArticle, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
145
+ export { type Article, 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, collectionDir, collectionsDir, configPath, createArticle, createCollection, doctor, findCollection, getConfigValue, initRepoWiki, listCollections, parseAddTarget, parseArticle, parsePathTarget, query, readConfig, readRegistry, readRepoConfig, registerCollection, registryPath, removeArticle, removeCollection, removeTags, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
package/dist/index.js CHANGED
@@ -161,17 +161,8 @@ function slugify(input) {
161
161
 
162
162
  // src/frontmatter.ts
163
163
  import matter from "gray-matter";
164
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
165
- var YAML_ENGINE = {
166
- parse: (input) => {
167
- const data = parseYaml(input);
168
- return data && typeof data === "object" ? data : {};
169
- },
170
- stringify: (data) => stringifyYaml(data)
171
- };
172
- var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
173
164
  function parseArticle(raw) {
174
- const { data, content } = matter(raw, MATTER_OPTS);
165
+ const { data, content } = matter(raw);
175
166
  const title = typeof data.title === "string" ? data.title : "";
176
167
  const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
177
168
  const created = typeof data.created === "string" ? data.created : void 0;
@@ -181,7 +172,7 @@ function serializeArticle(article) {
181
172
  const front = { title: article.title, tags: article.tags };
182
173
  if (article.created) front.created = article.created;
183
174
  if (article.updated) front.updated = article.updated;
184
- return matter.stringify(article.body, front, MATTER_OPTS);
175
+ return matter.stringify(article.body, front);
185
176
  }
186
177
 
187
178
  // src/articles.ts
@@ -300,7 +291,7 @@ import fs5 from "fs/promises";
300
291
  import path3 from "path";
301
292
  import { execFile } from "child_process";
302
293
  import { promisify } from "util";
303
- import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
294
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
304
295
  var execFileAsync = promisify(execFile);
305
296
  var REPO_CONFIG_FILE = "diffwiki.yaml";
306
297
  async function detectRepoName(cwd) {
@@ -314,7 +305,7 @@ async function detectRepoName(cwd) {
314
305
  async function readRepoConfig(cwd) {
315
306
  try {
316
307
  const raw = await fs5.readFile(path3.join(cwd, REPO_CONFIG_FILE), "utf8");
317
- const parsed = parseYaml2(raw) ?? {};
308
+ const parsed = parseYaml(raw) ?? {};
318
309
  if (!parsed.path) return void 0;
319
310
  return {
320
311
  collection: parsed.collection ?? path3.basename(path3.resolve(cwd)),
@@ -345,7 +336,7 @@ async function initRepoWiki(opts) {
345
336
  }
346
337
  await fs5.mkdir(absWiki, { recursive: true });
347
338
  const config = { collection: name, path: wikiPath };
348
- await fs5.writeFile(path3.join(cwd, REPO_CONFIG_FILE), stringifyYaml2(config), "utf8");
339
+ await fs5.writeFile(path3.join(cwd, REPO_CONFIG_FILE), stringifyYaml(config), "utf8");
349
340
  const entry = {
350
341
  name,
351
342
  type: "repo",
@@ -429,224 +420,6 @@ async function doctor() {
429
420
  }
430
421
  return out;
431
422
  }
432
-
433
- // src/browse.ts
434
- import fs8 from "fs/promises";
435
- import path5 from "path";
436
- var INDEX_RE = /^(index|readme)\.mdx?$/i;
437
- function buildArticleTree(relPaths) {
438
- const root = {};
439
- for (const raw of relPaths) {
440
- const rel = raw.replace(/\\/g, "/").replace(/^\/+/, "");
441
- if (!rel) continue;
442
- const segs = rel.split("/");
443
- let cur = root;
444
- segs.forEach((seg, i) => {
445
- if (i === segs.length - 1) {
446
- if (!(seg in cur)) cur[seg] = null;
447
- } else {
448
- if (cur[seg] === null || !(seg in cur)) cur[seg] = {};
449
- cur = cur[seg];
450
- }
451
- });
452
- }
453
- const stripExt = (f) => f.replace(/\.(mdx?)$/, "");
454
- const walk = (trie, parents) => {
455
- const entries = Object.entries(trie);
456
- const branches = entries.filter(([, v]) => v !== null);
457
- const leaves = entries.filter(([, v]) => v === null).map(([k]) => k).filter((f) => !INDEX_RE.test(f));
458
- branches.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
459
- leaves.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
460
- const branchNodes = branches.map(([seg, sub]) => ({
461
- name: seg,
462
- title: seg,
463
- // Folders are navigable to their index page (an authored index.md/README,
464
- // else a generated listing).
465
- slug: [...parents, seg].join("/"),
466
- children: walk(sub, [...parents, seg])
467
- }));
468
- const leafNodes = leaves.map((file) => {
469
- const nameNoExt = stripExt(file);
470
- return { name: nameNoExt, title: nameNoExt, slug: [...parents, nameNoExt].join("/") };
471
- });
472
- return [...branchNodes, ...leafNodes];
473
- };
474
- return walk(root, []);
475
- }
476
- async function collectRelPaths(dir, rel = "") {
477
- let dirents;
478
- try {
479
- dirents = await fs8.readdir(dir, { withFileTypes: true });
480
- } catch {
481
- return [];
482
- }
483
- const out = [];
484
- for (const d of dirents) {
485
- if (d.name.startsWith(".")) continue;
486
- const childRel = rel ? `${rel}/${d.name}` : d.name;
487
- if (d.isDirectory()) {
488
- out.push(...await collectRelPaths(path5.join(dir, d.name), childRel));
489
- } else if (/\.(mdx?)$/.test(d.name)) {
490
- out.push(childRel);
491
- }
492
- }
493
- return out;
494
- }
495
- async function readdirSafe(dir) {
496
- try {
497
- return await fs8.readdir(dir);
498
- } catch {
499
- return [];
500
- }
501
- }
502
- function pickIndexFile(names) {
503
- const rank = (n) => {
504
- const lower = n.toLowerCase();
505
- const base = lower.startsWith("index") ? 0 : 1;
506
- const ext = lower.endsWith(".mdx") ? 0 : 1;
507
- return base * 2 + ext;
508
- };
509
- return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
510
- }
511
- async function cheapTitle(absPath) {
512
- let handle;
513
- try {
514
- handle = await fs8.open(absPath, "r");
515
- const buf = Buffer.alloc(512);
516
- const { bytesRead } = await handle.read(buf, 0, 512, 0);
517
- const snippet = buf.subarray(0, bytesRead).toString("utf8");
518
- const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
519
- if (m) return m[1].trim().replace(/^["']|["']$/g, "");
520
- } catch {
521
- } finally {
522
- await handle?.close();
523
- }
524
- return void 0;
525
- }
526
- async function enrichTitles(nodes, collDir) {
527
- return Promise.all(
528
- nodes.map(async (node) => {
529
- if (node.children) {
530
- const children = await enrichTitles(node.children, collDir);
531
- let title = node.title;
532
- if (node.slug) {
533
- const dir = path5.join(collDir, node.slug);
534
- const idx = pickIndexFile(await readdirSafe(dir));
535
- if (idx) title = await cheapTitle(path5.join(dir, idx)) ?? title;
536
- }
537
- return { ...node, title, children };
538
- }
539
- for (const ext of [".mdx", ".md"]) {
540
- const title = await cheapTitle(path5.join(collDir, `${node.slug}${ext}`));
541
- if (title) return { ...node, title };
542
- }
543
- return node;
544
- })
545
- );
546
- }
547
- async function listArticleTree(coll) {
548
- const entry = await findCollection(coll);
549
- if (!entry) throw new CollectionNotFoundError(coll);
550
- const rels = await collectRelPaths(entry.path);
551
- return enrichTitles(buildArticleTree(rels), path5.resolve(entry.path));
552
- }
553
- async function readArticle(coll, slug) {
554
- const entry = await findCollection(coll);
555
- if (!entry) throw new CollectionNotFoundError(coll);
556
- const collDir = path5.resolve(entry.path);
557
- const normalized = slug.replace(/\\/g, "/");
558
- if (normalized.split("/").some((s) => s === "..")) {
559
- throw new InvalidTargetError(slug, "path traversal detected");
560
- }
561
- const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
562
- const inJail = (p) => p === collDir || p.startsWith(collDir + path5.sep);
563
- let abs;
564
- for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
565
- const candidate = path5.resolve(collDir, rel);
566
- if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
567
- try {
568
- await fs8.access(candidate);
569
- abs = candidate;
570
- break;
571
- } catch {
572
- }
573
- }
574
- if (!abs) {
575
- const dir = path5.resolve(collDir, safeSlug);
576
- if (inJail(dir)) {
577
- const idx = pickIndexFile(await readdirSafe(dir));
578
- if (idx) abs = path5.join(dir, idx);
579
- }
580
- }
581
- if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
582
- const parsed = parseArticle(await fs8.readFile(abs, "utf8"));
583
- const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
584
- return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
585
- }
586
-
587
- // src/render.ts
588
- import { unified } from "unified";
589
- import remarkParse from "remark-parse";
590
- import remarkGfm from "remark-gfm";
591
- import remarkRehype from "remark-rehype";
592
- import rehypeSlug from "rehype-slug";
593
- import rehypeShiki from "@shikijs/rehype";
594
- import rehypeStringify from "rehype-stringify";
595
- import { visit, SKIP } from "unist-util-visit";
596
- import { toString as hastToString } from "hast-util-to-string";
597
- var LANG_ALIASES = {
598
- "c#": "csharp",
599
- cs: "csharp",
600
- "c++": "cpp",
601
- cxx: "cpp",
602
- hs: "haskell",
603
- ex: "elixir"
604
- };
605
- function rehypeCollectToc(toc) {
606
- return (tree) => {
607
- visit(tree, "element", (node) => {
608
- if (node.tagName !== "h2" && node.tagName !== "h3") return;
609
- const id = node.properties?.id;
610
- if (!id) return;
611
- toc.push({ slug: String(id), text: hastToString(node), depth: node.tagName === "h2" ? 2 : 3 });
612
- });
613
- };
614
- }
615
- function rehypeStripLeadingH1() {
616
- return (tree) => {
617
- let removed = false;
618
- visit(tree, "element", (node, index, parent) => {
619
- if (!removed && node.tagName === "h1" && parent && index != null) {
620
- parent.children.splice(index, 1);
621
- removed = true;
622
- return [SKIP, index];
623
- }
624
- });
625
- };
626
- }
627
- function rehypeMarkMermaid() {
628
- return (tree) => {
629
- visit(tree, "element", (node) => {
630
- if (node.tagName !== "pre") return;
631
- const code = node.children.find((c) => c.type === "element" && c.tagName === "code");
632
- const cls = code?.properties?.className ?? [];
633
- if (!code || !cls.includes("language-mermaid")) return;
634
- const source = hastToString(code);
635
- node.properties = { className: ["mermaid"] };
636
- node.children = [{ type: "text", value: source }];
637
- });
638
- };
639
- }
640
- async function renderArticle(body) {
641
- const toc = [];
642
- const file = await unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeSlug).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(rehypeShiki, {
643
- themes: { light: "github-light", dark: "github-dark" },
644
- defaultColor: false,
645
- langAlias: LANG_ALIASES,
646
- fallbackLanguage: "plaintext"
647
- }).use(rehypeStringify).process(body);
648
- return { html: String(file), toc };
649
- }
650
423
  export {
651
424
  ArticleNotFoundError,
652
425
  CollectionExistsError,
@@ -656,7 +429,6 @@ export {
656
429
  NoDefaultCollectionError,
657
430
  REPO_CONFIG_FILE,
658
431
  addTags,
659
- buildArticleTree,
660
432
  collectionDir,
661
433
  collectionsDir,
662
434
  configPath,
@@ -666,13 +438,11 @@ export {
666
438
  findCollection,
667
439
  getConfigValue,
668
440
  initRepoWiki,
669
- listArticleTree,
670
441
  listCollections,
671
442
  parseAddTarget,
672
443
  parseArticle,
673
444
  parsePathTarget,
674
445
  query,
675
- readArticle,
676
446
  readConfig,
677
447
  readRegistry,
678
448
  readRepoConfig,
@@ -681,7 +451,6 @@ export {
681
451
  removeArticle,
682
452
  removeCollection,
683
453
  removeTags,
684
- renderArticle,
685
454
  resolveHome,
686
455
  serializeArticle,
687
456
  setConfigValue,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffwiki-core",
3
- "version": "0.3.0-rc.202607220233.d11ed2a",
3
+ "version": "0.3.0",
4
4
  "description": "Core configuration, registry, and article management for diffwiki",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,20 +21,10 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
- "@shikijs/rehype": "^3.0.0",
25
24
  "gray-matter": "^4.0.3",
26
- "hast-util-to-string": "^3.0.0",
27
- "rehype-slug": "^6.0.0",
28
- "rehype-stringify": "^10.0.0",
29
- "remark-gfm": "^4.0.0",
30
- "remark-parse": "^11.0.0",
31
- "remark-rehype": "^11.0.0",
32
- "unified": "^11.0.0",
33
- "unist-util-visit": "^5.0.0",
34
25
  "yaml": "^2.5.0"
35
26
  },
36
27
  "devDependencies": {
37
- "@types/hast": "^3.0.5",
38
28
  "@types/node": "^22.0.0"
39
29
  },
40
30
  "scripts": {