@minidocs/astro 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 +26 -2
- package/dist/behaviors.d.mts +114 -0
- package/dist/behaviors.mjs +635 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.mjs +262 -0
- package/dist/links-UTLyoo_o.mjs +177 -0
- package/dist/loaders.d.mts +20 -0
- package/dist/loaders.mjs +76 -0
- package/dist/routing-TMqRCY2Q.mjs +48 -0
- package/dist/runtime.d.mts +160 -0
- package/dist/runtime.mjs +310 -0
- package/package.json +80 -3
- package/src/components/Prose.astro +9 -0
- package/src/components/callout/Root.astro +14 -0
- package/src/components/callout/Title.astro +9 -0
- package/src/components/callout/index.ts +2 -0
- package/src/components/index.ts +7 -0
- package/src/components/layout/Aside.astro +29 -0
- package/src/components/layout/Header.astro +18 -0
- package/src/components/layout/Main.astro +9 -0
- package/src/components/layout/Root.astro +15 -0
- package/src/components/layout/Sidebar.astro +9 -0
- package/src/components/layout/Title.astro +10 -0
- package/src/components/layout/index.ts +6 -0
- package/src/components/pager/Label.astro +9 -0
- package/src/components/pager/Link.astro +12 -0
- package/src/components/pager/Root.astro +12 -0
- package/src/components/pager/Title.astro +9 -0
- package/src/components/pager/index.ts +4 -0
- package/src/components/search/Count.astro +10 -0
- package/src/components/search/Dialog.astro +14 -0
- package/src/components/search/Empty.astro +10 -0
- package/src/components/search/Error.astro +10 -0
- package/src/components/search/Found.astro +10 -0
- package/src/components/search/Idle.astro +10 -0
- package/src/components/search/Input.astro +26 -0
- package/src/components/search/Loading.astro +10 -0
- package/src/components/search/Query.astro +10 -0
- package/src/components/search/Results.astro +20 -0
- package/src/components/search/Root.astro +15 -0
- package/src/components/search/Shortcut.astro +14 -0
- package/src/components/search/Trigger.astro +28 -0
- package/src/components/search/index.ts +13 -0
- package/src/components/sidebar/Group.astro +45 -0
- package/src/components/sidebar/Item.astro +9 -0
- package/src/components/sidebar/Label.astro +9 -0
- package/src/components/sidebar/Link.astro +23 -0
- package/src/components/sidebar/List.astro +11 -0
- package/src/components/sidebar/Root.astro +24 -0
- package/src/components/sidebar/Trigger.astro +29 -0
- package/src/components/sidebar/index.ts +7 -0
- package/src/components/toc/Item.astro +9 -0
- package/src/components/toc/Link.astro +12 -0
- package/src/components/toc/List.astro +11 -0
- package/src/components/toc/Root.astro +20 -0
- package/src/components/toc/index.ts +4 -0
- package/src/styles/base.css +41 -0
- package/src/styles/callout.css +58 -0
- package/src/styles/layout.css +406 -0
- package/src/styles/pager.css +51 -0
- package/src/styles/prose.css +218 -0
- package/src/styles/search.css +202 -0
- package/src/styles/sidebar.css +162 -0
- package/src/styles/toc.css +79 -0
- package/src/styles.css +9 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { t as contentOptions } from "./routing-TMqRCY2Q.mjs";
|
|
2
|
+
import { a as renderConfigTypes, i as registry, n as remarkContentLinks, r as satteriContentLinks } from "./links-UTLyoo_o.mjs";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseFilePath } from "@minidocs/core";
|
|
7
|
+
import remarkDirective from "remark-directive";
|
|
8
|
+
//#region src/callouts.ts
|
|
9
|
+
/** Callout types, shared by `:::type` directives, `> [!TYPE]` alerts and the `Callout` component. */
|
|
10
|
+
const CALLOUT_TYPES = [
|
|
11
|
+
"note",
|
|
12
|
+
"tip",
|
|
13
|
+
"important",
|
|
14
|
+
"warning",
|
|
15
|
+
"caution"
|
|
16
|
+
];
|
|
17
|
+
const isType = (name) => CALLOUT_TYPES.includes(name);
|
|
18
|
+
/** Same markup as the Callout component renders, as mdast data for the HTML conversion. */
|
|
19
|
+
const calloutData = (type) => ({
|
|
20
|
+
hName: "div",
|
|
21
|
+
hProperties: {
|
|
22
|
+
className: ["md-callout"],
|
|
23
|
+
dataType: type,
|
|
24
|
+
role: "note"
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const titleData = {
|
|
28
|
+
hName: "p",
|
|
29
|
+
hProperties: { className: ["md-callout-title"] }
|
|
30
|
+
};
|
|
31
|
+
const ALERT = /^\[!(\w+)\][ \t]*(?:\n|$)/;
|
|
32
|
+
function unknownDirective(name) {
|
|
33
|
+
return /* @__PURE__ */ new Error(`[minidocs] Unknown callout type ":::${name}". Use one of ${CALLOUT_TYPES.join(", ")}, or handle it with your own plugin.`);
|
|
34
|
+
}
|
|
35
|
+
/** Reads the type of a `> [!TYPE]` alert and what is left of its first line, or `undefined` for plain blockquotes. */
|
|
36
|
+
function parseAlert(blockquote) {
|
|
37
|
+
const paragraph = blockquote.children?.[0];
|
|
38
|
+
const text = paragraph?.type === "paragraph" ? paragraph.children?.[0] : void 0;
|
|
39
|
+
const match = text?.type === "text" ? ALERT.exec(text.value ?? "") : null;
|
|
40
|
+
const type = match?.[1]?.toLowerCase();
|
|
41
|
+
if (!match || !type || !isType(type)) return void 0;
|
|
42
|
+
const rest = text.value.slice(match[0].length);
|
|
43
|
+
return {
|
|
44
|
+
type,
|
|
45
|
+
paragraph,
|
|
46
|
+
text,
|
|
47
|
+
rest,
|
|
48
|
+
empty: !rest && paragraph.children.length === 1
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Turning on directives also parses `:name` in text and `::name` lines, which would silently drop `10:30` or `a:hover`.
|
|
53
|
+
* Only `:::` blocks are callouts, so the others go back to their source text.
|
|
54
|
+
*/
|
|
55
|
+
function restoreSatteri(block) {
|
|
56
|
+
return (node, context) => {
|
|
57
|
+
const text = {
|
|
58
|
+
type: "text",
|
|
59
|
+
value: context.source.slice(node.position.start.offset, node.position.end.offset)
|
|
60
|
+
};
|
|
61
|
+
context.replaceNode(node, block ? {
|
|
62
|
+
type: "paragraph",
|
|
63
|
+
children: [text]
|
|
64
|
+
} : text);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Plugin for Sätteri, Astro's default Markdown processor. */
|
|
68
|
+
function satteriCallouts(options) {
|
|
69
|
+
return {
|
|
70
|
+
name: "minidocs-callouts",
|
|
71
|
+
options: { position: true },
|
|
72
|
+
...options.directives && {
|
|
73
|
+
containerDirective(node, context) {
|
|
74
|
+
if (node.data?.hName) return;
|
|
75
|
+
const name = node.name ?? "";
|
|
76
|
+
if (!isType(name)) throw unknownDirective(name);
|
|
77
|
+
const label = node.children?.[0];
|
|
78
|
+
if (label?.type === "paragraph" && label.data?.directiveLabel) context.setProperty(label, "data", titleData);
|
|
79
|
+
context.setProperty(node, "data", calloutData(name));
|
|
80
|
+
},
|
|
81
|
+
leafDirective: restoreSatteri(true),
|
|
82
|
+
textDirective: restoreSatteri(false)
|
|
83
|
+
},
|
|
84
|
+
...options.alerts && { blockquote(node, context) {
|
|
85
|
+
const alert = parseAlert(node);
|
|
86
|
+
if (!alert) return;
|
|
87
|
+
if (alert.empty) context.removeNode(alert.paragraph);
|
|
88
|
+
else context.setProperty(alert.text, "value", alert.rest);
|
|
89
|
+
context.setProperty(node, "data", calloutData(alert.type));
|
|
90
|
+
} }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Remark plugin for the unified processor. Directive parsing comes from `remark-directive`, added before it. */
|
|
94
|
+
function remarkCallouts(options) {
|
|
95
|
+
return () => (tree, file) => {
|
|
96
|
+
const source = String(file.value ?? "");
|
|
97
|
+
const visit = (node) => {
|
|
98
|
+
const children = node.children ?? [];
|
|
99
|
+
for (let index = 0; index < children.length; index++) {
|
|
100
|
+
const child = children[index];
|
|
101
|
+
if (options.directives && (child.type === "textDirective" || child.type === "leafDirective")) {
|
|
102
|
+
const text = {
|
|
103
|
+
type: "text",
|
|
104
|
+
value: source.slice(child.position?.start.offset, child.position?.end.offset)
|
|
105
|
+
};
|
|
106
|
+
children[index] = child.type === "leafDirective" ? {
|
|
107
|
+
type: "paragraph",
|
|
108
|
+
children: [text]
|
|
109
|
+
} : text;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (options.directives && child.type === "containerDirective" && !child.data?.hName) {
|
|
113
|
+
const name = child.name ?? "";
|
|
114
|
+
if (!isType(name)) throw unknownDirective(name);
|
|
115
|
+
const label = child.children?.[0];
|
|
116
|
+
if (label?.type === "paragraph" && label.data?.directiveLabel) label.data = {
|
|
117
|
+
...label.data,
|
|
118
|
+
...titleData
|
|
119
|
+
};
|
|
120
|
+
child.data = {
|
|
121
|
+
...child.data,
|
|
122
|
+
...calloutData(name)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (options.alerts && child.type === "blockquote") {
|
|
126
|
+
const alert = parseAlert(child);
|
|
127
|
+
if (alert) {
|
|
128
|
+
if (alert.empty) child.children.shift();
|
|
129
|
+
else alert.text.value = alert.rest;
|
|
130
|
+
child.data = {
|
|
131
|
+
...child.data,
|
|
132
|
+
...calloutData(alert.type)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
visit(child);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
visit(tree);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/markdown.ts
|
|
144
|
+
/**
|
|
145
|
+
* Adds minidocs' Markdown features to the configured processor, which MDX uses too unless given its own.
|
|
146
|
+
* Processors are meant to be extended in place. Returns `false` for processors minidocs does not know.
|
|
147
|
+
*/
|
|
148
|
+
function extendProcessor(processor, callouts) {
|
|
149
|
+
const options = processor.options;
|
|
150
|
+
if (processor.name === "satteri") {
|
|
151
|
+
if (callouts.directives) options.features = {
|
|
152
|
+
...options.features,
|
|
153
|
+
directive: true
|
|
154
|
+
};
|
|
155
|
+
options.mdastPlugins = [
|
|
156
|
+
...options.mdastPlugins ?? [],
|
|
157
|
+
satteriContentLinks,
|
|
158
|
+
satteriCallouts(callouts)
|
|
159
|
+
];
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (processor.name === "unified") {
|
|
163
|
+
options.remarkPlugins = [
|
|
164
|
+
...options.remarkPlugins ?? [],
|
|
165
|
+
...callouts.directives ? [remarkDirective] : [],
|
|
166
|
+
remarkContentLinks,
|
|
167
|
+
remarkCallouts(callouts)
|
|
168
|
+
];
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/settings.ts
|
|
175
|
+
const trim = (path) => path.replace(/^\/+|\/+$/g, "");
|
|
176
|
+
function resolveSettings(config, options) {
|
|
177
|
+
return {
|
|
178
|
+
prefix: trim(options.prefix ?? ""),
|
|
179
|
+
base: trim(config.base),
|
|
180
|
+
trailingSlash: config.trailingSlash === "always" || config.trailingSlash === "ignore" && config.build.format === "directory",
|
|
181
|
+
i18n: resolveI18n(config.i18n, options),
|
|
182
|
+
site: config.site ? config.site.replace(/\/+$/, "") : null
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function resolveI18n(i18n, options) {
|
|
186
|
+
if (!i18n) {
|
|
187
|
+
if (options.fallback) throw new Error("[minidocs] `fallback` requires `i18n` in your Astro config.");
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (i18n.fallback && Object.keys(i18n.fallback).length > 0) throw new Error("[minidocs] Astro's `i18n.fallback` works per route file and produces wrong pages for content-driven docs routes in static builds. Remove it and declare fallback with `minidocs({ fallback })` instead.");
|
|
191
|
+
if (i18n.routing === "manual") throw new Error("[minidocs] `i18n.routing: \"manual\"` is not supported yet.");
|
|
192
|
+
const locales = i18n.locales.map((locale) => {
|
|
193
|
+
if (typeof locale !== "string") throw new Error("[minidocs] Locales with custom paths are not supported yet. Use plain locale strings.");
|
|
194
|
+
return locale;
|
|
195
|
+
});
|
|
196
|
+
const fallback = options.fallback ?? {};
|
|
197
|
+
for (const [locale, targets] of Object.entries(fallback)) for (const target of [locale, ...[targets].flat()]) if (!locales.includes(target)) throw new Error(`[minidocs] Fallback refers to "${target}", which is not one of i18n.locales (${locales.join(", ")}).`);
|
|
198
|
+
return {
|
|
199
|
+
locales,
|
|
200
|
+
defaultLocale: i18n.defaultLocale,
|
|
201
|
+
prefixDefaultLocale: i18n.routing?.prefixDefaultLocale ?? false,
|
|
202
|
+
fallback
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/index.ts
|
|
207
|
+
const CONFIG_ID = "virtual:minidocs/config";
|
|
208
|
+
const RENDERERS_ID = "virtual:minidocs/renderers";
|
|
209
|
+
function minidocs(options = {}) {
|
|
210
|
+
let hasMdx = false;
|
|
211
|
+
return {
|
|
212
|
+
name: "minidocs",
|
|
213
|
+
hooks: {
|
|
214
|
+
"astro:config:setup"({ config, updateConfig, logger }) {
|
|
215
|
+
const callouts = {
|
|
216
|
+
directives: options.markdown?.directives ?? true,
|
|
217
|
+
alerts: options.markdown?.alerts ?? true
|
|
218
|
+
};
|
|
219
|
+
if (!extendProcessor(config.markdown.processor, callouts)) logger.warn(`Content links and callouts are not supported with the "${config.markdown.processor.name}" Markdown processor.`);
|
|
220
|
+
updateConfig({ vite: {
|
|
221
|
+
plugins: [{
|
|
222
|
+
name: "minidocs:config",
|
|
223
|
+
resolveId: (id) => id === CONFIG_ID ? `\0${CONFIG_ID}` : void 0,
|
|
224
|
+
load: (id) => id === `\0${CONFIG_ID}` ? `export default ${JSON.stringify({
|
|
225
|
+
...registry.settings,
|
|
226
|
+
bases: Object.fromEntries(Object.entries(registry.collections).map(([name, { base }]) => [name, base])),
|
|
227
|
+
pageIds: Object.fromEntries(Object.entries(registry.collections).map(([name, collection]) => [name, pageIds(collection)]))
|
|
228
|
+
})};` : void 0
|
|
229
|
+
}, {
|
|
230
|
+
name: "minidocs:renderers",
|
|
231
|
+
resolveId: (id) => id === RENDERERS_ID ? `\0${RENDERERS_ID}` : void 0,
|
|
232
|
+
load: (id) => id === `\0${RENDERERS_ID}` ? hasMdx ? `import { getContainerRenderer } from '@astrojs/mdx';\nexport default [getContainerRenderer()];` : "export default [];" : void 0
|
|
233
|
+
}],
|
|
234
|
+
ssr: { noExternal: ["@minidocs/astro", "@minidocs/core"] }
|
|
235
|
+
} });
|
|
236
|
+
},
|
|
237
|
+
"astro:config:done"({ config, injectTypes }) {
|
|
238
|
+
registry.settings = resolveSettings(config, options);
|
|
239
|
+
registry.root = fileURLToPath(config.root);
|
|
240
|
+
hasMdx = config.integrations.some(({ name }) => name === "@astrojs/mdx");
|
|
241
|
+
const injected = injectTypes({
|
|
242
|
+
filename: "types.d.ts",
|
|
243
|
+
content: renderConfigTypes(registry.settings)
|
|
244
|
+
});
|
|
245
|
+
registry.typesFile = new URL("./collections.d.ts", injected);
|
|
246
|
+
const path = fileURLToPath(registry.typesFile);
|
|
247
|
+
if (!existsSync(path)) {
|
|
248
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
249
|
+
writeFileSync(path, "export {};\n");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Ids of every page in a collection, in any locale. */
|
|
256
|
+
function pageIds({ base, filePaths }) {
|
|
257
|
+
if (!registry.settings) return [];
|
|
258
|
+
const { locales, localeDirs } = contentOptions(registry.settings, base);
|
|
259
|
+
return [...new Set(filePaths.map((filePath) => parseFilePath(filePath, base, locales, localeDirs).id))];
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
262
|
+
export { minidocs as default };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { n as href, t as contentOptions } from "./routing-TMqRCY2Q.mjs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, posix, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { listPages, parseFilePath } from "@minidocs/core";
|
|
6
|
+
//#region src/codegen.ts
|
|
7
|
+
const union = (values) => {
|
|
8
|
+
const list = [...new Set(values)].sort().map((value) => JSON.stringify(value));
|
|
9
|
+
return list.length > 0 ? list.join(" | ") : "never";
|
|
10
|
+
};
|
|
11
|
+
/** Renders the site-wide types, known as soon as the config is resolved. */
|
|
12
|
+
function renderConfigTypes(settings) {
|
|
13
|
+
return [
|
|
14
|
+
"// Generated by minidocs. Do not edit.",
|
|
15
|
+
"/// <reference path=\"./collections.d.ts\" />",
|
|
16
|
+
"declare module '@minidocs/astro/runtime' {",
|
|
17
|
+
`\tinterface Config { ${settings.i18n ? `i18n: true; locale: ${union(settings.i18n.locales)};` : "i18n: false;"} }`,
|
|
18
|
+
"}",
|
|
19
|
+
"export {};",
|
|
20
|
+
""
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
/** Renders the declaration file that types page ids and directories per collection, and per locale for multilingual sites. */
|
|
24
|
+
function renderTypes(settings, collections) {
|
|
25
|
+
const lines = [
|
|
26
|
+
"// Generated by minidocs. Do not edit.",
|
|
27
|
+
"declare module '@minidocs/astro/runtime' {",
|
|
28
|
+
" interface Collections {"
|
|
29
|
+
];
|
|
30
|
+
for (const [name, { base, filePaths }] of Object.entries(collections).sort(([a], [b]) => a < b ? -1 : 1)) {
|
|
31
|
+
const options = contentOptions(settings, base);
|
|
32
|
+
const pages = listPages(filePaths.map((filePath) => ({ filePath })), options);
|
|
33
|
+
const idsAndDirs = (locale) => {
|
|
34
|
+
const inLocale = pages.filter((page) => page.locale === locale);
|
|
35
|
+
const dirs = /* @__PURE__ */ new Set([""]);
|
|
36
|
+
for (const page of inLocale) {
|
|
37
|
+
const { dir } = parseFilePath(page.entry.filePath, base, options.locales, options.localeDirs);
|
|
38
|
+
const segments = dir.split("/").filter(Boolean);
|
|
39
|
+
for (let i = 1; i <= segments.length; i++) dirs.add(segments.slice(0, i).join("/"));
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ids: union(inLocale.map((page) => page.id)),
|
|
43
|
+
dirs: union(dirs)
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
lines.push(`\t\t${JSON.stringify(name)}: {`);
|
|
47
|
+
if (settings.i18n) {
|
|
48
|
+
const perLocale = settings.i18n.locales.map((locale) => [JSON.stringify(locale), idsAndDirs(locale)]);
|
|
49
|
+
lines.push(" pages: {", ...perLocale.map(([locale, { ids }]) => `\t\t\t\t${locale}: ${ids};`), " };");
|
|
50
|
+
lines.push(" dirs: {", ...perLocale.map(([locale, { dirs }]) => `\t\t\t\t${locale}: ${dirs};`), " };");
|
|
51
|
+
} else {
|
|
52
|
+
const { ids, dirs } = idsAndDirs(options.locales[0]);
|
|
53
|
+
lines.push(`\t\t\tpages: ${ids};`, `\t\t\tdirs: ${dirs};`);
|
|
54
|
+
}
|
|
55
|
+
lines.push(" };");
|
|
56
|
+
}
|
|
57
|
+
lines.push(" }", "}", "export {};", "");
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/registry.ts
|
|
62
|
+
const key = Symbol.for("minidocs.registry");
|
|
63
|
+
const scope = globalThis;
|
|
64
|
+
scope[key] ??= { collections: {} };
|
|
65
|
+
const registry = scope[key];
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/links.ts
|
|
68
|
+
/** Link to another page from a page in the same locale, relative so it stays in whatever locale the page is shown in. */
|
|
69
|
+
function relativeHref(settings, fromId, toId, locale) {
|
|
70
|
+
const from = href(settings, fromId, locale);
|
|
71
|
+
const to = href(settings, toId, locale);
|
|
72
|
+
const fromDir = from.slice(0, from.lastIndexOf("/") + 1);
|
|
73
|
+
if (to.endsWith("/")) return `${posix.relative(fromDir, to) || "."}/`;
|
|
74
|
+
return posix.join(posix.relative(fromDir, posix.dirname(to)), posix.basename(to));
|
|
75
|
+
}
|
|
76
|
+
const EXCLUDED = "points to a page that the collection's filter excludes.";
|
|
77
|
+
/**
|
|
78
|
+
* Rewrites a relative link to a content file into the URL of its page.
|
|
79
|
+
* Links within one locale become relative URLs, so pages shown through fallback keep linking within the shown locale.
|
|
80
|
+
* Returns `undefined` for links that are not to `.md` or `.mdx` files. Throws when the target does not exist.
|
|
81
|
+
*/
|
|
82
|
+
function resolveContentLink(settings, source, url, root) {
|
|
83
|
+
const match = /^([^#?]+\.mdx?)(#.*)?$/i.exec(url);
|
|
84
|
+
if (!match || /^([a-z][a-z\d+.-]*:|\/)/i.test(url)) return void 0;
|
|
85
|
+
const [, path, hash = ""] = match;
|
|
86
|
+
const absolute = resolve(root, dirname(source.file), decodeURI(path));
|
|
87
|
+
const target = relative(root, absolute).split(sep).join("/");
|
|
88
|
+
if (!target.startsWith(`${source.base}/`)) throw new Error(`"${url}" points outside the content collection, so it has no page.`);
|
|
89
|
+
if (!existsSync(absolute)) throw new Error(`"${url}" points to a file that does not exist.`);
|
|
90
|
+
if (source.isExcluded?.(target)) throw new Error(`"${url}" ${EXCLUDED}`);
|
|
91
|
+
const { locales, localeDirs } = contentOptions(settings, source.base);
|
|
92
|
+
const from = parseFilePath(source.file, source.base, locales, localeDirs);
|
|
93
|
+
const to = parseFilePath(target, source.base, locales, localeDirs);
|
|
94
|
+
return (from.locale === to.locale ? relativeHref(settings, from.id, to.id, settings.i18n ? from.locale : void 0) : href(settings, to.id, to.locale)) + hash;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolves a link found in a Markdown file, or returns `undefined` to leave it alone.
|
|
98
|
+
* Only files inside a collection loaded with minidocs' glob are rewritten.
|
|
99
|
+
*/
|
|
100
|
+
function rewrite(filePath, url) {
|
|
101
|
+
const { settings, root } = registry;
|
|
102
|
+
if (!settings || !root) return void 0;
|
|
103
|
+
const file = relative(root, filePath).split(sep).join("/");
|
|
104
|
+
const collection = Object.values(registry.collections).find(({ base }) => file.startsWith(`${base}/`));
|
|
105
|
+
if (!collection) return void 0;
|
|
106
|
+
const isExcluded = (target) => {
|
|
107
|
+
if (collection.loading) {
|
|
108
|
+
collection.pendingLinks.push({
|
|
109
|
+
file,
|
|
110
|
+
url,
|
|
111
|
+
target
|
|
112
|
+
});
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return collection.excluded.has(target);
|
|
116
|
+
};
|
|
117
|
+
return resolveContentLink(settings, {
|
|
118
|
+
file,
|
|
119
|
+
base: collection.base,
|
|
120
|
+
isExcluded
|
|
121
|
+
}, url, root);
|
|
122
|
+
}
|
|
123
|
+
/** Checks links that Markdown rendered during a load, now that every entry has been filtered. */
|
|
124
|
+
function checkPendingLinks(collection) {
|
|
125
|
+
const pending = collection.pendingLinks;
|
|
126
|
+
collection.pendingLinks = [];
|
|
127
|
+
const broken = pending.filter(({ file, target }) => !collection.excluded.has(file) && collection.excluded.has(target));
|
|
128
|
+
if (broken.length === 0) return;
|
|
129
|
+
const root = registry.root ?? "";
|
|
130
|
+
throw new Error(broken.map(({ file, url }) => failure(resolve(root, file), url, /* @__PURE__ */ new Error(`"${url}" ${EXCLUDED}`)).message).join("\n"));
|
|
131
|
+
}
|
|
132
|
+
/** Points at the line of the source file with the link. Processors may count lines from after the frontmatter. */
|
|
133
|
+
function failure(filePath, url, error) {
|
|
134
|
+
let line;
|
|
135
|
+
try {
|
|
136
|
+
const index = readFileSync(filePath, "utf8").split("\n").findIndex((text) => text.includes(url));
|
|
137
|
+
if (index !== -1) line = index + 1;
|
|
138
|
+
} catch {}
|
|
139
|
+
const location = `${relative(registry.root ?? "", filePath)}${line ? `:${line}` : ""}`;
|
|
140
|
+
return /* @__PURE__ */ new Error(`[minidocs] ${location}: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
/** Plugin for Sätteri, Astro's default Markdown processor. */
|
|
143
|
+
const satteriContentLinks = (() => {
|
|
144
|
+
const visit = (node, context) => {
|
|
145
|
+
if (!context.fileURL) return;
|
|
146
|
+
const filePath = fileURLToPath(context.fileURL);
|
|
147
|
+
try {
|
|
148
|
+
const url = rewrite(filePath, node.url);
|
|
149
|
+
if (url !== void 0) context.setProperty(node, "url", url);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw failure(filePath, node.url, error);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
name: "minidocs-content-links",
|
|
156
|
+
link: visit,
|
|
157
|
+
definition: visit
|
|
158
|
+
};
|
|
159
|
+
})();
|
|
160
|
+
/** Remark plugin for the unified processor from `@astrojs/markdown-remark`. */
|
|
161
|
+
function remarkContentLinks() {
|
|
162
|
+
return (tree, file) => {
|
|
163
|
+
const filePath = file.path;
|
|
164
|
+
if (!filePath) return;
|
|
165
|
+
const visit = (node) => {
|
|
166
|
+
if ((node.type === "link" || node.type === "definition") && node.url) try {
|
|
167
|
+
node.url = rewrite(filePath, node.url) ?? node.url;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw failure(filePath, node.url, error);
|
|
170
|
+
}
|
|
171
|
+
for (const child of node.children ?? []) visit(child);
|
|
172
|
+
};
|
|
173
|
+
visit(tree);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
export { renderConfigTypes as a, registry as i, remarkContentLinks as n, renderTypes as o, satteriContentLinks as r, checkPendingLinks as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Loader, glob as glob$1 } from "astro/loaders";
|
|
2
|
+
//#region src/loaders.d.ts
|
|
3
|
+
type GlobOptions = Parameters<typeof glob$1>[0];
|
|
4
|
+
/** An entry as the filter sees it, after the collection schema has parsed its data. */
|
|
5
|
+
export interface FilterEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
data: Record<string, unknown>;
|
|
8
|
+
filePath?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Astro's `glob()` loader, plus recording of the collection root so minidocs can
|
|
12
|
+
* derive locales and page ids, and generate types for them.
|
|
13
|
+
* Entries rejected by `filter` never reach the collection, so types, links and pages all agree on what exists.
|
|
14
|
+
*/
|
|
15
|
+
export declare function glob({ filter, ...options }: GlobOptions & {
|
|
16
|
+
base: string | URL;
|
|
17
|
+
filter?: (entry: FilterEntry) => boolean;
|
|
18
|
+
}): Loader;
|
|
19
|
+
//#endregion
|
|
20
|
+
export {};
|
package/dist/loaders.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { i as registry, o as renderTypes, t as checkPendingLinks } from "./links-UTLyoo_o.mjs";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, relative } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { glob as glob$1 } from "astro/loaders";
|
|
6
|
+
//#region src/loaders.ts
|
|
7
|
+
/**
|
|
8
|
+
* Astro's `glob()` loader, plus recording of the collection root so minidocs can
|
|
9
|
+
* derive locales and page ids, and generate types for them.
|
|
10
|
+
* Entries rejected by `filter` never reach the collection, so types, links and pages all agree on what exists.
|
|
11
|
+
*/
|
|
12
|
+
function glob({ filter, ...options }) {
|
|
13
|
+
const inner = glob$1(options);
|
|
14
|
+
return {
|
|
15
|
+
...inner,
|
|
16
|
+
name: "minidocs-glob",
|
|
17
|
+
async load(context) {
|
|
18
|
+
const baseUrl = options.base instanceof URL ? options.base : new URL(options.base, context.config.root);
|
|
19
|
+
const base = relative(fileURLToPath(context.config.root), fileURLToPath(baseUrl)).replaceAll("\\", "/");
|
|
20
|
+
let scheduled;
|
|
21
|
+
const update = () => {
|
|
22
|
+
clearTimeout(scheduled);
|
|
23
|
+
scheduled = setTimeout(() => writeTypes(context), 50);
|
|
24
|
+
};
|
|
25
|
+
registry.collections[context.collection] ??= {
|
|
26
|
+
base,
|
|
27
|
+
filePaths: [],
|
|
28
|
+
excluded: /* @__PURE__ */ new Set(),
|
|
29
|
+
loading: false,
|
|
30
|
+
pendingLinks: []
|
|
31
|
+
};
|
|
32
|
+
const state = registry.collections[context.collection];
|
|
33
|
+
state.base = base;
|
|
34
|
+
const store = new Proxy(context.store, { get(target, property, receiver) {
|
|
35
|
+
const value = Reflect.get(target, property, receiver);
|
|
36
|
+
if (property !== "set" && property !== "delete" && property !== "clear") return value;
|
|
37
|
+
return (...args) => {
|
|
38
|
+
update();
|
|
39
|
+
if (property === "set" && filter) {
|
|
40
|
+
const entry = args[0];
|
|
41
|
+
if (!filter(entry)) {
|
|
42
|
+
if (entry.filePath) state.excluded.add(entry.filePath);
|
|
43
|
+
if (target.has(entry.id)) target.delete(entry.id);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (entry.filePath) state.excluded.delete(entry.filePath);
|
|
47
|
+
}
|
|
48
|
+
return value.apply(target, args);
|
|
49
|
+
};
|
|
50
|
+
} });
|
|
51
|
+
state.loading = true;
|
|
52
|
+
try {
|
|
53
|
+
await inner.load({
|
|
54
|
+
...context,
|
|
55
|
+
store
|
|
56
|
+
});
|
|
57
|
+
} finally {
|
|
58
|
+
state.loading = false;
|
|
59
|
+
}
|
|
60
|
+
clearTimeout(scheduled);
|
|
61
|
+
writeTypes(context);
|
|
62
|
+
checkPendingLinks(state);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function writeTypes(context) {
|
|
67
|
+
const { settings, typesFile } = registry;
|
|
68
|
+
if (!settings || !typesFile) throw new Error("[minidocs] Add the minidocs() integration to your Astro config to use glob from '@minidocs/astro/loaders'.");
|
|
69
|
+
const filePaths = context.store.values().map((entry) => entry.filePath).filter((filePath) => Boolean(filePath));
|
|
70
|
+
registry.collections[context.collection].filePaths = filePaths;
|
|
71
|
+
const path = fileURLToPath(typesFile);
|
|
72
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
73
|
+
writeFileSync(path, renderTypes(settings, registry.collections));
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { glob };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/routing.ts
|
|
2
|
+
/** The locale every page of a single-language site belongs to inside core. Never exposed to users. */
|
|
3
|
+
const SINGLE_LOCALE = "default";
|
|
4
|
+
/** Options for core: locale directories for multilingual sites, one implicit locale otherwise. */
|
|
5
|
+
function contentOptions(settings, base) {
|
|
6
|
+
return settings.i18n ? {
|
|
7
|
+
base,
|
|
8
|
+
locales: settings.i18n.locales,
|
|
9
|
+
fallback: settings.i18n.fallback
|
|
10
|
+
} : {
|
|
11
|
+
base,
|
|
12
|
+
locales: [SINGLE_LOCALE],
|
|
13
|
+
localeDirs: false
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** The URL segment of a page id: the root index page has none. */
|
|
17
|
+
function slugOf(id) {
|
|
18
|
+
return id === "index" ? "" : id;
|
|
19
|
+
}
|
|
20
|
+
const normalizeLocale = (locale) => locale.replaceAll("_", "-").toLowerCase();
|
|
21
|
+
/**
|
|
22
|
+
* Route parameter for a page, e.g. `en/docs/getting-started`, without `base` or slashes around it.
|
|
23
|
+
* `undefined` stands for the site root, as Astro expects for rest parameters.
|
|
24
|
+
*/
|
|
25
|
+
function routePath(settings, id, locale) {
|
|
26
|
+
const { i18n } = settings;
|
|
27
|
+
return [
|
|
28
|
+
!i18n || locale === void 0 || locale === i18n.defaultLocale && !i18n.prefixDefaultLocale ? "" : normalizeLocale(locale),
|
|
29
|
+
settings.prefix,
|
|
30
|
+
slugOf(id)
|
|
31
|
+
].filter(Boolean).join("/") || void 0;
|
|
32
|
+
}
|
|
33
|
+
/** Link to a page, including `base` and the trailing slash Astro would use. */
|
|
34
|
+
function href(settings, id, locale) {
|
|
35
|
+
const path = [settings.base, routePath(settings, id, locale)].filter(Boolean).join("/");
|
|
36
|
+
if (!path) return "/";
|
|
37
|
+
return `/${path}${settings.trailingSlash ? "/" : ""}`;
|
|
38
|
+
}
|
|
39
|
+
/** Route parameter for a page in `src/pages/[...path].md.ts`. The site root has no segment of its own, so it becomes `index`. */
|
|
40
|
+
function markdownPath(settings, id, locale) {
|
|
41
|
+
return routePath(settings, id, locale) ?? "index";
|
|
42
|
+
}
|
|
43
|
+
/** Link to the Markdown version of a page, including `base`. */
|
|
44
|
+
function markdownHref(settings, id, locale) {
|
|
45
|
+
return `/${[settings.base, markdownPath(settings, id, locale)].filter(Boolean).join("/")}.md`;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { routePath as a, markdownPath as i, href as n, markdownHref as r, contentOptions as t };
|