@open-agent-toolkit/cli 0.3.32 → 0.3.34
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/contributing/documentation.md +1 -0
- package/assets/docs/docs-tooling/check.md +35 -11
- package/assets/docs/docs-tooling/commands.md +20 -2
- package/assets/docs/reference/cli-reference.md +2 -2
- package/assets/docs/reference/config-and-local-state.md +1 -1
- package/assets/docs/reference/configuration.md +1 -1
- package/assets/docs/reference/docs-index-contract.md +3 -2
- package/assets/docs/reference/oat-directory-structure.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-docs-analyze/SKILL.md +35 -11
- package/assets/skills/oat-docs-analyze/references/analysis-artifact-template.md +2 -2
- package/assets/skills/oat-docs-analyze/references/quality-checklist.md +3 -1
- package/assets/skills/oat-docs-analyze/scripts/package-drift.mjs +303 -30
- package/assets/skills/oat-docs-apply/SKILL.md +3 -1
- package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
- package/assets/skills/oat-docs-authoring/references/oat-fumadocs-contract.md +6 -1
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +42 -9
- package/dist/commands/docs/check/check.d.ts.map +1 -1
- package/dist/commands/docs/check/check.js +1 -0
- package/dist/commands/docs/check/checker.d.ts +2 -0
- package/dist/commands/docs/check/checker.d.ts.map +1 -1
- package/dist/commands/docs/check/checker.js +6 -1
- package/dist/commands/docs/check/commands.d.ts.map +1 -1
- package/dist/commands/docs/check/commands.js +74 -30
- package/dist/commands/docs/check/files.d.ts +2 -0
- package/dist/commands/docs/check/files.d.ts.map +1 -1
- package/dist/commands/docs/check/files.js +16 -0
- package/dist/commands/docs/check/navigation.d.ts +11 -1
- package/dist/commands/docs/check/navigation.d.ts.map +1 -1
- package/dist/commands/docs/check/navigation.js +40 -69
- package/dist/commands/docs/check/reader-rules.d.ts.map +1 -1
- package/dist/commands/docs/check/reader-rules.js +12 -9
- package/dist/commands/docs/nav/contents.d.ts +50 -2
- package/dist/commands/docs/nav/contents.d.ts.map +1 -1
- package/dist/commands/docs/nav/contents.js +137 -14
- package/dist/commands/docs/nav/fumadocs.d.ts +29 -0
- package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
- package/dist/commands/docs/nav/fumadocs.js +16 -41
- package/dist/commands/docs/nav/membership.d.ts +37 -0
- package/dist/commands/docs/nav/membership.d.ts.map +1 -0
- package/dist/commands/docs/nav/membership.js +474 -0
- package/dist/commands/docs/nav/sync.d.ts +26 -2
- package/dist/commands/docs/nav/sync.d.ts.map +1 -1
- package/dist/commands/docs/nav/sync.js +160 -43
- package/package.json +3 -3
|
@@ -1,7 +1,99 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, join, posix, relative, resolve, } from 'node:path';
|
|
3
3
|
import { createExclusionMatcher, isExcludedPath, } from '../../docs/shared/exclusions.js';
|
|
4
4
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
/**
|
|
8
|
+
* A defect in one index.md Contents map. `indexPath` is the index file that
|
|
9
|
+
* contains it, as the caller named it, and `line` the entry line when the
|
|
10
|
+
* entries were parsed with locations, so a consumer that only sees the thrown
|
|
11
|
+
* error can still report it against the file that contains it.
|
|
12
|
+
*/
|
|
13
|
+
export class DocsContentsError extends Error {
|
|
14
|
+
indexPath;
|
|
15
|
+
line;
|
|
16
|
+
constructor(message, indexPath, line) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.indexPath = indexPath;
|
|
19
|
+
this.line = line;
|
|
20
|
+
this.name = 'DocsContentsError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Reader page extensions: Fumadocs renders `.md` and `.mdx` pages. */
|
|
24
|
+
export const PAGE_EXTENSIONS = ['.md', '.mdx'];
|
|
25
|
+
/** Every non-excluded directory and reader page under the docs root. */
|
|
26
|
+
export async function inventoryDocs(docsRoot, matcher) {
|
|
27
|
+
const directories = [];
|
|
28
|
+
const pages = [];
|
|
29
|
+
async function walk(relativeDir) {
|
|
30
|
+
directories.push(relativeDir);
|
|
31
|
+
const entries = await readdir(join(docsRoot, relativeDir), {
|
|
32
|
+
withFileTypes: true,
|
|
33
|
+
});
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const entryPath = posix.join(relativeDir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
if (matcher.excludesDirectory(entryPath))
|
|
41
|
+
continue;
|
|
42
|
+
await walk(entryPath);
|
|
43
|
+
}
|
|
44
|
+
else if (entry.isFile() &&
|
|
45
|
+
PAGE_EXTENSIONS.includes(extname(entry.name))) {
|
|
46
|
+
if (!matcher.excludesFile(entryPath))
|
|
47
|
+
pages.push(entryPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await walk('.');
|
|
52
|
+
directories.sort();
|
|
53
|
+
pages.sort();
|
|
54
|
+
return { directories, pages };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether a directory is an index-only leaf: its own `index.md` is the only
|
|
58
|
+
* reader page in it or below it, so it renders as a single-page section.
|
|
59
|
+
* `pagePaths` are docs-relative POSIX reader page paths.
|
|
60
|
+
*/
|
|
61
|
+
export function isIndexOnlyLeaf(dirRelativePath, pagePaths) {
|
|
62
|
+
const prefix = dirRelativePath === '.' ? '' : `${dirRelativePath}/`;
|
|
63
|
+
const ownIndex = `${prefix}index.md`;
|
|
64
|
+
let hasIndex = false;
|
|
65
|
+
for (const path of pagePaths) {
|
|
66
|
+
if (!path.startsWith(prefix))
|
|
67
|
+
continue;
|
|
68
|
+
if (path !== ownIndex)
|
|
69
|
+
return false;
|
|
70
|
+
hasIndex = true;
|
|
71
|
+
}
|
|
72
|
+
return hasIndex;
|
|
73
|
+
}
|
|
74
|
+
/** A Contents href naming a heading on the same page, such as `#setup`. */
|
|
75
|
+
export function isSamePageAnchor(href) {
|
|
76
|
+
return href.trim().startsWith('#');
|
|
77
|
+
}
|
|
78
|
+
const inlineMarkdown = unified().use(remarkParse);
|
|
79
|
+
/**
|
|
80
|
+
* Whether a Contents entry is a real Markdown link to a same-page fragment.
|
|
81
|
+
* The Contents grammar is line-based, so it captures destinations such as
|
|
82
|
+
* `#a b`, `#a(b` or `#a\` that Markdown does not parse as links; those render
|
|
83
|
+
* as plain text and no link rule would ever validate them. The CommonMark
|
|
84
|
+
* parser the reader check uses decides, over the authored link text rather
|
|
85
|
+
* than the `href` truncated at the first `)`, so balanced `#a(b)` and escaped
|
|
86
|
+
* `#a\(b\)` fragments stay links. The links rule then checks the URL remark
|
|
87
|
+
* parsed against the page's headings.
|
|
88
|
+
*/
|
|
89
|
+
function isMarkdownSamePageLink(entry) {
|
|
90
|
+
const tree = inlineMarkdown.parse(entry.source ?? `[${entry.title}](${entry.href})`);
|
|
91
|
+
const paragraph = tree.children?.length === 1 ? tree.children[0] : undefined;
|
|
92
|
+
const link = paragraph?.type === 'paragraph' ? paragraph.children?.[0] : undefined;
|
|
93
|
+
return (link?.type === 'link' &&
|
|
94
|
+
link.position?.start.offset === 0 &&
|
|
95
|
+
(link.url ?? '').startsWith('#'));
|
|
96
|
+
}
|
|
5
97
|
/** Mask fenced examples without changing authored source line numbers. */
|
|
6
98
|
function unfencedLines(markdown) {
|
|
7
99
|
let fence;
|
|
@@ -34,14 +126,14 @@ export function parseIndexContents(markdown, sourcePath = 'index.md', options =
|
|
|
34
126
|
const lines = unfencedLines(markdown);
|
|
35
127
|
const headingIndex = contentsHeadingIndex(lines);
|
|
36
128
|
if (headingIndex < 0) {
|
|
37
|
-
throw new
|
|
129
|
+
throw new DocsContentsError(`Missing required ## Contents section in ${sourcePath}`, sourcePath);
|
|
38
130
|
}
|
|
39
131
|
const entries = [];
|
|
40
132
|
let group;
|
|
41
133
|
let groupEntries = 0;
|
|
42
134
|
const checkGroup = () => {
|
|
43
135
|
if (group && groupEntries === 0) {
|
|
44
|
-
throw new
|
|
136
|
+
throw new DocsContentsError(`${sourcePath}:${group.line}: Contents group "${group.title}" has no entries`, sourcePath);
|
|
45
137
|
}
|
|
46
138
|
};
|
|
47
139
|
for (let i = headingIndex + 1; i < lines.length; i += 1) {
|
|
@@ -56,7 +148,7 @@ export function parseIndexContents(markdown, sourcePath = 'index.md', options =
|
|
|
56
148
|
continue;
|
|
57
149
|
}
|
|
58
150
|
if (/^\s*[*+]\s+\[[^\]]+\]\([^)]+\)/.test(line)) {
|
|
59
|
-
throw new
|
|
151
|
+
throw new DocsContentsError(`${sourcePath}:${i + 1}: Contents links must use a dash (-) bullet`, sourcePath);
|
|
60
152
|
}
|
|
61
153
|
const match = line.match(/^\s*-\s+\[([^\]]+)\]\(([^)]+)\)/);
|
|
62
154
|
if (match) {
|
|
@@ -72,6 +164,7 @@ export function parseIndexContents(markdown, sourcePath = 'index.md', options =
|
|
|
72
164
|
.replace(/^\s*[-–—:]\s*/, '')
|
|
73
165
|
.trim(),
|
|
74
166
|
separator: /^\s*([-–—:])/.exec(line.slice(match[0].length))?.[1],
|
|
167
|
+
source: line.slice(match[0].indexOf('[')),
|
|
75
168
|
}
|
|
76
169
|
: {}),
|
|
77
170
|
});
|
|
@@ -80,22 +173,47 @@ export function parseIndexContents(markdown, sourcePath = 'index.md', options =
|
|
|
80
173
|
}
|
|
81
174
|
checkGroup();
|
|
82
175
|
if (entries.length === 0 && !options.allowEmpty) {
|
|
83
|
-
throw new
|
|
176
|
+
throw new DocsContentsError(`No machine-readable links found under ## Contents in ${sourcePath}`, sourcePath);
|
|
84
177
|
}
|
|
85
178
|
return entries;
|
|
86
179
|
}
|
|
87
180
|
function toPosixPath(path) {
|
|
88
181
|
return path.replaceAll('\\', '/');
|
|
89
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolve one Contents entry to the page or section it places in navigation.
|
|
185
|
+
*
|
|
186
|
+
* Contents is machine-readable navigation, so every entry must target a page
|
|
187
|
+
* or a section index. The one exception is an index-only leaf: a single-page
|
|
188
|
+
* section may list same-page heading anchors as an in-page table of contents.
|
|
189
|
+
* Those resolve to `anchor` and place nothing in navigation; the `links` check
|
|
190
|
+
* rule validates that each heading exists.
|
|
191
|
+
*/
|
|
90
192
|
export async function resolveEntryTarget(docsRoot, directoryPath, dirRelativePath, entry,
|
|
91
193
|
/**
|
|
92
194
|
* Page extensions a Contents link may target. MkDocs renders only `.md`;
|
|
93
195
|
* Fumadocs also renders `.mdx` pages.
|
|
94
196
|
*/
|
|
95
|
-
pageExtensions = ['.md'], matcher) {
|
|
197
|
+
pageExtensions = ['.md'], matcher, options = {}) {
|
|
198
|
+
const indexPath = join(docsRoot, dirRelativePath, 'index.md');
|
|
199
|
+
const fail = (message) => {
|
|
200
|
+
throw new DocsContentsError(message, indexPath, entry.line);
|
|
201
|
+
};
|
|
202
|
+
if (isSamePageAnchor(entry.href)) {
|
|
203
|
+
if (!options.indexOnlyLeaf) {
|
|
204
|
+
fail(`Contents link "${entry.href}" in ${indexPath} is a same-page anchor; Contents must link pages or sections unless index.md is the only reader page in its directory and it has no child sections.`);
|
|
205
|
+
}
|
|
206
|
+
if (!isMarkdownSamePageLink(entry)) {
|
|
207
|
+
fail(`Contents link "${entry.href}" in ${indexPath} is not a valid Markdown link; fix its destination (escape or remove spaces and unbalanced parentheses).`);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
kind: 'anchor',
|
|
211
|
+
path: toPosixPath(posix.join(dirRelativePath, 'index.md')),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
96
214
|
const href = entry.href.split('#', 1)[0]?.trim() ?? '';
|
|
97
215
|
if (!href) {
|
|
98
|
-
|
|
216
|
+
fail(`Invalid contents link "${entry.href}" in ${indexPath}`);
|
|
99
217
|
}
|
|
100
218
|
let targetPath = resolve(directoryPath, href);
|
|
101
219
|
if (!href.endsWith('.md')) {
|
|
@@ -108,16 +226,16 @@ pageExtensions = ['.md'], matcher) {
|
|
|
108
226
|
if (relativeTargetPath.startsWith('../') ||
|
|
109
227
|
relativeTargetPath === '..' ||
|
|
110
228
|
relativeTargetPath.length === 0) {
|
|
111
|
-
|
|
229
|
+
fail(`Contents link "${entry.href}" escapes the docs directory from ${indexPath}`);
|
|
112
230
|
}
|
|
113
231
|
if (matcher && isExcludedPath(matcher, relativeTargetPath)) {
|
|
114
232
|
return { kind: 'excluded', path: relativeTargetPath };
|
|
115
233
|
}
|
|
116
234
|
if (!(await fileExists(targetPath))) {
|
|
117
|
-
|
|
235
|
+
fail(`Contents link "${entry.href}" in ${indexPath} does not resolve to a Markdown file`);
|
|
118
236
|
}
|
|
119
237
|
if (!pageExtensions.includes(extname(targetPath))) {
|
|
120
|
-
|
|
238
|
+
fail(`Contents link "${entry.href}" in ${indexPath} must point to a Markdown file or directory index`);
|
|
121
239
|
}
|
|
122
240
|
if (basename(targetPath) === 'index.md') {
|
|
123
241
|
return {
|
|
@@ -139,16 +257,20 @@ export async function buildDocsNavTree(options) {
|
|
|
139
257
|
throw new Error(`Missing required index.md at ${indexPath}`);
|
|
140
258
|
}
|
|
141
259
|
const indexMarkdown = await readFile(indexPath, 'utf8');
|
|
142
|
-
const contentsEntries = parseIndexContents(indexMarkdown, indexPath
|
|
260
|
+
const contentsEntries = parseIndexContents(indexMarkdown, indexPath, {
|
|
261
|
+
locations: true,
|
|
262
|
+
});
|
|
143
263
|
const matcher = createExclusionMatcher(options.nonReaderPaths ?? []);
|
|
264
|
+
const pagePaths = options.pagePaths ?? (await inventoryDocs(options.docsRoot, matcher)).pages;
|
|
265
|
+
const indexOnlyLeaf = isIndexOnlyLeaf(dirRelativePath, pagePaths);
|
|
144
266
|
const nav = dirRelativePath === '.'
|
|
145
267
|
? [{ Home: 'index.md' }]
|
|
146
268
|
: [toPosixPath(join(dirRelativePath, 'index.md'))];
|
|
147
269
|
let currentGroup;
|
|
148
270
|
let groupNav = nav;
|
|
149
271
|
for (const entry of contentsEntries) {
|
|
150
|
-
const resolvedTarget = await resolveEntryTarget(options.docsRoot, directoryPath, dirRelativePath, entry, ['.md'], matcher);
|
|
151
|
-
if (resolvedTarget.kind === 'excluded')
|
|
272
|
+
const resolvedTarget = await resolveEntryTarget(options.docsRoot, directoryPath, dirRelativePath, entry, ['.md'], matcher, { indexOnlyLeaf });
|
|
273
|
+
if (resolvedTarget.kind === 'excluded' || resolvedTarget.kind === 'anchor')
|
|
152
274
|
continue;
|
|
153
275
|
if (resolvedTarget.kind === 'section' &&
|
|
154
276
|
resolvedTarget.path === toPosixPath(join(dirRelativePath, 'index.md'))) {
|
|
@@ -175,6 +297,7 @@ export async function buildDocsNavTree(options) {
|
|
|
175
297
|
dirRelativePath: childDirRelativePath,
|
|
176
298
|
visitedDirectories: childVisitedDirectories,
|
|
177
299
|
nonReaderPaths: options.nonReaderPaths,
|
|
300
|
+
pagePaths,
|
|
178
301
|
}),
|
|
179
302
|
});
|
|
180
303
|
}
|
|
@@ -27,6 +27,9 @@ export interface SyncFumadocsNavigationResult {
|
|
|
27
27
|
*/
|
|
28
28
|
unlisted: string[];
|
|
29
29
|
}
|
|
30
|
+
export declare const FUMADOCS_REST_ITEMS: ReadonlySet<string>;
|
|
31
|
+
export declare const FUMADOCS_SEPARATOR: RegExp;
|
|
32
|
+
export declare const FUMADOCS_LINK: RegExp;
|
|
30
33
|
/**
|
|
31
34
|
* Reduce a Markdown heading to the plain text a sidebar shows: images and
|
|
32
35
|
* links keep their text, code spans keep their content, and emphasis and
|
|
@@ -35,6 +38,12 @@ export interface SyncFumadocsNavigationResult {
|
|
|
35
38
|
export declare function stripInlineMarkdown(text: string): string;
|
|
36
39
|
/** Folder title: frontmatter `title`, else the first H1 outside code fences. */
|
|
37
40
|
export declare function resolveFolderTitle(markdown: string): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The URL Fumadocs assigns a page, following `getSlugs` in fumadocs-core's
|
|
43
|
+
* slugs plugin with the scaffolded `baseUrl: '/'`: group folders are dropped,
|
|
44
|
+
* segments are URI-encoded, and an `index` file maps to its folder.
|
|
45
|
+
*/
|
|
46
|
+
export declare function pageUrl(pagePath: string, nonIndexUrls?: ReadonlySet<string>): string;
|
|
38
47
|
/**
|
|
39
48
|
* The `pages` item for a page or folder in the same directory. A name the
|
|
40
49
|
* builder would read as a directive (`!hidden`, `z...a`, `---`, ...) is
|
|
@@ -42,6 +51,26 @@ export declare function resolveFolderTitle(markdown: string): string | undefined
|
|
|
42
51
|
* the same file; every other name is written as is.
|
|
43
52
|
*/
|
|
44
53
|
export declare function toFumadocsPageItem(name: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Build one folder's `pages` from its index.md Contents map.
|
|
56
|
+
*
|
|
57
|
+
* A page in this folder or an immediate subfolder is referenced by name.
|
|
58
|
+
* Local same-name page/folder collisions use a precise source-file item, as
|
|
59
|
+
* does `../contributing.md` from its own contributing/ map: pinned core16.10.2
|
|
60
|
+
* selects the folder first for an extensionless reference. Other non-local
|
|
61
|
+
* targets become link entries. There is never a `"..."` rest entry: pages the Contents map does
|
|
62
|
+
* not list stay out of the tree and are reported instead.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Whether the fumadocs-core 16.10.2 page-tree builder supplies this folder's
|
|
66
|
+
* `index` page on its own, given the folder's effective metadata.
|
|
67
|
+
*
|
|
68
|
+
* `buildFolder` attaches `<folder>/index` as the folder index only when the
|
|
69
|
+
* folder is not a root folder (the docs root, or a preserved `root: true`)
|
|
70
|
+
* and no `pagesIndex` names a different page. Otherwise the page is shown
|
|
71
|
+
* only if `pages` lists it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function hasImplicitIndex(relativeDir: string, existing: Record<string, unknown> | null): boolean;
|
|
45
74
|
/**
|
|
46
75
|
* Write one strict `meta.json` per docs directory that has an index.md,
|
|
47
76
|
* derived from that index's Contents map, and report every page the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fumadocs.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/fumadocs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fumadocs.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/fumadocs.ts"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAMD,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAGlD,CAAC;AACH,eAAO,MAAM,kBAAkB,QACoB,CAAC;AACpD,eAAO,MAAM,aAAa,QAC6D,CAAC;AASxF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQxD;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoCvE;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,MAAM,CAgBR;AAYD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAOD;;;;;;;;;GASG;AACH;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACvC,OAAO,CAUT;AAkLD;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,GAAG,OAAO,CAAC,4BAA4B,CAAC,CA8ExC"}
|
|
@@ -1,54 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, extname, join, posix } from 'node:path';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
import { createExclusionMatcher, } from '../../docs/shared/exclusions.js';
|
|
5
5
|
import { getFrontmatterBlock, parseFrontmatterScalarFields, } from '../../shared/frontmatter.js';
|
|
6
6
|
import { fileExists } from '../../../fs/io.js';
|
|
7
|
-
import { parseIndexContents, resolveEntryTarget, } from './contents.js';
|
|
8
|
-
const PAGE_EXTENSIONS = ['.md', '.mdx'];
|
|
7
|
+
import { inventoryDocs, isIndexOnlyLeaf, PAGE_EXTENSIONS, parseIndexContents, resolveEntryTarget, } from './contents.js';
|
|
9
8
|
const GROUP_FOLDER = /^\(.+\)$/;
|
|
10
9
|
// The `pages` item grammar of the fumadocs-core 16.10.2 page-tree builder
|
|
11
10
|
// (`resolveFolderItem`): exact rest entries, separators, links, and the
|
|
12
11
|
// exclude and extract prefixes are checked before an item is read as a path.
|
|
13
|
-
const FUMADOCS_REST_ITEMS = new Set([
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
export const FUMADOCS_REST_ITEMS = new Set([
|
|
13
|
+
'...',
|
|
14
|
+
'z...a',
|
|
15
|
+
]);
|
|
16
|
+
export const FUMADOCS_SEPARATOR = /^---(?:\[(?<icon>[^\]]+)])?(?<name>.+)---|^---$/;
|
|
17
|
+
export const FUMADOCS_LINK = /^(?<external>external:)?(?:\[(?<icon>[^\]]+)])?\[(?<name>[^\]]+)]\((?<url>[^)]+)\)$/;
|
|
16
18
|
const FUMADOCS_PREFIXES = ['!', '...'];
|
|
17
19
|
const H1 = /^#\s+(.+?)\s*#*\s*$/;
|
|
18
20
|
const FENCE = /^\s*(```|~~~)/;
|
|
19
21
|
function parentDir(path) {
|
|
20
22
|
return posix.dirname(path);
|
|
21
23
|
}
|
|
22
|
-
async function inventoryDocs(docsRoot, matcher) {
|
|
23
|
-
const directories = [];
|
|
24
|
-
const pages = [];
|
|
25
|
-
async function walk(relativeDir) {
|
|
26
|
-
directories.push(relativeDir);
|
|
27
|
-
const entries = await readdir(join(docsRoot, relativeDir), {
|
|
28
|
-
withFileTypes: true,
|
|
29
|
-
});
|
|
30
|
-
for (const entry of entries) {
|
|
31
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
const entryPath = posix.join(relativeDir, entry.name);
|
|
35
|
-
if (entry.isDirectory()) {
|
|
36
|
-
if (matcher.excludesDirectory(entryPath))
|
|
37
|
-
continue;
|
|
38
|
-
await walk(entryPath);
|
|
39
|
-
}
|
|
40
|
-
else if (entry.isFile() &&
|
|
41
|
-
PAGE_EXTENSIONS.includes(extname(entry.name))) {
|
|
42
|
-
if (!matcher.excludesFile(entryPath))
|
|
43
|
-
pages.push(entryPath);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
await walk('.');
|
|
48
|
-
directories.sort();
|
|
49
|
-
pages.sort();
|
|
50
|
-
return { directories, pages };
|
|
51
|
-
}
|
|
52
24
|
/**
|
|
53
25
|
* Reduce a Markdown heading to the plain text a sidebar shows: images and
|
|
54
26
|
* links keep their text, code spans keep their content, and emphasis and
|
|
@@ -104,7 +76,7 @@ export function resolveFolderTitle(markdown) {
|
|
|
104
76
|
* slugs plugin with the scaffolded `baseUrl: '/'`: group folders are dropped,
|
|
105
77
|
* segments are URI-encoded, and an `index` file maps to its folder.
|
|
106
78
|
*/
|
|
107
|
-
function pageUrl(pagePath, nonIndexUrls = new Set()) {
|
|
79
|
+
export function pageUrl(pagePath, nonIndexUrls = new Set()) {
|
|
108
80
|
const slugs = parentDir(pagePath)
|
|
109
81
|
.split('/')
|
|
110
82
|
.filter((segment) => segment.length > 0 && segment !== '.')
|
|
@@ -160,7 +132,7 @@ function hrefFragment(href) {
|
|
|
160
132
|
* and no `pagesIndex` names a different page. Otherwise the page is shown
|
|
161
133
|
* only if `pages` lists it.
|
|
162
134
|
*/
|
|
163
|
-
function hasImplicitIndex(relativeDir, existing) {
|
|
135
|
+
export function hasImplicitIndex(relativeDir, existing) {
|
|
164
136
|
if (relativeDir === '.' ||
|
|
165
137
|
existing?.root === true ||
|
|
166
138
|
(typeof existing?.root === 'string' && existing.root.trim().length > 0)) {
|
|
@@ -171,7 +143,8 @@ function hasImplicitIndex(relativeDir, existing) {
|
|
|
171
143
|
}
|
|
172
144
|
async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex, matcher, inventory, nonIndexUrls) {
|
|
173
145
|
const indexPath = join(docsRoot, relativeDir, 'index.md');
|
|
174
|
-
const entries = parseIndexContents(markdown, indexPath);
|
|
146
|
+
const entries = parseIndexContents(markdown, indexPath, { locations: true });
|
|
147
|
+
const indexOnlyLeaf = isIndexOnlyLeaf(relativeDir, inventory.pages);
|
|
175
148
|
// A folder the loader gives no implicit index (the docs root, a `root: true`
|
|
176
149
|
// folder) must list `index` to show its landing page. Elsewhere, listing
|
|
177
150
|
// `index` would demote the folder index to an ordinary child, so it is left
|
|
@@ -179,8 +152,10 @@ async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex,
|
|
|
179
152
|
const pages = implicitIndex ? [] : ['index'];
|
|
180
153
|
let currentGroup;
|
|
181
154
|
for (const entry of entries) {
|
|
182
|
-
const target = await resolveEntryTarget(docsRoot, join(docsRoot, relativeDir), relativeDir, entry, PAGE_EXTENSIONS, matcher);
|
|
183
|
-
|
|
155
|
+
const target = await resolveEntryTarget(docsRoot, join(docsRoot, relativeDir), relativeDir, entry, PAGE_EXTENSIONS, matcher, { indexOnlyLeaf });
|
|
156
|
+
// Same-page anchors in an index-only leaf are an in-page table of
|
|
157
|
+
// contents, not navigation.
|
|
158
|
+
if (target.kind === 'excluded' || target.kind === 'anchor')
|
|
184
159
|
continue;
|
|
185
160
|
const targetDir = parentDir(target.path);
|
|
186
161
|
let item;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type FumadocsMetaFile } from './fumadocs.js';
|
|
2
|
+
/**
|
|
3
|
+
* One way a hand-authored (or undeclared) sidebar disagrees with the authored
|
|
4
|
+
* Contents maps. The comparison checks membership and relative order only:
|
|
5
|
+
* separators, labels, link items, rest entries and pages borrowed from another
|
|
6
|
+
* folder are the author's presentation and are never compared with generator
|
|
7
|
+
* output.
|
|
8
|
+
*/
|
|
9
|
+
export interface NavMembershipIssue {
|
|
10
|
+
/** Content-relative path of the meta.json, or `mkdocs.yml`. */
|
|
11
|
+
path: string;
|
|
12
|
+
kind: 'missing' | 'dangling' | 'order' | 'malformed' | 'unreadable';
|
|
13
|
+
message: string;
|
|
14
|
+
line?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compare present Fumadocs `meta.json` files with the Contents-derived
|
|
18
|
+
* expectation in `metaFiles` (from a `check` run of nav sync).
|
|
19
|
+
*
|
|
20
|
+
* A Contents entry is reachable when its folder's `pages` lists it, a rest
|
|
21
|
+
* entry shows it, any other `pages` entry borrows it, or a root-relative link
|
|
22
|
+
* item points at its URL. An absent meta.json or one without `pages` uses the
|
|
23
|
+
* loader's folder order and is skipped.
|
|
24
|
+
*/
|
|
25
|
+
export declare function compareFumadocsMembership(docsRoot: string, metaFiles: readonly FumadocsMetaFile[]): Promise<NavMembershipIssue[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Compare a hand-authored MkDocs `nav:` with the Contents maps: every
|
|
28
|
+
* Contents page appears, every local nav page exists, and the entries of
|
|
29
|
+
* each Contents map (pages and section landing pages) keep their order. Section labels, nesting and external
|
|
30
|
+
* links are the author's. Returns no issues when `nav` is absent.
|
|
31
|
+
*/
|
|
32
|
+
export declare function compareMkDocsMembership(options: {
|
|
33
|
+
appRoot: string;
|
|
34
|
+
docsRoot: string;
|
|
35
|
+
nonReaderPaths?: readonly string[];
|
|
36
|
+
}): Promise<NavMembershipIssue[]>;
|
|
37
|
+
//# sourceMappingURL=membership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"membership.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/membership.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,GAAG,YAAY,CAAC;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAoHD;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,gBAAgB,EAAE,GACrC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAoS/B;AAeD;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAwEhC"}
|