@mulmoclaude/core 0.2.16 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/graph-2R4HbQAg.cjs +433 -0
- package/dist/graph-2R4HbQAg.cjs.map +1 -0
- package/dist/graph-o5FZZbXf.js +326 -0
- package/dist/graph-o5FZZbXf.js.map +1 -0
- 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 +172 -0
- package/dist/wiki/index.cjs.map +1 -0
- package/dist/wiki/index.d.ts +7 -0
- package/dist/wiki/index.js +144 -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/paths.cjs +42 -0
- package/dist/wiki/paths.cjs.map +1 -0
- package/dist/wiki/paths.js +38 -0
- package/dist/wiki/paths.js.map +1 -0
- package/dist/wiki/render.d.ts +19 -0
- package/dist/wiki/route.d.ts +32 -0
- package/dist/wiki/server/engine.d.ts +39 -0
- package/dist/wiki/server/frontmatter.d.ts +9 -0
- package/dist/wiki/server/fs.d.ts +5 -0
- package/dist/wiki/server/index.cjs +250 -0
- package/dist/wiki/server/index.cjs.map +1 -0
- package/dist/wiki/server/index.d.ts +4 -0
- package/dist/wiki/server/index.js +235 -0
- package/dist/wiki/server/index.js.map +1 -0
- package/dist/wiki/server/pageIndex.d.ts +13 -0
- package/dist/wiki/server/paths.d.ts +19 -0
- package/dist/wiki/slug.d.ts +24 -0
- package/package.json +20 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_graph = require("../graph-2R4HbQAg.cjs");
|
|
3
|
+
const require_slug = require("../slug-DbwORN9z.cjs");
|
|
4
|
+
//#region src/wiki/route.ts
|
|
5
|
+
var WIKI_ROUTE_SECTION = {
|
|
6
|
+
pages: "pages",
|
|
7
|
+
log: "log",
|
|
8
|
+
lintReport: "lint-report",
|
|
9
|
+
graph: "graph"
|
|
10
|
+
};
|
|
11
|
+
var WIKI_ACTION = {
|
|
12
|
+
index: "index",
|
|
13
|
+
page: "page",
|
|
14
|
+
log: "log",
|
|
15
|
+
lintReport: "lint_report",
|
|
16
|
+
graph: "graph",
|
|
17
|
+
save: "save"
|
|
18
|
+
};
|
|
19
|
+
function isSafeWikiSlug(value) {
|
|
20
|
+
return typeof value === "string" && require_slug.isSafeSlug(value);
|
|
21
|
+
}
|
|
22
|
+
function readWikiRouteTarget(params) {
|
|
23
|
+
if (!params || typeof params !== "object") return null;
|
|
24
|
+
const { section, slug } = params;
|
|
25
|
+
if (section === void 0 || section === "") return { kind: "index" };
|
|
26
|
+
if (section === WIKI_ROUTE_SECTION.pages) {
|
|
27
|
+
if (!isSafeWikiSlug(slug)) return null;
|
|
28
|
+
return {
|
|
29
|
+
kind: "page",
|
|
30
|
+
slug
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (section === WIKI_ROUTE_SECTION.log) return { kind: "log" };
|
|
34
|
+
if (section === WIKI_ROUTE_SECTION.lintReport) return { kind: "lint_report" };
|
|
35
|
+
if (section === WIKI_ROUTE_SECTION.graph) return { kind: "graph" };
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function buildWikiRouteParams(target) {
|
|
39
|
+
switch (target.kind) {
|
|
40
|
+
case "index": return {
|
|
41
|
+
section: "",
|
|
42
|
+
slug: ""
|
|
43
|
+
};
|
|
44
|
+
case "page": return {
|
|
45
|
+
section: WIKI_ROUTE_SECTION.pages,
|
|
46
|
+
slug: target.slug
|
|
47
|
+
};
|
|
48
|
+
case "log": return {
|
|
49
|
+
section: WIKI_ROUTE_SECTION.log,
|
|
50
|
+
slug: ""
|
|
51
|
+
};
|
|
52
|
+
case "lint_report": return {
|
|
53
|
+
section: WIKI_ROUTE_SECTION.lintReport,
|
|
54
|
+
slug: ""
|
|
55
|
+
};
|
|
56
|
+
case "graph": return {
|
|
57
|
+
section: WIKI_ROUTE_SECTION.graph,
|
|
58
|
+
slug: ""
|
|
59
|
+
};
|
|
60
|
+
default: throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(target)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function wikiActionFor(target) {
|
|
64
|
+
switch (target.kind) {
|
|
65
|
+
case "index": return WIKI_ACTION.index;
|
|
66
|
+
case "page": return WIKI_ACTION.page;
|
|
67
|
+
case "log": return WIKI_ACTION.log;
|
|
68
|
+
case "lint_report": return WIKI_ACTION.lintReport;
|
|
69
|
+
case "graph": return WIKI_ACTION.graph;
|
|
70
|
+
default: throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(target)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/wiki/render.ts
|
|
75
|
+
/** HTML-escape attribute / text content. Self-contained on purpose:
|
|
76
|
+
* this package can't reach the host's escaper, and the rule is a
|
|
77
|
+
* fixed five-char map. */
|
|
78
|
+
function escapeHtml(value) {
|
|
79
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
80
|
+
switch (char) {
|
|
81
|
+
case "&": return "&";
|
|
82
|
+
case "<": return "<";
|
|
83
|
+
case ">": return ">";
|
|
84
|
+
case "\"": return """;
|
|
85
|
+
default: return "'";
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Replace every `[[page name]]` occurrence in `content` with a
|
|
91
|
+
* `<span class="wiki-link" data-page="…">…</span>` element. The
|
|
92
|
+
* page name may not contain `]`; an opening `[[` that is not
|
|
93
|
+
* followed later by `]]` (with no bare `]` in between) is left
|
|
94
|
+
* untouched so malformed text renders as-is — matching the
|
|
95
|
+
* previous regex's non-match behaviour.
|
|
96
|
+
*
|
|
97
|
+
* `[[target|display]]` is split via the shared `parseWikiLink`
|
|
98
|
+
* helper so `data-page` carries only the target slug while the
|
|
99
|
+
* visible text shows the display half (#1297). Both halves are
|
|
100
|
+
* HTML-escaped before interpolation — `parseWikiLink` runs BEFORE
|
|
101
|
+
* the host's `marked.parse`, so escaping has to happen here.
|
|
102
|
+
*/
|
|
103
|
+
function renderWikiLinks(content) {
|
|
104
|
+
const out = [];
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < content.length) {
|
|
107
|
+
if (content[i] === "[" && content[i + 1] === "[") {
|
|
108
|
+
const closeStart = findNextCloseBrackets(content, i + 2);
|
|
109
|
+
if (closeStart !== -1) {
|
|
110
|
+
const { target, display } = require_graph.parseWikiLink(content.slice(i + 2, closeStart));
|
|
111
|
+
out.push(`<span class="wiki-link" data-page="${escapeHtml(target)}">${escapeHtml(display)}</span>`);
|
|
112
|
+
i = closeStart + 2;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
out.push(content[i]);
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
return out.join("");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Starting at `from`, scan forward for a `]]` sequence. Returns
|
|
123
|
+
* the index of the first `]` of that pair, or -1 if a bare `]`
|
|
124
|
+
* (one not immediately followed by a second `]`) is encountered
|
|
125
|
+
* first — mirroring the old regex's `[^\]]+` constraint that the
|
|
126
|
+
* page name must contain no `]` characters. Also returns -1 if
|
|
127
|
+
* nothing matched before the end of input, or if the pair sits
|
|
128
|
+
* immediately after `from` (zero-length page name, which the old
|
|
129
|
+
* regex rejected via the `+` quantifier).
|
|
130
|
+
*/
|
|
131
|
+
function findNextCloseBrackets(str, from) {
|
|
132
|
+
let j = from;
|
|
133
|
+
while (j < str.length) {
|
|
134
|
+
if (str[j] === "]") {
|
|
135
|
+
if (str[j + 1] === "]" && j > from) return j;
|
|
136
|
+
return -1;
|
|
137
|
+
}
|
|
138
|
+
j++;
|
|
139
|
+
}
|
|
140
|
+
return -1;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
exports.BULLET_LINK_PATTERN = require_graph.BULLET_LINK_PATTERN;
|
|
144
|
+
exports.BULLET_WIKI_LINK_PATTERN = require_graph.BULLET_WIKI_LINK_PATTERN;
|
|
145
|
+
exports.WIKI_ACTION = WIKI_ACTION;
|
|
146
|
+
exports.WIKI_LINK_PATTERN = require_graph.WIKI_LINK_PATTERN;
|
|
147
|
+
exports.WIKI_ROUTE_SECTION = WIKI_ROUTE_SECTION;
|
|
148
|
+
exports.buildTableColumnMap = require_graph.buildTableColumnMap;
|
|
149
|
+
exports.buildWikiGraph = require_graph.buildWikiGraph;
|
|
150
|
+
exports.buildWikiRouteParams = buildWikiRouteParams;
|
|
151
|
+
exports.escapeHtml = escapeHtml;
|
|
152
|
+
exports.extractHashTags = require_graph.extractHashTags;
|
|
153
|
+
exports.extractSlugFromBulletHref = require_graph.extractSlugFromBulletHref;
|
|
154
|
+
exports.findBrokenLinksInPage = require_graph.findBrokenLinksInPage;
|
|
155
|
+
exports.findMissingFiles = require_graph.findMissingFiles;
|
|
156
|
+
exports.findOrphanPages = require_graph.findOrphanPages;
|
|
157
|
+
exports.findTagDrift = require_graph.findTagDrift;
|
|
158
|
+
exports.formatLintReport = require_graph.formatLintReport;
|
|
159
|
+
exports.incomingLinks = require_graph.incomingLinks;
|
|
160
|
+
exports.isSafeSlug = require_slug.isSafeSlug;
|
|
161
|
+
exports.isSafeWikiSlug = isSafeWikiSlug;
|
|
162
|
+
exports.pageOutgoingSlugs = require_graph.pageOutgoingSlugs;
|
|
163
|
+
exports.parseIndexEntries = require_graph.parseIndexEntries;
|
|
164
|
+
exports.parseTagsCell = require_graph.parseTagsCell;
|
|
165
|
+
exports.parseWikiLink = require_graph.parseWikiLink;
|
|
166
|
+
exports.readWikiRouteTarget = readWikiRouteTarget;
|
|
167
|
+
exports.renderWikiLinks = renderWikiLinks;
|
|
168
|
+
exports.resolveLinkTarget = require_graph.resolveLinkTarget;
|
|
169
|
+
exports.wikiActionFor = wikiActionFor;
|
|
170
|
+
exports.wikiSlugify = require_slug.wikiSlugify;
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/wiki/route.ts","../../src/wiki/render.ts"],"sourcesContent":["// Pure helpers for reading, building, and validating wiki route\n// params. Kept free of Vue / vue-router imports so it can be shared\n// across hosts (MulmoClaude + MulmoTerminal) from\n// `@mulmoclaude/core/wiki`:\n//\n// - the host router guard (synchronous validation at navigation time)\n// - the wiki View (watcher + push helpers)\n// - the workspace-link click handler\n// - unit tests\n//\n// Mirrors the \"one file owns the URL ↔ domain mapping\" pattern so the\n// literals don't drift across the router definition, guards, views,\n// and tests.\n\nimport { isSafeSlug } from \"./slug.js\";\n\n// URL segment used in `/wiki/:section/...`. Closed enum — the router\n// regex `(pages|log|lint-report)` rejects anything else.\nexport const WIKI_ROUTE_SECTION = {\n pages: \"pages\",\n log: \"log\",\n lintReport: \"lint-report\",\n graph: \"graph\",\n} as const;\n\nexport type WikiRouteSection = (typeof WIKI_ROUTE_SECTION)[keyof typeof WIKI_ROUTE_SECTION];\n\n// Internal action name sent to the server / shown in the View. Diverges\n// from the URL segment in one place only: `lint-report` (URL) vs\n// `lint_report` (action), because the server API still speaks the\n// underscore form.\nexport const WIKI_ACTION = {\n index: \"index\",\n page: \"page\",\n log: \"log\",\n lintReport: \"lint_report\",\n graph: \"graph\",\n save: \"save\",\n} as const;\n\nexport type WikiAction = (typeof WIKI_ACTION)[keyof typeof WIKI_ACTION];\n\n// Route-level representation. `pushWiki(target)` and\n// `readWikiRouteTarget(params)` both speak this so the watcher, the\n// button handlers, and the router guard agree on the same shape.\nexport type WikiTarget = { kind: \"index\" } | { kind: \"page\"; slug: string } | { kind: \"log\" } | { kind: \"lint_report\" } | { kind: \"graph\" };\n\n// Reject anything that could escape `data/wiki/pages/` or collide\n// with a different page. Vue Router decodes `%2F` back to `/` in\n// `route.params.slug`, so `/wiki/pages/..%2Fsecrets` lands here as\n// `slug === \"../secrets\"` — this check is the last line of defence\n// before the slug is passed to the server's page resolver. Non-\n// ASCII characters (e.g. Japanese page titles) are allowed; only\n// separators and the literal `.` / `..` are blocked.\n//\n// Delegates the actual rule to `isSafeSlug` in\n// `src/lib/wiki-page/slug.ts` (imported at the top of this file)\n// so server and frontend share one implementation (#1297). The\n// wrapper adds the type-guard / non-string-input handling the\n// router needs.\nexport function isSafeWikiSlug(value: unknown): value is string {\n return typeof value === \"string\" && isSafeSlug(value);\n}\n\n// Read `route.params` from the wiki route and normalise to a\n// `WikiTarget`. Returns `null` when the params describe an invalid\n// state (unknown section, missing slug for a page view, unsafe slug)\n// so the caller can decide what to do — the router guard redirects to\n// `/wiki`, the view watcher treats it as \"render the index\".\nexport function readWikiRouteTarget(params: unknown): WikiTarget | null {\n if (!params || typeof params !== \"object\") return null;\n const { section, slug } = params as { section?: unknown; slug?: unknown };\n\n if (section === undefined || section === \"\") return { kind: \"index\" };\n\n if (section === WIKI_ROUTE_SECTION.pages) {\n if (!isSafeWikiSlug(slug)) return null;\n return { kind: \"page\", slug };\n }\n if (section === WIKI_ROUTE_SECTION.log) return { kind: \"log\" };\n if (section === WIKI_ROUTE_SECTION.lintReport) return { kind: \"lint_report\" };\n if (section === WIKI_ROUTE_SECTION.graph) return { kind: \"graph\" };\n\n return null;\n}\n\n// Inverse of `readWikiRouteTarget`: given a target, produce the\n// `{ section, slug }` params object that `router.push({ name: \"wiki\",\n// params })` needs.\n//\n// Optional route params are NOT cleared by named-route navigation\n// unless explicitly set — returning `{}` for `kind: \"index\"` would\n// leak the previous `section`/`slug` into the URL when navigating\n// from `/wiki/pages/foo` back to the index, leaving the user on\n// `/wiki/pages/foo` with the View believing it's the index. Pass\n// empty strings so the router writes out `/wiki` cleanly. The\n// readWikiRouteTarget() branch for `section === \"\"` already\n// normalises those back to `{ kind: \"index\" }`.\nexport function buildWikiRouteParams(target: WikiTarget): Record<string, string> {\n switch (target.kind) {\n case \"index\":\n return { section: \"\", slug: \"\" };\n case \"page\":\n return { section: WIKI_ROUTE_SECTION.pages, slug: target.slug };\n case \"log\":\n return { section: WIKI_ROUTE_SECTION.log, slug: \"\" };\n case \"lint_report\":\n return { section: WIKI_ROUTE_SECTION.lintReport, slug: \"\" };\n case \"graph\":\n return { section: WIKI_ROUTE_SECTION.graph, slug: \"\" };\n default: {\n const exhaustive: never = target;\n throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n// Resolve a target to the action name the server expects. Centralises\n// the one place URL-shape and action-shape diverge (`lint-report` ↔\n// `lint_report`).\nexport function wikiActionFor(target: WikiTarget): WikiAction {\n switch (target.kind) {\n case \"index\":\n return WIKI_ACTION.index;\n case \"page\":\n return WIKI_ACTION.page;\n case \"log\":\n return WIKI_ACTION.log;\n case \"lint_report\":\n return WIKI_ACTION.lintReport;\n case \"graph\":\n return WIKI_ACTION.graph;\n default: {\n const exhaustive: never = target;\n throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n","// Pure `[[wiki-link]]` → HTML renderer, shared across hosts via\n// `@mulmoclaude/core/wiki`. Extracted from the MulmoClaude host's\n// `src/plugins/wiki/helpers.ts` so MulmoTerminal can render the same\n// internal-link markup without forking the walker.\n//\n// String-only, no `marked` / DOM / Node deps — the host pipeline\n// (image-ref rewrite, marked.parse, task-interactive) wraps this.\n\nimport { parseWikiLink } from \"./link.js\";\n\n/** HTML-escape attribute / text content. Self-contained on purpose:\n * this package can't reach the host's escaper, and the rule is a\n * fixed five-char map. */\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => {\n switch (char) {\n case \"&\":\n return \"&\";\n case \"<\":\n return \"<\";\n case \">\":\n return \">\";\n case '\"':\n return \""\";\n default:\n return \"'\";\n }\n });\n}\n\n/**\n * Replace every `[[page name]]` occurrence in `content` with a\n * `<span class=\"wiki-link\" data-page=\"…\">…</span>` element. The\n * page name may not contain `]`; an opening `[[` that is not\n * followed later by `]]` (with no bare `]` in between) is left\n * untouched so malformed text renders as-is — matching the\n * previous regex's non-match behaviour.\n *\n * `[[target|display]]` is split via the shared `parseWikiLink`\n * helper so `data-page` carries only the target slug while the\n * visible text shows the display half (#1297). Both halves are\n * HTML-escaped before interpolation — `parseWikiLink` runs BEFORE\n * the host's `marked.parse`, so escaping has to happen here.\n */\nexport function renderWikiLinks(content: string): string {\n const out: string[] = [];\n let i = 0;\n while (i < content.length) {\n if (content[i] === \"[\" && content[i + 1] === \"[\") {\n const closeStart = findNextCloseBrackets(content, i + 2);\n if (closeStart !== -1) {\n const inner = content.slice(i + 2, closeStart);\n const { target, display } = parseWikiLink(inner);\n out.push(`<span class=\"wiki-link\" data-page=\"${escapeHtml(target)}\">${escapeHtml(display)}</span>`);\n i = closeStart + 2;\n continue;\n }\n }\n out.push(content[i]);\n i++;\n }\n return out.join(\"\");\n}\n\n/**\n * Starting at `from`, scan forward for a `]]` sequence. Returns\n * the index of the first `]` of that pair, or -1 if a bare `]`\n * (one not immediately followed by a second `]`) is encountered\n * first — mirroring the old regex's `[^\\]]+` constraint that the\n * page name must contain no `]` characters. Also returns -1 if\n * nothing matched before the end of input, or if the pair sits\n * immediately after `from` (zero-length page name, which the old\n * regex rejected via the `+` quantifier).\n */\nfunction findNextCloseBrackets(str: string, from: number): number {\n let j = from;\n while (j < str.length) {\n if (str[j] === \"]\") {\n if (str[j + 1] === \"]\" && j > from) return j;\n // Bare `]` inside the page-name span — old regex would not\n // match here, so we bail and let the caller emit the `[[`\n // as literal text.\n return -1;\n }\n j++;\n }\n return -1;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { WIKI_LINK_PATTERN, parseWikiLink, type WikiLink } from './link.js';
|
|
2
|
+
export { isSafeSlug, wikiSlugify } from './slug.js';
|
|
3
|
+
export { type WikiPageEntry, BULLET_LINK_PATTERN, BULLET_WIKI_LINK_PATTERN, buildTableColumnMap, extractHashTags, extractSlugFromBulletHref, parseIndexEntries, parseTagsCell, } from './index-parse.js';
|
|
4
|
+
export { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift, formatLintReport } from './lint.js';
|
|
5
|
+
export { type WikiGraph, type WikiGraphNode, type WikiGraphEdge, type WikiPageContent, buildWikiGraph, incomingLinks, pageOutgoingSlugs, resolveLinkTarget, } from './graph.js';
|
|
6
|
+
export { type WikiAction, type WikiRouteSection, type WikiTarget, WIKI_ACTION, WIKI_ROUTE_SECTION, buildWikiRouteParams, isSafeWikiSlug, readWikiRouteTarget, wikiActionFor, } from './route.js';
|
|
7
|
+
export { escapeHtml, renderWikiLinks } from './render.js';
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { _ as WIKI_LINK_PATTERN, a as findBrokenLinksInPage, c as findTagDrift, d as BULLET_WIKI_LINK_PATTERN, f as buildTableColumnMap, g as parseTagsCell, h as parseIndexEntries, i as resolveLinkTarget, l as formatLintReport, m as extractSlugFromBulletHref, n as incomingLinks, o as findMissingFiles, p as extractHashTags, r as pageOutgoingSlugs, s as findOrphanPages, t as buildWikiGraph, u as BULLET_LINK_PATTERN, v as parseWikiLink } from "../graph-o5FZZbXf.js";
|
|
2
|
+
import { n as wikiSlugify, t as isSafeSlug } from "../slug-CdN-pQX1.js";
|
|
3
|
+
//#region src/wiki/route.ts
|
|
4
|
+
var WIKI_ROUTE_SECTION = {
|
|
5
|
+
pages: "pages",
|
|
6
|
+
log: "log",
|
|
7
|
+
lintReport: "lint-report",
|
|
8
|
+
graph: "graph"
|
|
9
|
+
};
|
|
10
|
+
var WIKI_ACTION = {
|
|
11
|
+
index: "index",
|
|
12
|
+
page: "page",
|
|
13
|
+
log: "log",
|
|
14
|
+
lintReport: "lint_report",
|
|
15
|
+
graph: "graph",
|
|
16
|
+
save: "save"
|
|
17
|
+
};
|
|
18
|
+
function isSafeWikiSlug(value) {
|
|
19
|
+
return typeof value === "string" && isSafeSlug(value);
|
|
20
|
+
}
|
|
21
|
+
function readWikiRouteTarget(params) {
|
|
22
|
+
if (!params || typeof params !== "object") return null;
|
|
23
|
+
const { section, slug } = params;
|
|
24
|
+
if (section === void 0 || section === "") return { kind: "index" };
|
|
25
|
+
if (section === WIKI_ROUTE_SECTION.pages) {
|
|
26
|
+
if (!isSafeWikiSlug(slug)) return null;
|
|
27
|
+
return {
|
|
28
|
+
kind: "page",
|
|
29
|
+
slug
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (section === WIKI_ROUTE_SECTION.log) return { kind: "log" };
|
|
33
|
+
if (section === WIKI_ROUTE_SECTION.lintReport) return { kind: "lint_report" };
|
|
34
|
+
if (section === WIKI_ROUTE_SECTION.graph) return { kind: "graph" };
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function buildWikiRouteParams(target) {
|
|
38
|
+
switch (target.kind) {
|
|
39
|
+
case "index": return {
|
|
40
|
+
section: "",
|
|
41
|
+
slug: ""
|
|
42
|
+
};
|
|
43
|
+
case "page": return {
|
|
44
|
+
section: WIKI_ROUTE_SECTION.pages,
|
|
45
|
+
slug: target.slug
|
|
46
|
+
};
|
|
47
|
+
case "log": return {
|
|
48
|
+
section: WIKI_ROUTE_SECTION.log,
|
|
49
|
+
slug: ""
|
|
50
|
+
};
|
|
51
|
+
case "lint_report": return {
|
|
52
|
+
section: WIKI_ROUTE_SECTION.lintReport,
|
|
53
|
+
slug: ""
|
|
54
|
+
};
|
|
55
|
+
case "graph": return {
|
|
56
|
+
section: WIKI_ROUTE_SECTION.graph,
|
|
57
|
+
slug: ""
|
|
58
|
+
};
|
|
59
|
+
default: throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(target)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function wikiActionFor(target) {
|
|
63
|
+
switch (target.kind) {
|
|
64
|
+
case "index": return WIKI_ACTION.index;
|
|
65
|
+
case "page": return WIKI_ACTION.page;
|
|
66
|
+
case "log": return WIKI_ACTION.log;
|
|
67
|
+
case "lint_report": return WIKI_ACTION.lintReport;
|
|
68
|
+
case "graph": return WIKI_ACTION.graph;
|
|
69
|
+
default: throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(target)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/wiki/render.ts
|
|
74
|
+
/** HTML-escape attribute / text content. Self-contained on purpose:
|
|
75
|
+
* this package can't reach the host's escaper, and the rule is a
|
|
76
|
+
* fixed five-char map. */
|
|
77
|
+
function escapeHtml(value) {
|
|
78
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
79
|
+
switch (char) {
|
|
80
|
+
case "&": return "&";
|
|
81
|
+
case "<": return "<";
|
|
82
|
+
case ">": return ">";
|
|
83
|
+
case "\"": return """;
|
|
84
|
+
default: return "'";
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Replace every `[[page name]]` occurrence in `content` with a
|
|
90
|
+
* `<span class="wiki-link" data-page="…">…</span>` element. The
|
|
91
|
+
* page name may not contain `]`; an opening `[[` that is not
|
|
92
|
+
* followed later by `]]` (with no bare `]` in between) is left
|
|
93
|
+
* untouched so malformed text renders as-is — matching the
|
|
94
|
+
* previous regex's non-match behaviour.
|
|
95
|
+
*
|
|
96
|
+
* `[[target|display]]` is split via the shared `parseWikiLink`
|
|
97
|
+
* helper so `data-page` carries only the target slug while the
|
|
98
|
+
* visible text shows the display half (#1297). Both halves are
|
|
99
|
+
* HTML-escaped before interpolation — `parseWikiLink` runs BEFORE
|
|
100
|
+
* the host's `marked.parse`, so escaping has to happen here.
|
|
101
|
+
*/
|
|
102
|
+
function renderWikiLinks(content) {
|
|
103
|
+
const out = [];
|
|
104
|
+
let i = 0;
|
|
105
|
+
while (i < content.length) {
|
|
106
|
+
if (content[i] === "[" && content[i + 1] === "[") {
|
|
107
|
+
const closeStart = findNextCloseBrackets(content, i + 2);
|
|
108
|
+
if (closeStart !== -1) {
|
|
109
|
+
const { target, display } = parseWikiLink(content.slice(i + 2, closeStart));
|
|
110
|
+
out.push(`<span class="wiki-link" data-page="${escapeHtml(target)}">${escapeHtml(display)}</span>`);
|
|
111
|
+
i = closeStart + 2;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
out.push(content[i]);
|
|
116
|
+
i++;
|
|
117
|
+
}
|
|
118
|
+
return out.join("");
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Starting at `from`, scan forward for a `]]` sequence. Returns
|
|
122
|
+
* the index of the first `]` of that pair, or -1 if a bare `]`
|
|
123
|
+
* (one not immediately followed by a second `]`) is encountered
|
|
124
|
+
* first — mirroring the old regex's `[^\]]+` constraint that the
|
|
125
|
+
* page name must contain no `]` characters. Also returns -1 if
|
|
126
|
+
* nothing matched before the end of input, or if the pair sits
|
|
127
|
+
* immediately after `from` (zero-length page name, which the old
|
|
128
|
+
* regex rejected via the `+` quantifier).
|
|
129
|
+
*/
|
|
130
|
+
function findNextCloseBrackets(str, from) {
|
|
131
|
+
let j = from;
|
|
132
|
+
while (j < str.length) {
|
|
133
|
+
if (str[j] === "]") {
|
|
134
|
+
if (str[j + 1] === "]" && j > from) return j;
|
|
135
|
+
return -1;
|
|
136
|
+
}
|
|
137
|
+
j++;
|
|
138
|
+
}
|
|
139
|
+
return -1;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
export { BULLET_LINK_PATTERN, BULLET_WIKI_LINK_PATTERN, WIKI_ACTION, WIKI_LINK_PATTERN, WIKI_ROUTE_SECTION, buildTableColumnMap, buildWikiGraph, buildWikiRouteParams, escapeHtml, extractHashTags, extractSlugFromBulletHref, findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift, formatLintReport, incomingLinks, isSafeSlug, isSafeWikiSlug, pageOutgoingSlugs, parseIndexEntries, parseTagsCell, parseWikiLink, readWikiRouteTarget, renderWikiLinks, resolveLinkTarget, wikiActionFor, wikiSlugify };
|
|
143
|
+
|
|
144
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/wiki/route.ts","../../src/wiki/render.ts"],"sourcesContent":["// Pure helpers for reading, building, and validating wiki route\n// params. Kept free of Vue / vue-router imports so it can be shared\n// across hosts (MulmoClaude + MulmoTerminal) from\n// `@mulmoclaude/core/wiki`:\n//\n// - the host router guard (synchronous validation at navigation time)\n// - the wiki View (watcher + push helpers)\n// - the workspace-link click handler\n// - unit tests\n//\n// Mirrors the \"one file owns the URL ↔ domain mapping\" pattern so the\n// literals don't drift across the router definition, guards, views,\n// and tests.\n\nimport { isSafeSlug } from \"./slug.js\";\n\n// URL segment used in `/wiki/:section/...`. Closed enum — the router\n// regex `(pages|log|lint-report)` rejects anything else.\nexport const WIKI_ROUTE_SECTION = {\n pages: \"pages\",\n log: \"log\",\n lintReport: \"lint-report\",\n graph: \"graph\",\n} as const;\n\nexport type WikiRouteSection = (typeof WIKI_ROUTE_SECTION)[keyof typeof WIKI_ROUTE_SECTION];\n\n// Internal action name sent to the server / shown in the View. Diverges\n// from the URL segment in one place only: `lint-report` (URL) vs\n// `lint_report` (action), because the server API still speaks the\n// underscore form.\nexport const WIKI_ACTION = {\n index: \"index\",\n page: \"page\",\n log: \"log\",\n lintReport: \"lint_report\",\n graph: \"graph\",\n save: \"save\",\n} as const;\n\nexport type WikiAction = (typeof WIKI_ACTION)[keyof typeof WIKI_ACTION];\n\n// Route-level representation. `pushWiki(target)` and\n// `readWikiRouteTarget(params)` both speak this so the watcher, the\n// button handlers, and the router guard agree on the same shape.\nexport type WikiTarget = { kind: \"index\" } | { kind: \"page\"; slug: string } | { kind: \"log\" } | { kind: \"lint_report\" } | { kind: \"graph\" };\n\n// Reject anything that could escape `data/wiki/pages/` or collide\n// with a different page. Vue Router decodes `%2F` back to `/` in\n// `route.params.slug`, so `/wiki/pages/..%2Fsecrets` lands here as\n// `slug === \"../secrets\"` — this check is the last line of defence\n// before the slug is passed to the server's page resolver. Non-\n// ASCII characters (e.g. Japanese page titles) are allowed; only\n// separators and the literal `.` / `..` are blocked.\n//\n// Delegates the actual rule to `isSafeSlug` in\n// `src/lib/wiki-page/slug.ts` (imported at the top of this file)\n// so server and frontend share one implementation (#1297). The\n// wrapper adds the type-guard / non-string-input handling the\n// router needs.\nexport function isSafeWikiSlug(value: unknown): value is string {\n return typeof value === \"string\" && isSafeSlug(value);\n}\n\n// Read `route.params` from the wiki route and normalise to a\n// `WikiTarget`. Returns `null` when the params describe an invalid\n// state (unknown section, missing slug for a page view, unsafe slug)\n// so the caller can decide what to do — the router guard redirects to\n// `/wiki`, the view watcher treats it as \"render the index\".\nexport function readWikiRouteTarget(params: unknown): WikiTarget | null {\n if (!params || typeof params !== \"object\") return null;\n const { section, slug } = params as { section?: unknown; slug?: unknown };\n\n if (section === undefined || section === \"\") return { kind: \"index\" };\n\n if (section === WIKI_ROUTE_SECTION.pages) {\n if (!isSafeWikiSlug(slug)) return null;\n return { kind: \"page\", slug };\n }\n if (section === WIKI_ROUTE_SECTION.log) return { kind: \"log\" };\n if (section === WIKI_ROUTE_SECTION.lintReport) return { kind: \"lint_report\" };\n if (section === WIKI_ROUTE_SECTION.graph) return { kind: \"graph\" };\n\n return null;\n}\n\n// Inverse of `readWikiRouteTarget`: given a target, produce the\n// `{ section, slug }` params object that `router.push({ name: \"wiki\",\n// params })` needs.\n//\n// Optional route params are NOT cleared by named-route navigation\n// unless explicitly set — returning `{}` for `kind: \"index\"` would\n// leak the previous `section`/`slug` into the URL when navigating\n// from `/wiki/pages/foo` back to the index, leaving the user on\n// `/wiki/pages/foo` with the View believing it's the index. Pass\n// empty strings so the router writes out `/wiki` cleanly. The\n// readWikiRouteTarget() branch for `section === \"\"` already\n// normalises those back to `{ kind: \"index\" }`.\nexport function buildWikiRouteParams(target: WikiTarget): Record<string, string> {\n switch (target.kind) {\n case \"index\":\n return { section: \"\", slug: \"\" };\n case \"page\":\n return { section: WIKI_ROUTE_SECTION.pages, slug: target.slug };\n case \"log\":\n return { section: WIKI_ROUTE_SECTION.log, slug: \"\" };\n case \"lint_report\":\n return { section: WIKI_ROUTE_SECTION.lintReport, slug: \"\" };\n case \"graph\":\n return { section: WIKI_ROUTE_SECTION.graph, slug: \"\" };\n default: {\n const exhaustive: never = target;\n throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n// Resolve a target to the action name the server expects. Centralises\n// the one place URL-shape and action-shape diverge (`lint-report` ↔\n// `lint_report`).\nexport function wikiActionFor(target: WikiTarget): WikiAction {\n switch (target.kind) {\n case \"index\":\n return WIKI_ACTION.index;\n case \"page\":\n return WIKI_ACTION.page;\n case \"log\":\n return WIKI_ACTION.log;\n case \"lint_report\":\n return WIKI_ACTION.lintReport;\n case \"graph\":\n return WIKI_ACTION.graph;\n default: {\n const exhaustive: never = target;\n throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n","// Pure `[[wiki-link]]` → HTML renderer, shared across hosts via\n// `@mulmoclaude/core/wiki`. Extracted from the MulmoClaude host's\n// `src/plugins/wiki/helpers.ts` so MulmoTerminal can render the same\n// internal-link markup without forking the walker.\n//\n// String-only, no `marked` / DOM / Node deps — the host pipeline\n// (image-ref rewrite, marked.parse, task-interactive) wraps this.\n\nimport { parseWikiLink } from \"./link.js\";\n\n/** HTML-escape attribute / text content. Self-contained on purpose:\n * this package can't reach the host's escaper, and the rule is a\n * fixed five-char map. */\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => {\n switch (char) {\n case \"&\":\n return \"&\";\n case \"<\":\n return \"<\";\n case \">\":\n return \">\";\n case '\"':\n return \""\";\n default:\n return \"'\";\n }\n });\n}\n\n/**\n * Replace every `[[page name]]` occurrence in `content` with a\n * `<span class=\"wiki-link\" data-page=\"…\">…</span>` element. The\n * page name may not contain `]`; an opening `[[` that is not\n * followed later by `]]` (with no bare `]` in between) is left\n * untouched so malformed text renders as-is — matching the\n * previous regex's non-match behaviour.\n *\n * `[[target|display]]` is split via the shared `parseWikiLink`\n * helper so `data-page` carries only the target slug while the\n * visible text shows the display half (#1297). Both halves are\n * HTML-escaped before interpolation — `parseWikiLink` runs BEFORE\n * the host's `marked.parse`, so escaping has to happen here.\n */\nexport function renderWikiLinks(content: string): string {\n const out: string[] = [];\n let i = 0;\n while (i < content.length) {\n if (content[i] === \"[\" && content[i + 1] === \"[\") {\n const closeStart = findNextCloseBrackets(content, i + 2);\n if (closeStart !== -1) {\n const inner = content.slice(i + 2, closeStart);\n const { target, display } = parseWikiLink(inner);\n out.push(`<span class=\"wiki-link\" data-page=\"${escapeHtml(target)}\">${escapeHtml(display)}</span>`);\n i = closeStart + 2;\n continue;\n }\n }\n out.push(content[i]);\n i++;\n }\n return out.join(\"\");\n}\n\n/**\n * Starting at `from`, scan forward for a `]]` sequence. Returns\n * the index of the first `]` of that pair, or -1 if a bare `]`\n * (one not immediately followed by a second `]`) is encountered\n * first — mirroring the old regex's `[^\\]]+` constraint that the\n * page name must contain no `]` characters. Also returns -1 if\n * nothing matched before the end of input, or if the pair sits\n * immediately after `from` (zero-length page name, which the old\n * regex rejected via the `+` quantifier).\n */\nfunction findNextCloseBrackets(str: string, from: number): number {\n let j = from;\n while (j < str.length) {\n if (str[j] === \"]\") {\n if (str[j + 1] === \"]\" && j > from) return j;\n // Bare `]` inside the page-name span — old regex would not\n // match here, so we bail and let the caller emit the `[[`\n // as literal text.\n return -1;\n }\n j++;\n }\n return -1;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Inline `[[...]]` link pattern. The capture group is the raw
|
|
2
|
+
* text between the brackets — the caller is expected to feed it
|
|
3
|
+
* through `parseWikiLink` to split off any `|display` suffix.
|
|
4
|
+
*
|
|
5
|
+
* Body length is capped at 200 chars to keep the regex linear:
|
|
6
|
+
* catastrophic backtracking isn't possible (no nested
|
|
7
|
+
* alternation), but a malicious page with thousands of
|
|
8
|
+
* unmatched `[[` could still pin the CPU. 200 is well above the
|
|
9
|
+
* longest legitimate title we've seen and matches the cap the
|
|
10
|
+
* server-side `WIKI_LINK_PATTERN` constant has carried since the
|
|
11
|
+
* original implementation in #951. */
|
|
12
|
+
export declare const WIKI_LINK_PATTERN: RegExp;
|
|
13
|
+
/** The structural shape of an inline `[[target|display]]` link.
|
|
14
|
+
* `target` is what the resolver / lint compares against page
|
|
15
|
+
* slugs; `display` is what the renderer shows to the user.
|
|
16
|
+
* When the user writes `[[foo]]` (no pipe), both fields hold
|
|
17
|
+
* `foo`. */
|
|
18
|
+
export interface WikiLink {
|
|
19
|
+
target: string;
|
|
20
|
+
display: string;
|
|
21
|
+
}
|
|
22
|
+
/** Split the raw text inside `[[...]]` into target + display.
|
|
23
|
+
*
|
|
24
|
+
* - `[[foo|Bar Baz]]` → `{ target: "foo", display: "Bar Baz" }`
|
|
25
|
+
* - `[[foo]]` → `{ target: "foo", display: "foo" }`
|
|
26
|
+
* - `[[|empty target]]` → `{ target: "", display: "empty target" }`
|
|
27
|
+
* (caller decides whether an empty target is meaningful;
|
|
28
|
+
* typically lint flags it, renderer shows the display text raw)
|
|
29
|
+
* - `[[foo|]]` → `{ target: "foo", display: "" }`
|
|
30
|
+
* (same — caller decides)
|
|
31
|
+
*
|
|
32
|
+
* Whitespace around the pipe is preserved on the display side
|
|
33
|
+
* (so the user can write `[[foo| ある記事 ]]` and get the
|
|
34
|
+
* spacing intentionally) but TRIMMED on the target side, since
|
|
35
|
+
* a slug-comparable target with leading/trailing whitespace is
|
|
36
|
+
* always a typo.
|
|
37
|
+
*
|
|
38
|
+
* Only the FIRST pipe splits. `[[a|b|c]]` becomes
|
|
39
|
+
* `{ target: "a", display: "b|c" }` so display strings can
|
|
40
|
+
* legitimately contain a literal `|`. */
|
|
41
|
+
export declare function parseWikiLink(inner: string): WikiLink;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { WikiPageEntry } from './index-parse.js';
|
|
2
|
+
/** Files on disk that aren't referenced by index.md. */
|
|
3
|
+
export declare function findOrphanPages(fileSlugs: ReadonlySet<string>, indexedSlugs: ReadonlySet<string>): string[];
|
|
4
|
+
/** Slugs referenced by index.md that have no corresponding file. */
|
|
5
|
+
export declare function findMissingFiles(pageEntries: readonly WikiPageEntry[], fileSlugs: ReadonlySet<string>): string[];
|
|
6
|
+
/** Walk a page's body for `[[…]]` links and flag any whose
|
|
7
|
+
* resolved slug doesn't exist in the file set.
|
|
8
|
+
*
|
|
9
|
+
* Critically: this routes through `parseWikiLink` so
|
|
10
|
+
* `[[slug|display]]` is split correctly — the lint slugifies the
|
|
11
|
+
* TARGET, not the full bracket body. Pre-#1297 the lint
|
|
12
|
+
* slugified the entire content (`slug|display`), which collapsed
|
|
13
|
+
* to a slug that always missed and produced ~168 false-positive
|
|
14
|
+
* "broken link" warnings. */
|
|
15
|
+
export declare function findBrokenLinksInPage(fileName: string, content: string, fileSlugs: ReadonlySet<string>): string[];
|
|
16
|
+
/** Flag any slug whose index.md tags differ from the page's own
|
|
17
|
+
* frontmatter `tags:` field. Comparison is set-based and order-
|
|
18
|
+
* insensitive; both sides are lowercased at parse time. Slugs
|
|
19
|
+
* missing from `frontmatterTagsBySlug` are ignored here — the
|
|
20
|
+
* missing file itself is already reported by `findMissingFiles`. */
|
|
21
|
+
export declare function findTagDrift(pageEntries: readonly WikiPageEntry[], frontmatterTagsBySlug: ReadonlyMap<string, readonly string[]>): string[];
|
|
22
|
+
/** Render the final markdown report from the accumulated issues
|
|
23
|
+
* list. Empty input yields the "wiki is healthy" sentinel so the
|
|
24
|
+
* caller can write the same file every time without branching on
|
|
25
|
+
* presence. */
|
|
26
|
+
export declare function formatLintReport(issues: readonly string[]): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
|
|
3
|
+
const require_slug = require("../slug-DbwORN9z.cjs");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
6
|
+
//#region src/wiki/server/paths.ts
|
|
7
|
+
/** Canonical on-disk layout of a wiki, derived from the workspace
|
|
8
|
+
* root. Owning this here (not per host) is what keeps MulmoClaude and
|
|
9
|
+
* MulmoTerminal agreeing on where `data/wiki/` lives. */
|
|
10
|
+
function wikiDirs(workspace) {
|
|
11
|
+
const root = node_path.default.join(workspace, "data", "wiki");
|
|
12
|
+
return {
|
|
13
|
+
pagesDir: node_path.default.join(root, "pages"),
|
|
14
|
+
indexFile: node_path.default.join(root, "index.md"),
|
|
15
|
+
logFile: node_path.default.join(root, "log.md")
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Given an absolute path and the absolute `pagesDir`, return the
|
|
19
|
+
* slug if `absPath` is a direct `.md` child of `pagesDir`, else
|
|
20
|
+
* null. Pure path-string math — no fs IO, no symlink resolution.
|
|
21
|
+
*
|
|
22
|
+
* Caller responsibility: pass already-realpath'd values for both
|
|
23
|
+
* arguments. Mixing a realpath'd `absPath` with a symlinked
|
|
24
|
+
* `pagesDir` (or vice versa) silently mismatches because
|
|
25
|
+
* `path.relative` is plain string arithmetic. The trap caused
|
|
26
|
+
* #883 review-iter-1 — a symlinked workspace silently routed
|
|
27
|
+
* wiki writes through the generic writer. */
|
|
28
|
+
function wikiSlugFromAbsPath(absPath, pagesDir) {
|
|
29
|
+
const rel = node_path.default.relative(pagesDir, absPath);
|
|
30
|
+
if (rel.length === 0) return null;
|
|
31
|
+
if (node_path.default.isAbsolute(rel)) return null;
|
|
32
|
+
if (rel.includes(node_path.default.sep)) return null;
|
|
33
|
+
if (!rel.endsWith(".md")) return null;
|
|
34
|
+
const slug = rel.slice(0, -3);
|
|
35
|
+
if (!require_slug.isSafeSlug(slug)) return null;
|
|
36
|
+
return slug;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
exports.wikiDirs = wikiDirs;
|
|
40
|
+
exports.wikiSlugFromAbsPath = wikiSlugFromAbsPath;
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=paths.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.cjs","names":[],"sources":["../../src/wiki/server/paths.ts"],"sourcesContent":["// Node-only wiki-page helpers — anything that needs `node:path`\n// lives here so the rest of `@mulmoclaude/core/wiki` stays pure\n// (string-only, importable from both browser and Node bundles).\n// Frontend code MUST NOT import this file directly; use the pure\n// `@mulmoclaude/core/wiki` modules instead.\n\nimport path from \"node:path\";\nimport { isSafeSlug } from \"../slug.js\";\n\n/** Canonical on-disk layout of a wiki, derived from the workspace\n * root. Owning this here (not per host) is what keeps MulmoClaude and\n * MulmoTerminal agreeing on where `data/wiki/` lives. */\nexport function wikiDirs(workspace: string): { pagesDir: string; indexFile: string; logFile: string } {\n const root = path.join(workspace, \"data\", \"wiki\");\n return {\n pagesDir: path.join(root, \"pages\"),\n indexFile: path.join(root, \"index.md\"),\n logFile: path.join(root, \"log.md\"),\n };\n}\n\n/** Given an absolute path and the absolute `pagesDir`, return the\n * slug if `absPath` is a direct `.md` child of `pagesDir`, else\n * null. Pure path-string math — no fs IO, no symlink resolution.\n *\n * Caller responsibility: pass already-realpath'd values for both\n * arguments. Mixing a realpath'd `absPath` with a symlinked\n * `pagesDir` (or vice versa) silently mismatches because\n * `path.relative` is plain string arithmetic. The trap caused\n * #883 review-iter-1 — a symlinked workspace silently routed\n * wiki writes through the generic writer. */\nexport function wikiSlugFromAbsPath(absPath: string, pagesDir: string): string | null {\n const rel = path.relative(pagesDir, absPath);\n if (rel.length === 0) return null;\n if (path.isAbsolute(rel)) return null;\n // Direct child only — no nested layout today. Any separator\n // means the path either escapes (`../secret.md`) or descends\n // (`subdir/foo.md`). A literal page name like `..foo.md` is a\n // single segment without a separator and is allowed (codex\n // iter-3 #883 — the prior `startsWith(\"..\")` rule wrongly\n // rejected it).\n if (rel.includes(path.sep)) return null;\n if (!rel.endsWith(\".md\")) return null;\n const slug = rel.slice(0, -\".md\".length);\n if (!isSafeSlug(slug)) return null;\n return slug;\n}\n"],"mappings":";;;;;;;;;AAYA,SAAgB,SAAS,WAA6E;CACpG,MAAM,OAAO,UAAA,QAAK,KAAK,WAAW,QAAQ,MAAM;CAChD,OAAO;EACL,UAAU,UAAA,QAAK,KAAK,MAAM,OAAO;EACjC,WAAW,UAAA,QAAK,KAAK,MAAM,UAAU;EACrC,SAAS,UAAA,QAAK,KAAK,MAAM,QAAQ;CACnC;AACF;;;;;;;;;;;AAYA,SAAgB,oBAAoB,SAAiB,UAAiC;CACpF,MAAM,MAAM,UAAA,QAAK,SAAS,UAAU,OAAO;CAC3C,IAAI,IAAI,WAAW,GAAG,OAAO;CAC7B,IAAI,UAAA,QAAK,WAAW,GAAG,GAAG,OAAO;CAOjC,IAAI,IAAI,SAAS,UAAA,QAAK,GAAG,GAAG,OAAO;CACnC,IAAI,CAAC,IAAI,SAAS,KAAK,GAAG,OAAO;CACjC,MAAM,OAAO,IAAI,MAAM,GAAG,EAAa;CACvC,IAAI,CAAC,aAAA,WAAW,IAAI,GAAG,OAAO;CAC9B,OAAO;AACT"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { t as isSafeSlug } from "../slug-CdN-pQX1.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/wiki/server/paths.ts
|
|
4
|
+
/** Canonical on-disk layout of a wiki, derived from the workspace
|
|
5
|
+
* root. Owning this here (not per host) is what keeps MulmoClaude and
|
|
6
|
+
* MulmoTerminal agreeing on where `data/wiki/` lives. */
|
|
7
|
+
function wikiDirs(workspace) {
|
|
8
|
+
const root = path.join(workspace, "data", "wiki");
|
|
9
|
+
return {
|
|
10
|
+
pagesDir: path.join(root, "pages"),
|
|
11
|
+
indexFile: path.join(root, "index.md"),
|
|
12
|
+
logFile: path.join(root, "log.md")
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Given an absolute path and the absolute `pagesDir`, return the
|
|
16
|
+
* slug if `absPath` is a direct `.md` child of `pagesDir`, else
|
|
17
|
+
* null. Pure path-string math — no fs IO, no symlink resolution.
|
|
18
|
+
*
|
|
19
|
+
* Caller responsibility: pass already-realpath'd values for both
|
|
20
|
+
* arguments. Mixing a realpath'd `absPath` with a symlinked
|
|
21
|
+
* `pagesDir` (or vice versa) silently mismatches because
|
|
22
|
+
* `path.relative` is plain string arithmetic. The trap caused
|
|
23
|
+
* #883 review-iter-1 — a symlinked workspace silently routed
|
|
24
|
+
* wiki writes through the generic writer. */
|
|
25
|
+
function wikiSlugFromAbsPath(absPath, pagesDir) {
|
|
26
|
+
const rel = path.relative(pagesDir, absPath);
|
|
27
|
+
if (rel.length === 0) return null;
|
|
28
|
+
if (path.isAbsolute(rel)) return null;
|
|
29
|
+
if (rel.includes(path.sep)) return null;
|
|
30
|
+
if (!rel.endsWith(".md")) return null;
|
|
31
|
+
const slug = rel.slice(0, -3);
|
|
32
|
+
if (!isSafeSlug(slug)) return null;
|
|
33
|
+
return slug;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { wikiDirs, wikiSlugFromAbsPath };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","names":[],"sources":["../../src/wiki/server/paths.ts"],"sourcesContent":["// Node-only wiki-page helpers — anything that needs `node:path`\n// lives here so the rest of `@mulmoclaude/core/wiki` stays pure\n// (string-only, importable from both browser and Node bundles).\n// Frontend code MUST NOT import this file directly; use the pure\n// `@mulmoclaude/core/wiki` modules instead.\n\nimport path from \"node:path\";\nimport { isSafeSlug } from \"../slug.js\";\n\n/** Canonical on-disk layout of a wiki, derived from the workspace\n * root. Owning this here (not per host) is what keeps MulmoClaude and\n * MulmoTerminal agreeing on where `data/wiki/` lives. */\nexport function wikiDirs(workspace: string): { pagesDir: string; indexFile: string; logFile: string } {\n const root = path.join(workspace, \"data\", \"wiki\");\n return {\n pagesDir: path.join(root, \"pages\"),\n indexFile: path.join(root, \"index.md\"),\n logFile: path.join(root, \"log.md\"),\n };\n}\n\n/** Given an absolute path and the absolute `pagesDir`, return the\n * slug if `absPath` is a direct `.md` child of `pagesDir`, else\n * null. Pure path-string math — no fs IO, no symlink resolution.\n *\n * Caller responsibility: pass already-realpath'd values for both\n * arguments. Mixing a realpath'd `absPath` with a symlinked\n * `pagesDir` (or vice versa) silently mismatches because\n * `path.relative` is plain string arithmetic. The trap caused\n * #883 review-iter-1 — a symlinked workspace silently routed\n * wiki writes through the generic writer. */\nexport function wikiSlugFromAbsPath(absPath: string, pagesDir: string): string | null {\n const rel = path.relative(pagesDir, absPath);\n if (rel.length === 0) return null;\n if (path.isAbsolute(rel)) return null;\n // Direct child only — no nested layout today. Any separator\n // means the path either escapes (`../secret.md`) or descends\n // (`subdir/foo.md`). A literal page name like `..foo.md` is a\n // single segment without a separator and is allowed (codex\n // iter-3 #883 — the prior `startsWith(\"..\")` rule wrongly\n // rejected it).\n if (rel.includes(path.sep)) return null;\n if (!rel.endsWith(\".md\")) return null;\n const slug = rel.slice(0, -\".md\".length);\n if (!isSafeSlug(slug)) return null;\n return slug;\n}\n"],"mappings":";;;;;;AAYA,SAAgB,SAAS,WAA6E;CACpG,MAAM,OAAO,KAAK,KAAK,WAAW,QAAQ,MAAM;CAChD,OAAO;EACL,UAAU,KAAK,KAAK,MAAM,OAAO;EACjC,WAAW,KAAK,KAAK,MAAM,UAAU;EACrC,SAAS,KAAK,KAAK,MAAM,QAAQ;CACnC;AACF;;;;;;;;;;;AAYA,SAAgB,oBAAoB,SAAiB,UAAiC;CACpF,MAAM,MAAM,KAAK,SAAS,UAAU,OAAO;CAC3C,IAAI,IAAI,WAAW,GAAG,OAAO;CAC7B,IAAI,KAAK,WAAW,GAAG,GAAG,OAAO;CAOjC,IAAI,IAAI,SAAS,KAAK,GAAG,GAAG,OAAO;CACnC,IAAI,CAAC,IAAI,SAAS,KAAK,GAAG,OAAO;CACjC,MAAM,OAAO,IAAI,MAAM,GAAG,EAAa;CACvC,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO;CAC9B,OAAO;AACT"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** HTML-escape attribute / text content. Self-contained on purpose:
|
|
2
|
+
* this package can't reach the host's escaper, and the rule is a
|
|
3
|
+
* fixed five-char map. */
|
|
4
|
+
export declare function escapeHtml(value: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Replace every `[[page name]]` occurrence in `content` with a
|
|
7
|
+
* `<span class="wiki-link" data-page="…">…</span>` element. The
|
|
8
|
+
* page name may not contain `]`; an opening `[[` that is not
|
|
9
|
+
* followed later by `]]` (with no bare `]` in between) is left
|
|
10
|
+
* untouched so malformed text renders as-is — matching the
|
|
11
|
+
* previous regex's non-match behaviour.
|
|
12
|
+
*
|
|
13
|
+
* `[[target|display]]` is split via the shared `parseWikiLink`
|
|
14
|
+
* helper so `data-page` carries only the target slug while the
|
|
15
|
+
* visible text shows the display half (#1297). Both halves are
|
|
16
|
+
* HTML-escaped before interpolation — `parseWikiLink` runs BEFORE
|
|
17
|
+
* the host's `marked.parse`, so escaping has to happen here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderWikiLinks(content: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const WIKI_ROUTE_SECTION: {
|
|
2
|
+
readonly pages: "pages";
|
|
3
|
+
readonly log: "log";
|
|
4
|
+
readonly lintReport: "lint-report";
|
|
5
|
+
readonly graph: "graph";
|
|
6
|
+
};
|
|
7
|
+
export type WikiRouteSection = (typeof WIKI_ROUTE_SECTION)[keyof typeof WIKI_ROUTE_SECTION];
|
|
8
|
+
export declare const WIKI_ACTION: {
|
|
9
|
+
readonly index: "index";
|
|
10
|
+
readonly page: "page";
|
|
11
|
+
readonly log: "log";
|
|
12
|
+
readonly lintReport: "lint_report";
|
|
13
|
+
readonly graph: "graph";
|
|
14
|
+
readonly save: "save";
|
|
15
|
+
};
|
|
16
|
+
export type WikiAction = (typeof WIKI_ACTION)[keyof typeof WIKI_ACTION];
|
|
17
|
+
export type WikiTarget = {
|
|
18
|
+
kind: "index";
|
|
19
|
+
} | {
|
|
20
|
+
kind: "page";
|
|
21
|
+
slug: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "log";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "lint_report";
|
|
26
|
+
} | {
|
|
27
|
+
kind: "graph";
|
|
28
|
+
};
|
|
29
|
+
export declare function isSafeWikiSlug(value: unknown): value is string;
|
|
30
|
+
export declare function readWikiRouteTarget(params: unknown): WikiTarget | null;
|
|
31
|
+
export declare function buildWikiRouteParams(target: WikiTarget): Record<string, string>;
|
|
32
|
+
export declare function wikiActionFor(target: WikiTarget): WikiAction;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { WikiPageEntry } from '../index-parse.js';
|
|
2
|
+
import { WikiGraph } from '../graph.js';
|
|
3
|
+
/** Walk every indexed slug for an `includes`-style match. Returns the
|
|
4
|
+
* single best candidate, or null when the slug is too short OR several
|
|
5
|
+
* candidates tie at the top score (ambiguous → defer to the caller's
|
|
6
|
+
* title-match fallback). Score = min/max length, decoupled from Map
|
|
7
|
+
* iteration order so resolution is deterministic across hosts. */
|
|
8
|
+
export declare function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null;
|
|
9
|
+
/** Resolve a page name to an absolute `.md` path: exact slug → fuzzy →
|
|
10
|
+
* index-title fallback (for non-ASCII names that slugify to empty).
|
|
11
|
+
* `pageName` may carry the `[[target|display]]` form; `parseWikiLink`
|
|
12
|
+
* strips the display half so the lookup uses just the target. */
|
|
13
|
+
export declare function resolvePagePath(workspace: string, pageName: string): Promise<string | null>;
|
|
14
|
+
/** Raw `index.md` content + its parsed entries. */
|
|
15
|
+
export declare function readWikiIndex(workspace: string): {
|
|
16
|
+
content: string;
|
|
17
|
+
entries: WikiPageEntry[];
|
|
18
|
+
};
|
|
19
|
+
/** Raw `log.md` content (empty string if absent). */
|
|
20
|
+
export declare function readWikiLog(workspace: string): string;
|
|
21
|
+
export interface WikiPageRead {
|
|
22
|
+
/** Absolute path of the resolved file, or null when nothing matched. */
|
|
23
|
+
filePath: string | null;
|
|
24
|
+
/** File body (empty when missing OR when the file is an empty placeholder). */
|
|
25
|
+
content: string;
|
|
26
|
+
/** True iff a page file resolved (distinct from empty content). */
|
|
27
|
+
exists: boolean;
|
|
28
|
+
/** Title to display — the resolved filename stem, or the raw pageName. */
|
|
29
|
+
resolvedTitle: string;
|
|
30
|
+
}
|
|
31
|
+
/** Resolve + read a page. Distinguishes missing (`exists: false`) from
|
|
32
|
+
* empty-but-present (`exists: true`, `content: ""`). */
|
|
33
|
+
export declare function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead>;
|
|
34
|
+
/** Read every page + the index and build the page→page link graph.
|
|
35
|
+
* No cache: the graph is requested explicitly and a content edit does
|
|
36
|
+
* not advance the pagesDir mtime the page index caches on. */
|
|
37
|
+
export declare function loadWikiGraph(workspace: string): Promise<WikiGraph>;
|
|
38
|
+
/** Run every lint rule over the on-disk wiki, returning issue strings. */
|
|
39
|
+
export declare function collectLintIssues(workspace: string): Promise<string[]>;
|