@minidocs/core 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +11 -2
- package/dist/index.d.mts +138 -0
- package/dist/index.mjs +275 -0
- package/package.json +38 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan1F
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @minidocs/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-agnostic logic behind [minidocs](https://github.com/Ivan-1F/minidocs): page trees, navigation state, previous and next pages, and tables of contents.
|
|
4
|
+
|
|
5
|
+
Most sites use it through [`@minidocs/astro`](https://www.npmjs.com/package/@minidocs/astro), which re-exports what pages need from `@minidocs/astro/runtime`.
|
|
6
|
+
|
|
7
|
+
> [!WARNING]
|
|
8
|
+
> minidocs is experimental. Until 1.0, minor versions may include breaking changes.
|
|
9
|
+
|
|
10
|
+
## License
|
|
11
|
+
|
|
12
|
+
[MIT](./LICENSE)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//#region src/path.d.ts
|
|
2
|
+
interface ParsedPath<L extends string> {
|
|
3
|
+
locale: L;
|
|
4
|
+
/**
|
|
5
|
+
* Locale-free page id, e.g. `concepts/primitives`. Directory index files use the directory path, the root index uses `index`.
|
|
6
|
+
* Route groups, directories named in parentheses like `(basics)`, are left out.
|
|
7
|
+
*/
|
|
8
|
+
id: string;
|
|
9
|
+
/** Directory the page belongs to as it is on disk, route groups included, `''` for the root. An index file belongs to its own directory. */
|
|
10
|
+
dir: string;
|
|
11
|
+
isIndex: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Derives the locale and page id of a content file from its path.
|
|
15
|
+
* With `localeDirs: false`, files are not inside locale directories and all belong to the only locale.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseFilePath<L extends string>(filePath: string, base: string, locales: readonly L[], localeDirs?: boolean): ParsedPath<L>;
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/content.d.ts
|
|
20
|
+
interface EntryLike {
|
|
21
|
+
filePath?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
interface ContentOptions<L extends string> {
|
|
24
|
+
/** Collection root that `entry.filePath` starts with, e.g. `src/content/docs`. */
|
|
25
|
+
base: string;
|
|
26
|
+
locales: readonly L[];
|
|
27
|
+
/** Locales to try, in order, when a page is not translated. Locales without an entry never fall back. */
|
|
28
|
+
fallback?: Partial<Record<L, L | readonly L[]>>;
|
|
29
|
+
/**
|
|
30
|
+
* Whether content files sit inside locale directories. Defaults to `true`.
|
|
31
|
+
* Single-language sites set it to `false` with exactly one locale, which every file then belongs to.
|
|
32
|
+
*/
|
|
33
|
+
localeDirs?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** A page as seen from one locale, possibly served with content from a fallback locale. */
|
|
36
|
+
interface ResolvedPage<L extends string, E> {
|
|
37
|
+
id: string;
|
|
38
|
+
/** Locale the page is shown in. */
|
|
39
|
+
locale: L;
|
|
40
|
+
/** Locale the content actually comes from. Differs from `locale` only for fallback pages. */
|
|
41
|
+
contentLocale: L;
|
|
42
|
+
entry: E;
|
|
43
|
+
isFallback: boolean;
|
|
44
|
+
path: ParsedPath<L>;
|
|
45
|
+
}
|
|
46
|
+
/** Lists every page that needs a route, in every locale, including untranslated pages served through fallback. */
|
|
47
|
+
export declare function listPages<L extends string, E extends EntryLike>(entries: readonly E[], options: ContentOptions<L>): Omit<ResolvedPage<L, E>, 'path'>[];
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/query.d.ts
|
|
50
|
+
/** The part of a node the queries rely on, so callers can pass trimmed-down node shapes. */
|
|
51
|
+
interface AnyNode {
|
|
52
|
+
page?: {
|
|
53
|
+
id: string;
|
|
54
|
+
} | undefined;
|
|
55
|
+
children: AnyNode[];
|
|
56
|
+
}
|
|
57
|
+
/** Returns the previous and next page of a page in reading order. */
|
|
58
|
+
export declare function neighbors<N extends AnyNode>(nodes: N[], id: string): {
|
|
59
|
+
prev: N | undefined;
|
|
60
|
+
next: N | undefined;
|
|
61
|
+
};
|
|
62
|
+
/** Returns the nodes from the root down to the first occurrence of a page, or an empty array if it is not in the tree. */
|
|
63
|
+
export declare function findPath<N extends AnyNode>(nodes: N[], id: string): N[];
|
|
64
|
+
interface NavState<N extends AnyNode> {
|
|
65
|
+
/** Whether a node is the current page, and whether the current page is the node itself or below it. */
|
|
66
|
+
of(node: N): {
|
|
67
|
+
current: boolean;
|
|
68
|
+
open: boolean;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Tracks where the current page sits in a tree, for marking and expanding navigation. */
|
|
72
|
+
export declare function navState<N extends AnyNode>(nodes: N[], id: string): NavState<N>;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/toc.d.ts
|
|
75
|
+
/** A heading as returned by Astro's `render(entry)`. */
|
|
76
|
+
interface Heading {
|
|
77
|
+
depth: number;
|
|
78
|
+
slug: string;
|
|
79
|
+
text: string;
|
|
80
|
+
}
|
|
81
|
+
interface TocNode extends Heading {
|
|
82
|
+
children: TocNode[];
|
|
83
|
+
}
|
|
84
|
+
interface TocOptions {
|
|
85
|
+
/** Inclusive range of heading levels to keep. Defaults to `[2, 3]`. */
|
|
86
|
+
depth?: [min: number, max: number];
|
|
87
|
+
}
|
|
88
|
+
/** Nests a flat heading list into a tree. A heading that skips levels goes under the closest shallower heading. */
|
|
89
|
+
export declare function tocTree(headings: readonly Heading[], options?: TocOptions): TocNode[];
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/tree.d.ts
|
|
92
|
+
interface PageRef<Id extends string, L extends string, E> {
|
|
93
|
+
id: Id;
|
|
94
|
+
/** Locale of the tree this node belongs to. */
|
|
95
|
+
locale: L;
|
|
96
|
+
/** Locale `entry` comes from. Differs from `locale` only for fallback pages. */
|
|
97
|
+
contentLocale: L;
|
|
98
|
+
entry: E;
|
|
99
|
+
/** True when `entry` comes from a fallback locale because the page is not translated. */
|
|
100
|
+
isFallback: boolean;
|
|
101
|
+
}
|
|
102
|
+
interface Node<Id extends string = string, L extends string = string, E = EntryLike> {
|
|
103
|
+
label: string;
|
|
104
|
+
page?: PageRef<Id, L, E>;
|
|
105
|
+
/** External link target, never set together with `page`. */
|
|
106
|
+
href?: string;
|
|
107
|
+
children: Node<Id, L, E>[];
|
|
108
|
+
}
|
|
109
|
+
type Tree<Id extends string, L extends string, E> = Record<L, Node<Id, L, E>[]>;
|
|
110
|
+
interface TreeOptions<L extends string, E> extends ContentOptions<L> {
|
|
111
|
+
label: (entry: E) => string;
|
|
112
|
+
}
|
|
113
|
+
interface DirOptions<Id extends string, L extends string, E> {
|
|
114
|
+
/** Orders siblings. Without it, siblings are ordered by file or directory name. */
|
|
115
|
+
compare?: (a: Node<Id, L, E>, b: Node<Id, L, E>) => number;
|
|
116
|
+
}
|
|
117
|
+
interface BuildContext<Id extends string, L extends string, E> {
|
|
118
|
+
locale: L;
|
|
119
|
+
page: (id: Id, children?: Node<Id, L, E>[]) => Node<Id, L, E>;
|
|
120
|
+
group: (label: string, children: Node<Id, L, E>[]) => Node<Id, L, E>;
|
|
121
|
+
/**
|
|
122
|
+
* Returns the contents of a directory, excluding the directory's own index page.
|
|
123
|
+
* Route groups inside it add no level: their pages and directories join their siblings.
|
|
124
|
+
*/
|
|
125
|
+
dir: (path: string, options?: DirOptions<Id, L, E>) => Node<Id, L, E>[];
|
|
126
|
+
link: (label: string, href: string) => Node<Id, L, E>;
|
|
127
|
+
}
|
|
128
|
+
type Build<Id extends string, L extends string, E> = (ctx: BuildContext<Id, L, E>) => Node<Id, L, E>[] | Promise<Node<Id, L, E>[]>;
|
|
129
|
+
/** Thrown by `page()` for an id that the given entries do not contain in the locale or its fallbacks. */
|
|
130
|
+
export declare class MissingPageError extends Error {
|
|
131
|
+
readonly id: string;
|
|
132
|
+
readonly locale: string;
|
|
133
|
+
constructor(id: string, locale: string, searched: readonly string[]);
|
|
134
|
+
}
|
|
135
|
+
/** Builds one navigation tree per locale by running `build` once for each locale. */
|
|
136
|
+
export declare function createTree<Id extends string, L extends string, E extends EntryLike>(entries: readonly E[], options: TreeOptions<L, E>, build: Build<Id, L, E>): Promise<Tree<Id, L, E>>;
|
|
137
|
+
//#endregion
|
|
138
|
+
export type { BuildContext, ContentOptions, DirOptions, EntryLike, Heading, NavState, Node, PageRef, ParsedPath, ResolvedPage, TocNode, TocOptions, Tree, TreeOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
//#region src/path.ts
|
|
2
|
+
/** Whether a directory name is a route group, which organizes files without becoming part of page ids. */
|
|
3
|
+
function isRouteGroup(name) {
|
|
4
|
+
return /^\(.+\)$/.test(name);
|
|
5
|
+
}
|
|
6
|
+
function normalize(path) {
|
|
7
|
+
return path.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Derives the locale and page id of a content file from its path.
|
|
11
|
+
* With `localeDirs: false`, files are not inside locale directories and all belong to the only locale.
|
|
12
|
+
*/
|
|
13
|
+
function parseFilePath(filePath, base, locales, localeDirs = true) {
|
|
14
|
+
const file = normalize(filePath);
|
|
15
|
+
const root = normalize(base);
|
|
16
|
+
if (!file.startsWith(`${root}/`)) throw new Error(`File "${filePath}" is outside the content base "${base}".`);
|
|
17
|
+
const segments = file.slice(root.length + 1).split("/");
|
|
18
|
+
const locale = localeDirs ? segments.shift() : locales[0];
|
|
19
|
+
if (segments.length === 0 || !locales.includes(locale)) throw new Error(`File "${filePath}" must be inside a locale directory (${locales.join(", ")}).`);
|
|
20
|
+
const name = segments.pop().replace(/\.[^.]+$/, "");
|
|
21
|
+
const dir = segments.join("/");
|
|
22
|
+
const route = segments.filter((segment) => !isRouteGroup(segment)).join("/");
|
|
23
|
+
if (name === "index") return {
|
|
24
|
+
locale,
|
|
25
|
+
id: route || "index",
|
|
26
|
+
dir,
|
|
27
|
+
isIndex: true
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
locale,
|
|
31
|
+
id: route ? `${route}/${name}` : name,
|
|
32
|
+
dir,
|
|
33
|
+
isIndex: false
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/content.ts
|
|
38
|
+
function indexContent(entries, options) {
|
|
39
|
+
const { base, locales, fallback = {}, localeDirs = true } = options;
|
|
40
|
+
if (!localeDirs && locales.length !== 1) throw new Error("Content without locale directories must have exactly one locale.");
|
|
41
|
+
const chains = /* @__PURE__ */ new Map();
|
|
42
|
+
for (const locale of locales) chains.set(locale, [locale]);
|
|
43
|
+
for (const [locale, targets] of Object.entries(fallback)) {
|
|
44
|
+
if (!locales.includes(locale)) throw new Error(`Fallback is declared for unknown locale "${locale}".`);
|
|
45
|
+
for (const target of [targets ?? []].flat()) {
|
|
46
|
+
if (!locales.includes(target)) throw new Error(`Locale "${locale}" falls back to unknown locale "${target}".`);
|
|
47
|
+
const chain = chains.get(locale);
|
|
48
|
+
if (!chain.includes(target)) chain.push(target);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const own = new Map(locales.map((locale) => [locale, /* @__PURE__ */ new Map()]));
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (!entry.filePath) throw new Error("Every entry must have a filePath.");
|
|
54
|
+
const path = parseFilePath(entry.filePath, base, locales, localeDirs);
|
|
55
|
+
const pages = own.get(path.locale);
|
|
56
|
+
const existing = pages.get(path.id);
|
|
57
|
+
if (existing) throw new Error(`"${existing.entry.filePath}" and "${entry.filePath}" both map to the page id "${path.id}".`);
|
|
58
|
+
pages.set(path.id, {
|
|
59
|
+
entry,
|
|
60
|
+
path
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const cache = /* @__PURE__ */ new Map();
|
|
64
|
+
return {
|
|
65
|
+
chain: (locale) => chains.get(locale),
|
|
66
|
+
visible(locale) {
|
|
67
|
+
let visible = cache.get(locale);
|
|
68
|
+
if (visible) return visible;
|
|
69
|
+
visible = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const contentLocale of chains.get(locale)) for (const [id, { entry, path }] of own.get(contentLocale)) {
|
|
71
|
+
if (visible.has(id)) continue;
|
|
72
|
+
visible.set(id, {
|
|
73
|
+
id,
|
|
74
|
+
locale,
|
|
75
|
+
contentLocale,
|
|
76
|
+
entry,
|
|
77
|
+
isFallback: contentLocale !== locale,
|
|
78
|
+
path
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
cache.set(locale, visible);
|
|
82
|
+
return visible;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Lists every page that needs a route, in every locale, including untranslated pages served through fallback. */
|
|
87
|
+
function listPages(entries, options) {
|
|
88
|
+
const index = indexContent(entries, options);
|
|
89
|
+
return options.locales.flatMap((locale) => [...index.visible(locale).values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map(({ path: _, ...page }) => page));
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/query.ts
|
|
93
|
+
/** Page nodes in reading order (depth-first, parents before children), keeping only the first occurrence of each page. */
|
|
94
|
+
function pages(nodes, seen = /* @__PURE__ */ new Set(), out = []) {
|
|
95
|
+
for (const node of nodes) {
|
|
96
|
+
if (node.page && !seen.has(node.page.id)) {
|
|
97
|
+
seen.add(node.page.id);
|
|
98
|
+
out.push(node);
|
|
99
|
+
}
|
|
100
|
+
pages(node.children, seen, out);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** Returns the previous and next page of a page in reading order. */
|
|
105
|
+
function neighbors(nodes, id) {
|
|
106
|
+
const list = pages(nodes);
|
|
107
|
+
const index = list.findIndex((node) => node.page.id === id);
|
|
108
|
+
if (index === -1) return {
|
|
109
|
+
prev: void 0,
|
|
110
|
+
next: void 0
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
prev: list[index - 1],
|
|
114
|
+
next: list[index + 1]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Returns the nodes from the root down to the first occurrence of a page, or an empty array if it is not in the tree. */
|
|
118
|
+
function findPath(nodes, id) {
|
|
119
|
+
for (const node of nodes) {
|
|
120
|
+
if (node.page?.id === id) return [node];
|
|
121
|
+
const rest = findPath(node.children, id);
|
|
122
|
+
if (rest.length > 0) return [node, ...rest];
|
|
123
|
+
}
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
/** Tracks where the current page sits in a tree, for marking and expanding navigation. */
|
|
127
|
+
function navState(nodes, id) {
|
|
128
|
+
const path = findPath(nodes, id);
|
|
129
|
+
const current = path.at(-1);
|
|
130
|
+
return { of: (node) => ({
|
|
131
|
+
current: node === current,
|
|
132
|
+
open: path.includes(node)
|
|
133
|
+
}) };
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/toc.ts
|
|
137
|
+
/** Nests a flat heading list into a tree. A heading that skips levels goes under the closest shallower heading. */
|
|
138
|
+
function tocTree(headings, options = {}) {
|
|
139
|
+
const [min, max] = options.depth ?? [2, 3];
|
|
140
|
+
const root = [];
|
|
141
|
+
const stack = [];
|
|
142
|
+
for (const { depth, slug, text } of headings) {
|
|
143
|
+
if (depth < min || depth > max) continue;
|
|
144
|
+
const node = {
|
|
145
|
+
depth,
|
|
146
|
+
slug,
|
|
147
|
+
text,
|
|
148
|
+
children: []
|
|
149
|
+
};
|
|
150
|
+
while (stack.length > 0 && stack.at(-1).depth >= depth) stack.pop();
|
|
151
|
+
(stack.at(-1)?.children ?? root).push(node);
|
|
152
|
+
stack.push(node);
|
|
153
|
+
}
|
|
154
|
+
return root;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/tree.ts
|
|
158
|
+
/** Thrown by `page()` for an id that the given entries do not contain in the locale or its fallbacks. */
|
|
159
|
+
var MissingPageError = class extends Error {
|
|
160
|
+
id;
|
|
161
|
+
locale;
|
|
162
|
+
constructor(id, locale, searched) {
|
|
163
|
+
super(`Page "${id}" does not exist in locale "${locale}" (searched: ${searched.join(", ")}).`);
|
|
164
|
+
this.id = id;
|
|
165
|
+
this.locale = locale;
|
|
166
|
+
this.name = "MissingPageError";
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
/** Builds one navigation tree per locale by running `build` once for each locale. */
|
|
170
|
+
async function createTree(entries, options, build) {
|
|
171
|
+
const { locales, label } = options;
|
|
172
|
+
const index = indexContent(entries, options);
|
|
173
|
+
const tree = {};
|
|
174
|
+
for (const locale of locales) tree[locale] = prune(await build(createContext(locale)));
|
|
175
|
+
return tree;
|
|
176
|
+
function createContext(locale) {
|
|
177
|
+
const visible = index.visible(locale);
|
|
178
|
+
function page(id, children = []) {
|
|
179
|
+
const resolved = visible.get(id);
|
|
180
|
+
if (!resolved) throw new MissingPageError(id, locale, index.chain(locale));
|
|
181
|
+
const { contentLocale, entry, isFallback } = resolved;
|
|
182
|
+
return {
|
|
183
|
+
label: label(entry),
|
|
184
|
+
page: {
|
|
185
|
+
id,
|
|
186
|
+
locale,
|
|
187
|
+
contentLocale,
|
|
188
|
+
entry,
|
|
189
|
+
isFallback
|
|
190
|
+
},
|
|
191
|
+
children
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function group(text, children) {
|
|
195
|
+
return {
|
|
196
|
+
label: text,
|
|
197
|
+
children
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function link(text, href) {
|
|
201
|
+
return {
|
|
202
|
+
label: text,
|
|
203
|
+
href,
|
|
204
|
+
children: []
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function dir(path, dirOptions = {}) {
|
|
208
|
+
const root = path.replace(/^\/+|\/+$/g, "");
|
|
209
|
+
const pages = [...visible.values()].map((resolved) => resolved.path);
|
|
210
|
+
if (!pages.some((p) => root === "" || p.dir === root || p.dir.startsWith(`${root}/`))) throw new Error(`Directory "${path}" does not contain any page in locale "${locale}".`);
|
|
211
|
+
return list(root);
|
|
212
|
+
function list(current) {
|
|
213
|
+
const items = collect(current);
|
|
214
|
+
items.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
215
|
+
const nodes = items.map((item) => item.node);
|
|
216
|
+
return dirOptions.compare ? nodes.sort(dirOptions.compare) : nodes;
|
|
217
|
+
}
|
|
218
|
+
function collect(current) {
|
|
219
|
+
const items = [];
|
|
220
|
+
const subdirs = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const p of pages) if (p.dir === current) {
|
|
222
|
+
if (!p.isIndex) items.push({
|
|
223
|
+
name: p.id.slice(p.id.lastIndexOf("/") + 1),
|
|
224
|
+
node: page(p.id)
|
|
225
|
+
});
|
|
226
|
+
} else if (current === "" || p.dir.startsWith(`${current}/`)) {
|
|
227
|
+
const rest = current === "" ? p.dir : p.dir.slice(current.length + 1);
|
|
228
|
+
subdirs.add(rest.split("/")[0]);
|
|
229
|
+
}
|
|
230
|
+
for (const name of subdirs) {
|
|
231
|
+
const full = current === "" ? name : `${current}/${name}`;
|
|
232
|
+
if (isRouteGroup(name)) {
|
|
233
|
+
items.push(...collect(full));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const children = list(full);
|
|
237
|
+
const index = pages.find((p) => p.isIndex && indexDirs(full).includes(p.dir));
|
|
238
|
+
items.push({
|
|
239
|
+
name,
|
|
240
|
+
node: index ? page(index.id, children) : group(name, children)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return items;
|
|
244
|
+
}
|
|
245
|
+
function indexDirs(full) {
|
|
246
|
+
const dirs = [full];
|
|
247
|
+
for (const p of pages) {
|
|
248
|
+
if (!p.dir.startsWith(`${full}/`)) continue;
|
|
249
|
+
if (p.dir.slice(full.length + 1).split("/").every(isRouteGroup)) dirs.push(p.dir);
|
|
250
|
+
}
|
|
251
|
+
return dirs;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
locale,
|
|
256
|
+
page,
|
|
257
|
+
group,
|
|
258
|
+
dir,
|
|
259
|
+
link
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Drops nodes that have no page, no link and no children left. */
|
|
264
|
+
function prune(nodes) {
|
|
265
|
+
return nodes.flatMap((node) => {
|
|
266
|
+
const children = prune(node.children);
|
|
267
|
+
if (!node.page && !node.href && children.length === 0) return [];
|
|
268
|
+
return [{
|
|
269
|
+
...node,
|
|
270
|
+
children
|
|
271
|
+
}];
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
export { MissingPageError, createTree, findPath, listPages, navState, neighbors, parseFilePath, tocTree };
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minidocs/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic logic for building documentation sites",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"documentation",
|
|
7
|
+
"docs"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/Ivan-1F/minidocs#readme",
|
|
10
|
+
"bugs": "https://github.com/Ivan-1F/minidocs/issues",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/Ivan-1F/minidocs.git",
|
|
14
|
+
"directory": "packages/core"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"default": "./dist/index.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsdown": "^0.23.1",
|
|
33
|
+
"typescript": "^7.0.2",
|
|
34
|
+
"vitest": "^5.0.5"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"test": "vitest run --passWithNoTests",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
}
|
|
6
41
|
}
|