@mulmoclaude/core 0.2.16 → 0.4.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/slug-CdN-pQX1.js +37 -0
- package/dist/slug-CdN-pQX1.js.map +1 -0
- package/dist/slug-DbwORN9z.cjs +48 -0
- package/dist/slug-DbwORN9z.cjs.map +1 -0
- package/dist/wiki/graph.d.ts +33 -0
- package/dist/wiki/index-parse.d.ts +39 -0
- package/dist/wiki/index.cjs +493 -0
- package/dist/wiki/index.cjs.map +1 -0
- package/dist/wiki/index.d.ts +7 -0
- package/dist/wiki/index.js +465 -0
- package/dist/wiki/index.js.map +1 -0
- package/dist/wiki/link.d.ts +41 -0
- package/dist/wiki/lint.d.ts +26 -0
- package/dist/wiki/render.d.ts +19 -0
- package/dist/wiki/route.d.ts +32 -0
- package/dist/wiki/server/index.cjs +30 -0
- package/dist/wiki/server/index.cjs.map +1 -0
- package/dist/wiki/server/index.d.ts +1 -0
- package/dist/wiki/server/index.js +27 -0
- package/dist/wiki/server/index.js.map +1 -0
- package/dist/wiki/server/paths.d.ts +11 -0
- package/dist/wiki/slug.d.ts +24 -0
- package/package.json +13 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/wiki/slug.ts
|
|
2
|
+
/** Reject slugs that would escape `data/wiki/pages/` once
|
|
3
|
+
* joined back into a path, or that are otherwise invalid as
|
|
4
|
+
* page filenames. The chokepoint must defend itself even when
|
|
5
|
+
* callers derive the slug from a trusted source — a typo or
|
|
6
|
+
* future caller mistake should fail loud, not silently write
|
|
7
|
+
* outside the wiki tree.
|
|
8
|
+
*
|
|
9
|
+
* The rule is intentionally narrow — separators / `..` / NUL /
|
|
10
|
+
* empty — so it only rejects unambiguous violations. Aesthetic
|
|
11
|
+
* concerns (e.g. dot-prefixed filenames) are out of scope: a
|
|
12
|
+
* pre-existing `data/wiki/pages/.foo.md` should remain writable
|
|
13
|
+
* through the chokepoint (codex review iter-2 #883). */
|
|
14
|
+
function isSafeSlug(slug) {
|
|
15
|
+
if (slug.length === 0) return false;
|
|
16
|
+
if (slug === "." || slug === "..") return false;
|
|
17
|
+
if (slug.includes("/") || slug.includes("\\")) return false;
|
|
18
|
+
if (slug.includes("\0")) return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/** Slug rules for `[[wiki link]]` text → slug derivation:
|
|
22
|
+
* lowercase, spaces collapsed to hyphens, every non-ASCII /
|
|
23
|
+
* non-alphanumeric / non-hyphen character stripped.
|
|
24
|
+
*
|
|
25
|
+
* Pure: no normalisation, no transliteration — this is the
|
|
26
|
+
* same shape the index parser, page resolver, and frontend
|
|
27
|
+
* renderer all need to agree on. Non-ASCII titles (e.g.
|
|
28
|
+
* Japanese) collapse to an empty string here; callers fall back
|
|
29
|
+
* to other strategies (title-match in the index, or the agent
|
|
30
|
+
* pre-resolving to a slug-form target via `[[slug|display]]`). */
|
|
31
|
+
function wikiSlugify(text) {
|
|
32
|
+
return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { wikiSlugify as n, isSafeSlug as t };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=slug-CdN-pQX1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug-CdN-pQX1.js","names":[],"sources":["../src/wiki/slug.ts"],"sourcesContent":["// Pure wiki-page slug helpers — string-only, no Node / browser\n// dependencies, importable from any bundle.\n//\n// Used by:\n// - server/workspace/wiki-pages/io.ts (the write chokepoint)\n// - server/workspace/hooks/handlers/wikiSnapshot.ts (the\n// PostToolUse handler that fires on Write/Edit of a wiki page)\n// - server/api/routes/wiki.ts (page resolver + lint)\n// - server/api/routes/wiki/history.ts (history slug guard)\n// - src/plugins/wiki/route.ts (router slug guard — replaces the\n// local `isSafeWikiSlug` duplicate that existed before the\n// pure-lib refactor)\n// - src/plugins/wiki/helpers.ts (renderer slugify for [[…]] links)\n//\n// Any helper that needs `node:path` lives in `./paths.ts` so that\n// importing this file from frontend code never pulls in Node\n// builtins.\n\n/** Reject slugs that would escape `data/wiki/pages/` once\n * joined back into a path, or that are otherwise invalid as\n * page filenames. The chokepoint must defend itself even when\n * callers derive the slug from a trusted source — a typo or\n * future caller mistake should fail loud, not silently write\n * outside the wiki tree.\n *\n * The rule is intentionally narrow — separators / `..` / NUL /\n * empty — so it only rejects unambiguous violations. Aesthetic\n * concerns (e.g. dot-prefixed filenames) are out of scope: a\n * pre-existing `data/wiki/pages/.foo.md` should remain writable\n * through the chokepoint (codex review iter-2 #883). */\nexport function isSafeSlug(slug: string): boolean {\n if (slug.length === 0) return false;\n if (slug === \".\" || slug === \"..\") return false;\n if (slug.includes(\"/\") || slug.includes(\"\\\\\")) return false;\n if (slug.includes(\"\\0\")) return false;\n return true;\n}\n\n/** Slug rules for `[[wiki link]]` text → slug derivation:\n * lowercase, spaces collapsed to hyphens, every non-ASCII /\n * non-alphanumeric / non-hyphen character stripped.\n *\n * Pure: no normalisation, no transliteration — this is the\n * same shape the index parser, page resolver, and frontend\n * renderer all need to agree on. Non-ASCII titles (e.g.\n * Japanese) collapse to an empty string here; callers fall back\n * to other strategies (title-match in the index, or the agent\n * pre-resolving to a slug-form target via `[[slug|display]]`). */\nexport function wikiSlugify(text: string): string {\n return text\n .toLowerCase()\n .replace(/\\s+/g, \"-\")\n .replace(/[^a-z0-9-]/g, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;AA8BA,SAAgB,WAAW,MAAuB;CAChD,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,IAAI,SAAS,OAAO,SAAS,MAAM,OAAO;CAC1C,IAAI,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,GAAG,OAAO;CACtD,IAAI,KAAK,SAAS,IAAI,GAAG,OAAO;CAChC,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,YAAY,MAAsB;CAChD,OAAO,KACJ,YAAY,CAAC,CACb,QAAQ,QAAQ,GAAG,CAAC,CACpB,QAAQ,eAAe,EAAE;AAC9B"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/wiki/slug.ts
|
|
2
|
+
/** Reject slugs that would escape `data/wiki/pages/` once
|
|
3
|
+
* joined back into a path, or that are otherwise invalid as
|
|
4
|
+
* page filenames. The chokepoint must defend itself even when
|
|
5
|
+
* callers derive the slug from a trusted source — a typo or
|
|
6
|
+
* future caller mistake should fail loud, not silently write
|
|
7
|
+
* outside the wiki tree.
|
|
8
|
+
*
|
|
9
|
+
* The rule is intentionally narrow — separators / `..` / NUL /
|
|
10
|
+
* empty — so it only rejects unambiguous violations. Aesthetic
|
|
11
|
+
* concerns (e.g. dot-prefixed filenames) are out of scope: a
|
|
12
|
+
* pre-existing `data/wiki/pages/.foo.md` should remain writable
|
|
13
|
+
* through the chokepoint (codex review iter-2 #883). */
|
|
14
|
+
function isSafeSlug(slug) {
|
|
15
|
+
if (slug.length === 0) return false;
|
|
16
|
+
if (slug === "." || slug === "..") return false;
|
|
17
|
+
if (slug.includes("/") || slug.includes("\\")) return false;
|
|
18
|
+
if (slug.includes("\0")) return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/** Slug rules for `[[wiki link]]` text → slug derivation:
|
|
22
|
+
* lowercase, spaces collapsed to hyphens, every non-ASCII /
|
|
23
|
+
* non-alphanumeric / non-hyphen character stripped.
|
|
24
|
+
*
|
|
25
|
+
* Pure: no normalisation, no transliteration — this is the
|
|
26
|
+
* same shape the index parser, page resolver, and frontend
|
|
27
|
+
* renderer all need to agree on. Non-ASCII titles (e.g.
|
|
28
|
+
* Japanese) collapse to an empty string here; callers fall back
|
|
29
|
+
* to other strategies (title-match in the index, or the agent
|
|
30
|
+
* pre-resolving to a slug-form target via `[[slug|display]]`). */
|
|
31
|
+
function wikiSlugify(text) {
|
|
32
|
+
return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
Object.defineProperty(exports, "isSafeSlug", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: function() {
|
|
38
|
+
return isSafeSlug;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(exports, "wikiSlugify", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get: function() {
|
|
44
|
+
return wikiSlugify;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=slug-DbwORN9z.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug-DbwORN9z.cjs","names":[],"sources":["../src/wiki/slug.ts"],"sourcesContent":["// Pure wiki-page slug helpers — string-only, no Node / browser\n// dependencies, importable from any bundle.\n//\n// Used by:\n// - server/workspace/wiki-pages/io.ts (the write chokepoint)\n// - server/workspace/hooks/handlers/wikiSnapshot.ts (the\n// PostToolUse handler that fires on Write/Edit of a wiki page)\n// - server/api/routes/wiki.ts (page resolver + lint)\n// - server/api/routes/wiki/history.ts (history slug guard)\n// - src/plugins/wiki/route.ts (router slug guard — replaces the\n// local `isSafeWikiSlug` duplicate that existed before the\n// pure-lib refactor)\n// - src/plugins/wiki/helpers.ts (renderer slugify for [[…]] links)\n//\n// Any helper that needs `node:path` lives in `./paths.ts` so that\n// importing this file from frontend code never pulls in Node\n// builtins.\n\n/** Reject slugs that would escape `data/wiki/pages/` once\n * joined back into a path, or that are otherwise invalid as\n * page filenames. The chokepoint must defend itself even when\n * callers derive the slug from a trusted source — a typo or\n * future caller mistake should fail loud, not silently write\n * outside the wiki tree.\n *\n * The rule is intentionally narrow — separators / `..` / NUL /\n * empty — so it only rejects unambiguous violations. Aesthetic\n * concerns (e.g. dot-prefixed filenames) are out of scope: a\n * pre-existing `data/wiki/pages/.foo.md` should remain writable\n * through the chokepoint (codex review iter-2 #883). */\nexport function isSafeSlug(slug: string): boolean {\n if (slug.length === 0) return false;\n if (slug === \".\" || slug === \"..\") return false;\n if (slug.includes(\"/\") || slug.includes(\"\\\\\")) return false;\n if (slug.includes(\"\\0\")) return false;\n return true;\n}\n\n/** Slug rules for `[[wiki link]]` text → slug derivation:\n * lowercase, spaces collapsed to hyphens, every non-ASCII /\n * non-alphanumeric / non-hyphen character stripped.\n *\n * Pure: no normalisation, no transliteration — this is the\n * same shape the index parser, page resolver, and frontend\n * renderer all need to agree on. Non-ASCII titles (e.g.\n * Japanese) collapse to an empty string here; callers fall back\n * to other strategies (title-match in the index, or the agent\n * pre-resolving to a slug-form target via `[[slug|display]]`). */\nexport function wikiSlugify(text: string): string {\n return text\n .toLowerCase()\n .replace(/\\s+/g, \"-\")\n .replace(/[^a-z0-9-]/g, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;AA8BA,SAAgB,WAAW,MAAuB;CAChD,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,IAAI,SAAS,OAAO,SAAS,MAAM,OAAO;CAC1C,IAAI,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,GAAG,OAAO;CACtD,IAAI,KAAK,SAAS,IAAI,GAAG,OAAO;CAChC,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,YAAY,MAAsB;CAChD,OAAO,KACJ,YAAY,CAAC,CACb,QAAQ,QAAQ,GAAG,CAAC,CACpB,QAAQ,eAAe,EAAE;AAC9B"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WikiPageEntry } from './index-parse.js';
|
|
2
|
+
export interface WikiGraphNode {
|
|
3
|
+
slug: string;
|
|
4
|
+
title: string;
|
|
5
|
+
}
|
|
6
|
+
export interface WikiGraphEdge {
|
|
7
|
+
from: string;
|
|
8
|
+
to: string;
|
|
9
|
+
}
|
|
10
|
+
export interface WikiGraph {
|
|
11
|
+
nodes: WikiGraphNode[];
|
|
12
|
+
edges: WikiGraphEdge[];
|
|
13
|
+
}
|
|
14
|
+
/** One page's raw body plus its canonical (file-derived) slug. */
|
|
15
|
+
export interface WikiPageContent {
|
|
16
|
+
slug: string;
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
/** Resolve a raw `[[link]]` target to an existing page slug, or null.
|
|
20
|
+
* Mirrors the route resolver's strategy: slugify first, then fall
|
|
21
|
+
* back to matching an index entry title so non-ASCII targets like
|
|
22
|
+
* `[[さくらインターネット]]` resolve to their ASCII file slug. */
|
|
23
|
+
export declare function resolveLinkTarget(target: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string | null;
|
|
24
|
+
/** Resolved, deduped outgoing slugs for one page body. Self-links and
|
|
25
|
+
* links to non-existent pages are dropped (the latter are already a
|
|
26
|
+
* lint "broken link", not a graph edge). */
|
|
27
|
+
export declare function pageOutgoingSlugs(fromSlug: string, content: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string[];
|
|
28
|
+
/** Build the full page→page graph. Nodes are the existing page files
|
|
29
|
+
* (titled from index.md, falling back to the slug for un-indexed
|
|
30
|
+
* pages); edges are the resolved `[[links]]`, deduped per (from,to). */
|
|
31
|
+
export declare function buildWikiGraph(pages: readonly WikiPageContent[], entries: readonly WikiPageEntry[]): WikiGraph;
|
|
32
|
+
/** Pages that link TO `slug` (incoming edges), deduped, as nodes. */
|
|
33
|
+
export declare function incomingLinks(graph: WikiGraph, slug: string): WikiGraphNode[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface WikiPageEntry {
|
|
2
|
+
title: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
description: string;
|
|
5
|
+
tags: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const BULLET_LINK_PATTERN: RegExp;
|
|
8
|
+
export declare const BULLET_WIKI_LINK_PATTERN: RegExp;
|
|
9
|
+
/** Extract `#tag` tokens from a bullet description, returning the
|
|
10
|
+
* stripped description and a sorted, deduped, lowercased tag list.
|
|
11
|
+
* Only matches at word boundaries so mid-word `#` (e.g. anchor
|
|
12
|
+
* URLs) is left alone. */
|
|
13
|
+
export declare function extractHashTags(text: string): {
|
|
14
|
+
description: string;
|
|
15
|
+
tags: string[];
|
|
16
|
+
};
|
|
17
|
+
/** Split a table Tags cell — tolerates comma, whitespace, or `#`
|
|
18
|
+
* prefixes. Empty cell yields an empty list. */
|
|
19
|
+
export declare function parseTagsCell(cell: string): string[];
|
|
20
|
+
/** Map header cell names → column indices, case- and whitespace-
|
|
21
|
+
* tolerant. Used by the row parser to locate the Tags column (and
|
|
22
|
+
* any other named column) without assuming a fixed position, so
|
|
23
|
+
* older 3- and 4-column tables keep working. */
|
|
24
|
+
export declare function buildTableColumnMap(headerRow: string): Map<string, number>;
|
|
25
|
+
/** Extract the slug segment from a bullet link's href. Accepts the
|
|
26
|
+
* canonical `pages/<slug>.md`, a bare `<slug>.md`, or just `<slug>`
|
|
27
|
+
* — the three forms produced by different historical writers of
|
|
28
|
+
* index.md. Returns "" for hrefs that don't look like a wiki page
|
|
29
|
+
* reference (e.g. `https://example.com`) so the caller can fall
|
|
30
|
+
* back to title-based slugification. */
|
|
31
|
+
export declare function extractSlugFromBulletHref(rawHref: string): string;
|
|
32
|
+
/** Parse entries from index.md. Supports three formats:
|
|
33
|
+
* 1. Table: `| slug | Title | Summary | (Tags) |`
|
|
34
|
+
* 2. Bullet link: `- [Title](pages/slug.md) — description`
|
|
35
|
+
* 3. Wiki link: `- [[Title]] — description`
|
|
36
|
+
*
|
|
37
|
+
* Returns entries in source order. Any unrecognised line is
|
|
38
|
+
* silently skipped — index.md is freeform markdown otherwise. */
|
|
39
|
+
export declare function parseIndexEntries(content: string): WikiPageEntry[];
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_slug = require("../slug-DbwORN9z.cjs");
|
|
3
|
+
//#region src/wiki/link.ts
|
|
4
|
+
/** Inline `[[...]]` link pattern. The capture group is the raw
|
|
5
|
+
* text between the brackets — the caller is expected to feed it
|
|
6
|
+
* through `parseWikiLink` to split off any `|display` suffix.
|
|
7
|
+
*
|
|
8
|
+
* Body length is capped at 200 chars to keep the regex linear:
|
|
9
|
+
* catastrophic backtracking isn't possible (no nested
|
|
10
|
+
* alternation), but a malicious page with thousands of
|
|
11
|
+
* unmatched `[[` could still pin the CPU. 200 is well above the
|
|
12
|
+
* longest legitimate title we've seen and matches the cap the
|
|
13
|
+
* server-side `WIKI_LINK_PATTERN` constant has carried since the
|
|
14
|
+
* original implementation in #951. */
|
|
15
|
+
var WIKI_LINK_PATTERN = /\[\[([^\][\r\n]{1,200})\]\]/g;
|
|
16
|
+
/** Split the raw text inside `[[...]]` into target + display.
|
|
17
|
+
*
|
|
18
|
+
* - `[[foo|Bar Baz]]` → `{ target: "foo", display: "Bar Baz" }`
|
|
19
|
+
* - `[[foo]]` → `{ target: "foo", display: "foo" }`
|
|
20
|
+
* - `[[|empty target]]` → `{ target: "", display: "empty target" }`
|
|
21
|
+
* (caller decides whether an empty target is meaningful;
|
|
22
|
+
* typically lint flags it, renderer shows the display text raw)
|
|
23
|
+
* - `[[foo|]]` → `{ target: "foo", display: "" }`
|
|
24
|
+
* (same — caller decides)
|
|
25
|
+
*
|
|
26
|
+
* Whitespace around the pipe is preserved on the display side
|
|
27
|
+
* (so the user can write `[[foo| ある記事 ]]` and get the
|
|
28
|
+
* spacing intentionally) but TRIMMED on the target side, since
|
|
29
|
+
* a slug-comparable target with leading/trailing whitespace is
|
|
30
|
+
* always a typo.
|
|
31
|
+
*
|
|
32
|
+
* Only the FIRST pipe splits. `[[a|b|c]]` becomes
|
|
33
|
+
* `{ target: "a", display: "b|c" }` so display strings can
|
|
34
|
+
* legitimately contain a literal `|`. */
|
|
35
|
+
function parseWikiLink(inner) {
|
|
36
|
+
const pipeIdx = inner.indexOf("|");
|
|
37
|
+
if (pipeIdx === -1) return {
|
|
38
|
+
target: inner,
|
|
39
|
+
display: inner
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
target: inner.slice(0, pipeIdx).trim(),
|
|
43
|
+
display: inner.slice(pipeIdx + 1)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/wiki/index-parse.ts
|
|
48
|
+
var BULLET_LINK_PATTERN = /^[-*]\s+\[([^\]]+)\]\(([^)]*)\)(?:\s*[—–-]\s*(.*))?/;
|
|
49
|
+
var BULLET_WIKI_LINK_PATTERN = /^[-*]\s+\[\[([^\]]+)\]\](?:\s*[—–-]\s*(.*))?/;
|
|
50
|
+
var TABLE_SEPARATOR_PATTERN = /^\|[\s|:-]+\|$/;
|
|
51
|
+
var HASHTAG_PATTERN = /(?:^|\s)#([\p{L}\p{N}][\p{L}\p{N}_-]*)/gu;
|
|
52
|
+
/** Extract `#tag` tokens from a bullet description, returning the
|
|
53
|
+
* stripped description and a sorted, deduped, lowercased tag list.
|
|
54
|
+
* Only matches at word boundaries so mid-word `#` (e.g. anchor
|
|
55
|
+
* URLs) is left alone. */
|
|
56
|
+
function extractHashTags(text) {
|
|
57
|
+
const tags = [];
|
|
58
|
+
HASHTAG_PATTERN.lastIndex = 0;
|
|
59
|
+
let match;
|
|
60
|
+
while ((match = HASHTAG_PATTERN.exec(text)) !== null) tags.push(match[1].toLowerCase());
|
|
61
|
+
return {
|
|
62
|
+
description: text.replace(HASHTAG_PATTERN, "").replace(/\s+/g, " ").trim(),
|
|
63
|
+
tags: [...new Set(tags)].sort()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Split a table Tags cell — tolerates comma, whitespace, or `#`
|
|
67
|
+
* prefixes. Empty cell yields an empty list. */
|
|
68
|
+
function parseTagsCell(cell) {
|
|
69
|
+
const tokens = cell.split(/[,\s]+/).map((token) => token.trim().replace(/^#/, "").toLowerCase()).filter((token) => token.length > 0);
|
|
70
|
+
return [...new Set(tokens)].sort();
|
|
71
|
+
}
|
|
72
|
+
/** Map header cell names → column indices, case- and whitespace-
|
|
73
|
+
* tolerant. Used by the row parser to locate the Tags column (and
|
|
74
|
+
* any other named column) without assuming a fixed position, so
|
|
75
|
+
* older 3- and 4-column tables keep working. */
|
|
76
|
+
function buildTableColumnMap(headerRow) {
|
|
77
|
+
const cells = headerRow.split("|").slice(1, -1).map((cell) => cell.trim().replace(/^`|`$/g, "").toLowerCase());
|
|
78
|
+
const map = /* @__PURE__ */ new Map();
|
|
79
|
+
cells.forEach((cell, i) => {
|
|
80
|
+
if (cell) map.set(cell, i);
|
|
81
|
+
});
|
|
82
|
+
return map;
|
|
83
|
+
}
|
|
84
|
+
/** Resolve the per-column indices the row parser needs. Falls back
|
|
85
|
+
* to positional defaults (0/1/2) when the table has no header map.
|
|
86
|
+
* "summary" is the canonical column name; "description" is accepted
|
|
87
|
+
* as a legacy alias used by older fixtures. */
|
|
88
|
+
function resolveTableColumnIndices(columnMap) {
|
|
89
|
+
return {
|
|
90
|
+
slug: columnMap?.get("slug") ?? 0,
|
|
91
|
+
title: columnMap?.get("title") ?? 1,
|
|
92
|
+
summary: columnMap?.get("summary") ?? columnMap?.get("description") ?? 2,
|
|
93
|
+
tags: columnMap?.get("tags")
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function parseTableRow(trimmed, columnMap) {
|
|
97
|
+
const cols = trimmed.split("|").slice(1, -1).map((column) => column.trim().replace(/^`|`$/g, ""));
|
|
98
|
+
if (cols.length < 2) return null;
|
|
99
|
+
const idx = resolveTableColumnIndices(columnMap);
|
|
100
|
+
const slug = cols[idx.slug] ?? "";
|
|
101
|
+
const title = cols[idx.title] || slug;
|
|
102
|
+
if (!slug || !title) return null;
|
|
103
|
+
return {
|
|
104
|
+
title,
|
|
105
|
+
slug,
|
|
106
|
+
description: cols[idx.summary] ?? "",
|
|
107
|
+
tags: idx.tags !== void 0 ? parseTagsCell(cols[idx.tags] ?? "") : []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Extract the slug segment from a bullet link's href. Accepts the
|
|
111
|
+
* canonical `pages/<slug>.md`, a bare `<slug>.md`, or just `<slug>`
|
|
112
|
+
* — the three forms produced by different historical writers of
|
|
113
|
+
* index.md. Returns "" for hrefs that don't look like a wiki page
|
|
114
|
+
* reference (e.g. `https://example.com`) so the caller can fall
|
|
115
|
+
* back to title-based slugification. */
|
|
116
|
+
function extractSlugFromBulletHref(rawHref) {
|
|
117
|
+
const href = rawHref.trim();
|
|
118
|
+
if (!href) return "";
|
|
119
|
+
if (/^[a-z]+:\/\//i.test(href)) return "";
|
|
120
|
+
return (href.split("/").pop() ?? href).replace(/\.md$/i, "");
|
|
121
|
+
}
|
|
122
|
+
function parseBulletLinkRow(trimmed) {
|
|
123
|
+
const match = BULLET_LINK_PATTERN.exec(trimmed);
|
|
124
|
+
if (!match) return null;
|
|
125
|
+
const title = match[1].trim();
|
|
126
|
+
const href = match[2] ?? "";
|
|
127
|
+
const { description, tags } = extractHashTags(match[3]?.trim() ?? "");
|
|
128
|
+
return {
|
|
129
|
+
title,
|
|
130
|
+
slug: extractSlugFromBulletHref(href) || require_slug.wikiSlugify(title),
|
|
131
|
+
description,
|
|
132
|
+
tags
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function parseBulletWikiLinkRow(trimmed) {
|
|
136
|
+
const match = BULLET_WIKI_LINK_PATTERN.exec(trimmed);
|
|
137
|
+
if (!match) return null;
|
|
138
|
+
const { target, display } = parseWikiLink(match[1]);
|
|
139
|
+
const title = (display || target).trim();
|
|
140
|
+
const slug = target ? require_slug.wikiSlugify(target) : require_slug.wikiSlugify(title);
|
|
141
|
+
const { description, tags } = extractHashTags(match[2]?.trim() ?? "");
|
|
142
|
+
return {
|
|
143
|
+
title,
|
|
144
|
+
slug,
|
|
145
|
+
description,
|
|
146
|
+
tags
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Parse entries from index.md. Supports three formats:
|
|
150
|
+
* 1. Table: `| slug | Title | Summary | (Tags) |`
|
|
151
|
+
* 2. Bullet link: `- [Title](pages/slug.md) — description`
|
|
152
|
+
* 3. Wiki link: `- [[Title]] — description`
|
|
153
|
+
*
|
|
154
|
+
* Returns entries in source order. Any unrecognised line is
|
|
155
|
+
* silently skipped — index.md is freeform markdown otherwise. */
|
|
156
|
+
function parseIndexEntries(content) {
|
|
157
|
+
const entries = [];
|
|
158
|
+
let inTable = false;
|
|
159
|
+
let columnMap = null;
|
|
160
|
+
for (const line of content.split("\n")) {
|
|
161
|
+
const trimmed = line.trim();
|
|
162
|
+
if (trimmed.startsWith("|")) {
|
|
163
|
+
if (TABLE_SEPARATOR_PATTERN.test(trimmed)) {
|
|
164
|
+
inTable = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!inTable) {
|
|
168
|
+
columnMap = buildTableColumnMap(trimmed);
|
|
169
|
+
inTable = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const entry = parseTableRow(trimmed, columnMap);
|
|
173
|
+
if (entry) entries.push(entry);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
inTable = false;
|
|
177
|
+
columnMap = null;
|
|
178
|
+
const bullet = parseBulletLinkRow(trimmed) ?? parseBulletWikiLinkRow(trimmed);
|
|
179
|
+
if (bullet) entries.push(bullet);
|
|
180
|
+
}
|
|
181
|
+
return entries;
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/wiki/lint.ts
|
|
185
|
+
/** Files on disk that aren't referenced by index.md. */
|
|
186
|
+
function findOrphanPages(fileSlugs, indexedSlugs) {
|
|
187
|
+
const issues = [];
|
|
188
|
+
for (const slug of fileSlugs) if (!indexedSlugs.has(slug)) issues.push(`- **Orphan page**: \`${slug}.md\` exists but is missing from index.md`);
|
|
189
|
+
return issues;
|
|
190
|
+
}
|
|
191
|
+
/** Slugs referenced by index.md that have no corresponding file. */
|
|
192
|
+
function findMissingFiles(pageEntries, fileSlugs) {
|
|
193
|
+
const issues = [];
|
|
194
|
+
for (const entry of pageEntries) if (!fileSlugs.has(entry.slug)) issues.push(`- **Missing file**: index.md references \`${entry.slug}\` but the file does not exist`);
|
|
195
|
+
return issues;
|
|
196
|
+
}
|
|
197
|
+
/** Walk a page's body for `[[…]]` links and flag any whose
|
|
198
|
+
* resolved slug doesn't exist in the file set.
|
|
199
|
+
*
|
|
200
|
+
* Critically: this routes through `parseWikiLink` so
|
|
201
|
+
* `[[slug|display]]` is split correctly — the lint slugifies the
|
|
202
|
+
* TARGET, not the full bracket body. Pre-#1297 the lint
|
|
203
|
+
* slugified the entire content (`slug|display`), which collapsed
|
|
204
|
+
* to a slug that always missed and produced ~168 false-positive
|
|
205
|
+
* "broken link" warnings. */
|
|
206
|
+
function findBrokenLinksInPage(fileName, content, fileSlugs) {
|
|
207
|
+
const issues = [];
|
|
208
|
+
const matches = [...content.matchAll(WIKI_LINK_PATTERN)];
|
|
209
|
+
for (const match of matches) {
|
|
210
|
+
const { target } = parseWikiLink(match[1]);
|
|
211
|
+
const linkSlug = require_slug.wikiSlugify(target);
|
|
212
|
+
if (linkSlug.length === 0) {
|
|
213
|
+
issues.push(`- **Broken link** in \`${fileName}\`: [[${match[1]}]] → empty target`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!fileSlugs.has(linkSlug)) issues.push(`- **Broken link** in \`${fileName}\`: [[${match[1]}]] → \`${linkSlug}.md\` not found`);
|
|
217
|
+
}
|
|
218
|
+
return issues;
|
|
219
|
+
}
|
|
220
|
+
function formatTagList(tags) {
|
|
221
|
+
return `[${[...tags].sort().join(", ")}]`;
|
|
222
|
+
}
|
|
223
|
+
/** Flag any slug whose index.md tags differ from the page's own
|
|
224
|
+
* frontmatter `tags:` field. Comparison is set-based and order-
|
|
225
|
+
* insensitive; both sides are lowercased at parse time. Slugs
|
|
226
|
+
* missing from `frontmatterTagsBySlug` are ignored here — the
|
|
227
|
+
* missing file itself is already reported by `findMissingFiles`. */
|
|
228
|
+
function findTagDrift(pageEntries, frontmatterTagsBySlug) {
|
|
229
|
+
const issues = [];
|
|
230
|
+
for (const entry of pageEntries) {
|
|
231
|
+
const pageTags = frontmatterTagsBySlug.get(entry.slug.toLowerCase());
|
|
232
|
+
if (pageTags === void 0) continue;
|
|
233
|
+
const pageSet = new Set(pageTags);
|
|
234
|
+
const indexSet = new Set(entry.tags);
|
|
235
|
+
if (pageSet.size !== indexSet.size || [...pageSet].some((tag) => !indexSet.has(tag))) issues.push(`- **Tag drift**: \`${entry.slug}.md\` frontmatter has ${formatTagList(pageTags)} but index.md has ${formatTagList(entry.tags)}`);
|
|
236
|
+
}
|
|
237
|
+
return issues;
|
|
238
|
+
}
|
|
239
|
+
/** Render the final markdown report from the accumulated issues
|
|
240
|
+
* list. Empty input yields the "wiki is healthy" sentinel so the
|
|
241
|
+
* caller can write the same file every time without branching on
|
|
242
|
+
* presence. */
|
|
243
|
+
function formatLintReport(issues) {
|
|
244
|
+
if (issues.length === 0) return "# Wiki Lint Report\n\n✓ No issues found. Wiki is healthy.";
|
|
245
|
+
const noun = `issue${issues.length !== 1 ? "s" : ""}`;
|
|
246
|
+
return `# Wiki Lint Report\n\n${issues.length} ${noun} found:\n\n${issues.join("\n")}`;
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/wiki/graph.ts
|
|
250
|
+
/** Resolve a raw `[[link]]` target to an existing page slug, or null.
|
|
251
|
+
* Mirrors the route resolver's strategy: slugify first, then fall
|
|
252
|
+
* back to matching an index entry title so non-ASCII targets like
|
|
253
|
+
* `[[さくらインターネット]]` resolve to their ASCII file slug. */
|
|
254
|
+
function resolveLinkTarget(target, fileSlugs, slugByTitle) {
|
|
255
|
+
const slug = require_slug.wikiSlugify(target);
|
|
256
|
+
if (slug.length > 0 && fileSlugs.has(slug)) return slug;
|
|
257
|
+
const byTitle = slugByTitle.get(target.trim());
|
|
258
|
+
if (byTitle !== void 0 && fileSlugs.has(byTitle)) return byTitle;
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
/** Resolved, deduped outgoing slugs for one page body. Self-links and
|
|
262
|
+
* links to non-existent pages are dropped (the latter are already a
|
|
263
|
+
* lint "broken link", not a graph edge). */
|
|
264
|
+
function pageOutgoingSlugs(fromSlug, content, fileSlugs, slugByTitle) {
|
|
265
|
+
const out = /* @__PURE__ */ new Set();
|
|
266
|
+
for (const match of content.matchAll(WIKI_LINK_PATTERN)) {
|
|
267
|
+
const { target } = parseWikiLink(match[1]);
|
|
268
|
+
const resolved = resolveLinkTarget(target, fileSlugs, slugByTitle);
|
|
269
|
+
if (resolved !== null && resolved !== fromSlug) out.add(resolved);
|
|
270
|
+
}
|
|
271
|
+
return [...out];
|
|
272
|
+
}
|
|
273
|
+
function buildTitleMaps(entries) {
|
|
274
|
+
const titleBySlug = /* @__PURE__ */ new Map();
|
|
275
|
+
const slugByTitle = /* @__PURE__ */ new Map();
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
if (!titleBySlug.has(entry.slug)) titleBySlug.set(entry.slug, entry.title);
|
|
278
|
+
if (entry.title.length > 0 && !slugByTitle.has(entry.title)) slugByTitle.set(entry.title, entry.slug);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
titleBySlug,
|
|
282
|
+
slugByTitle
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** Build the full page→page graph. Nodes are the existing page files
|
|
286
|
+
* (titled from index.md, falling back to the slug for un-indexed
|
|
287
|
+
* pages); edges are the resolved `[[links]]`, deduped per (from,to). */
|
|
288
|
+
function buildWikiGraph(pages, entries) {
|
|
289
|
+
const fileSlugs = new Set(pages.map((page) => page.slug));
|
|
290
|
+
const { titleBySlug, slugByTitle } = buildTitleMaps(entries);
|
|
291
|
+
const nodes = pages.map((page) => ({
|
|
292
|
+
slug: page.slug,
|
|
293
|
+
title: titleBySlug.get(page.slug) ?? page.slug
|
|
294
|
+
}));
|
|
295
|
+
const edges = [];
|
|
296
|
+
const seen = /* @__PURE__ */ new Set();
|
|
297
|
+
for (const page of pages) for (const toSlug of pageOutgoingSlugs(page.slug, page.content, fileSlugs, slugByTitle)) {
|
|
298
|
+
const key = [page.slug, toSlug].join("\n");
|
|
299
|
+
if (seen.has(key)) continue;
|
|
300
|
+
seen.add(key);
|
|
301
|
+
edges.push({
|
|
302
|
+
from: page.slug,
|
|
303
|
+
to: toSlug
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
nodes,
|
|
308
|
+
edges
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Pages that link TO `slug` (incoming edges), deduped, as nodes. */
|
|
312
|
+
function incomingLinks(graph, slug) {
|
|
313
|
+
const nodeBySlug = new Map(graph.nodes.map((node) => [node.slug, node]));
|
|
314
|
+
const result = [];
|
|
315
|
+
const seen = /* @__PURE__ */ new Set();
|
|
316
|
+
for (const edge of graph.edges) {
|
|
317
|
+
if (edge.to !== slug || seen.has(edge.from)) continue;
|
|
318
|
+
seen.add(edge.from);
|
|
319
|
+
const node = nodeBySlug.get(edge.from);
|
|
320
|
+
if (node) result.push(node);
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/wiki/route.ts
|
|
326
|
+
var WIKI_ROUTE_SECTION = {
|
|
327
|
+
pages: "pages",
|
|
328
|
+
log: "log",
|
|
329
|
+
lintReport: "lint-report",
|
|
330
|
+
graph: "graph"
|
|
331
|
+
};
|
|
332
|
+
var WIKI_ACTION = {
|
|
333
|
+
index: "index",
|
|
334
|
+
page: "page",
|
|
335
|
+
log: "log",
|
|
336
|
+
lintReport: "lint_report",
|
|
337
|
+
graph: "graph",
|
|
338
|
+
save: "save"
|
|
339
|
+
};
|
|
340
|
+
function isSafeWikiSlug(value) {
|
|
341
|
+
return typeof value === "string" && require_slug.isSafeSlug(value);
|
|
342
|
+
}
|
|
343
|
+
function readWikiRouteTarget(params) {
|
|
344
|
+
if (!params || typeof params !== "object") return null;
|
|
345
|
+
const { section, slug } = params;
|
|
346
|
+
if (section === void 0 || section === "") return { kind: "index" };
|
|
347
|
+
if (section === WIKI_ROUTE_SECTION.pages) {
|
|
348
|
+
if (!isSafeWikiSlug(slug)) return null;
|
|
349
|
+
return {
|
|
350
|
+
kind: "page",
|
|
351
|
+
slug
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (section === WIKI_ROUTE_SECTION.log) return { kind: "log" };
|
|
355
|
+
if (section === WIKI_ROUTE_SECTION.lintReport) return { kind: "lint_report" };
|
|
356
|
+
if (section === WIKI_ROUTE_SECTION.graph) return { kind: "graph" };
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
function buildWikiRouteParams(target) {
|
|
360
|
+
switch (target.kind) {
|
|
361
|
+
case "index": return {
|
|
362
|
+
section: "",
|
|
363
|
+
slug: ""
|
|
364
|
+
};
|
|
365
|
+
case "page": return {
|
|
366
|
+
section: WIKI_ROUTE_SECTION.pages,
|
|
367
|
+
slug: target.slug
|
|
368
|
+
};
|
|
369
|
+
case "log": return {
|
|
370
|
+
section: WIKI_ROUTE_SECTION.log,
|
|
371
|
+
slug: ""
|
|
372
|
+
};
|
|
373
|
+
case "lint_report": return {
|
|
374
|
+
section: WIKI_ROUTE_SECTION.lintReport,
|
|
375
|
+
slug: ""
|
|
376
|
+
};
|
|
377
|
+
case "graph": return {
|
|
378
|
+
section: WIKI_ROUTE_SECTION.graph,
|
|
379
|
+
slug: ""
|
|
380
|
+
};
|
|
381
|
+
default: throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(target)}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function wikiActionFor(target) {
|
|
385
|
+
switch (target.kind) {
|
|
386
|
+
case "index": return WIKI_ACTION.index;
|
|
387
|
+
case "page": return WIKI_ACTION.page;
|
|
388
|
+
case "log": return WIKI_ACTION.log;
|
|
389
|
+
case "lint_report": return WIKI_ACTION.lintReport;
|
|
390
|
+
case "graph": return WIKI_ACTION.graph;
|
|
391
|
+
default: throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(target)}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/wiki/render.ts
|
|
396
|
+
/** HTML-escape attribute / text content. Self-contained on purpose:
|
|
397
|
+
* this package can't reach the host's escaper, and the rule is a
|
|
398
|
+
* fixed five-char map. */
|
|
399
|
+
function escapeHtml(value) {
|
|
400
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
401
|
+
switch (char) {
|
|
402
|
+
case "&": return "&";
|
|
403
|
+
case "<": return "<";
|
|
404
|
+
case ">": return ">";
|
|
405
|
+
case "\"": return """;
|
|
406
|
+
default: return "'";
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Replace every `[[page name]]` occurrence in `content` with a
|
|
412
|
+
* `<span class="wiki-link" data-page="…">…</span>` element. The
|
|
413
|
+
* page name may not contain `]`; an opening `[[` that is not
|
|
414
|
+
* followed later by `]]` (with no bare `]` in between) is left
|
|
415
|
+
* untouched so malformed text renders as-is — matching the
|
|
416
|
+
* previous regex's non-match behaviour.
|
|
417
|
+
*
|
|
418
|
+
* `[[target|display]]` is split via the shared `parseWikiLink`
|
|
419
|
+
* helper so `data-page` carries only the target slug while the
|
|
420
|
+
* visible text shows the display half (#1297). Both halves are
|
|
421
|
+
* HTML-escaped before interpolation — `parseWikiLink` runs BEFORE
|
|
422
|
+
* the host's `marked.parse`, so escaping has to happen here.
|
|
423
|
+
*/
|
|
424
|
+
function renderWikiLinks(content) {
|
|
425
|
+
const out = [];
|
|
426
|
+
let i = 0;
|
|
427
|
+
while (i < content.length) {
|
|
428
|
+
if (content[i] === "[" && content[i + 1] === "[") {
|
|
429
|
+
const closeStart = findNextCloseBrackets(content, i + 2);
|
|
430
|
+
if (closeStart !== -1) {
|
|
431
|
+
const { target, display } = parseWikiLink(content.slice(i + 2, closeStart));
|
|
432
|
+
out.push(`<span class="wiki-link" data-page="${escapeHtml(target)}">${escapeHtml(display)}</span>`);
|
|
433
|
+
i = closeStart + 2;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
out.push(content[i]);
|
|
438
|
+
i++;
|
|
439
|
+
}
|
|
440
|
+
return out.join("");
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Starting at `from`, scan forward for a `]]` sequence. Returns
|
|
444
|
+
* the index of the first `]` of that pair, or -1 if a bare `]`
|
|
445
|
+
* (one not immediately followed by a second `]`) is encountered
|
|
446
|
+
* first — mirroring the old regex's `[^\]]+` constraint that the
|
|
447
|
+
* page name must contain no `]` characters. Also returns -1 if
|
|
448
|
+
* nothing matched before the end of input, or if the pair sits
|
|
449
|
+
* immediately after `from` (zero-length page name, which the old
|
|
450
|
+
* regex rejected via the `+` quantifier).
|
|
451
|
+
*/
|
|
452
|
+
function findNextCloseBrackets(str, from) {
|
|
453
|
+
let j = from;
|
|
454
|
+
while (j < str.length) {
|
|
455
|
+
if (str[j] === "]") {
|
|
456
|
+
if (str[j + 1] === "]" && j > from) return j;
|
|
457
|
+
return -1;
|
|
458
|
+
}
|
|
459
|
+
j++;
|
|
460
|
+
}
|
|
461
|
+
return -1;
|
|
462
|
+
}
|
|
463
|
+
//#endregion
|
|
464
|
+
exports.BULLET_LINK_PATTERN = BULLET_LINK_PATTERN;
|
|
465
|
+
exports.BULLET_WIKI_LINK_PATTERN = BULLET_WIKI_LINK_PATTERN;
|
|
466
|
+
exports.WIKI_ACTION = WIKI_ACTION;
|
|
467
|
+
exports.WIKI_LINK_PATTERN = WIKI_LINK_PATTERN;
|
|
468
|
+
exports.WIKI_ROUTE_SECTION = WIKI_ROUTE_SECTION;
|
|
469
|
+
exports.buildTableColumnMap = buildTableColumnMap;
|
|
470
|
+
exports.buildWikiGraph = buildWikiGraph;
|
|
471
|
+
exports.buildWikiRouteParams = buildWikiRouteParams;
|
|
472
|
+
exports.escapeHtml = escapeHtml;
|
|
473
|
+
exports.extractHashTags = extractHashTags;
|
|
474
|
+
exports.extractSlugFromBulletHref = extractSlugFromBulletHref;
|
|
475
|
+
exports.findBrokenLinksInPage = findBrokenLinksInPage;
|
|
476
|
+
exports.findMissingFiles = findMissingFiles;
|
|
477
|
+
exports.findOrphanPages = findOrphanPages;
|
|
478
|
+
exports.findTagDrift = findTagDrift;
|
|
479
|
+
exports.formatLintReport = formatLintReport;
|
|
480
|
+
exports.incomingLinks = incomingLinks;
|
|
481
|
+
exports.isSafeSlug = require_slug.isSafeSlug;
|
|
482
|
+
exports.isSafeWikiSlug = isSafeWikiSlug;
|
|
483
|
+
exports.pageOutgoingSlugs = pageOutgoingSlugs;
|
|
484
|
+
exports.parseIndexEntries = parseIndexEntries;
|
|
485
|
+
exports.parseTagsCell = parseTagsCell;
|
|
486
|
+
exports.parseWikiLink = parseWikiLink;
|
|
487
|
+
exports.readWikiRouteTarget = readWikiRouteTarget;
|
|
488
|
+
exports.renderWikiLinks = renderWikiLinks;
|
|
489
|
+
exports.resolveLinkTarget = resolveLinkTarget;
|
|
490
|
+
exports.wikiActionFor = wikiActionFor;
|
|
491
|
+
exports.wikiSlugify = require_slug.wikiSlugify;
|
|
492
|
+
|
|
493
|
+
//# sourceMappingURL=index.cjs.map
|