@minidocs/astro 0.0.0-stage → 0.2.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -2
  3. package/dist/behaviors.d.mts +114 -0
  4. package/dist/behaviors.mjs +635 -0
  5. package/dist/index.d.mts +23 -0
  6. package/dist/index.mjs +275 -0
  7. package/dist/links-Dp7d5R-O.mjs +242 -0
  8. package/dist/loaders.d.mts +25 -0
  9. package/dist/loaders.mjs +88 -0
  10. package/dist/routing-D28b92w8.mjs +60 -0
  11. package/dist/runtime.d.mts +202 -0
  12. package/dist/runtime.mjs +327 -0
  13. package/package.json +80 -3
  14. package/src/components/Prose.astro +9 -0
  15. package/src/components/callout/Root.astro +14 -0
  16. package/src/components/callout/Title.astro +9 -0
  17. package/src/components/callout/index.ts +2 -0
  18. package/src/components/index.ts +7 -0
  19. package/src/components/layout/Aside.astro +29 -0
  20. package/src/components/layout/Header.astro +18 -0
  21. package/src/components/layout/Main.astro +9 -0
  22. package/src/components/layout/Root.astro +15 -0
  23. package/src/components/layout/Sidebar.astro +9 -0
  24. package/src/components/layout/Title.astro +10 -0
  25. package/src/components/layout/index.ts +6 -0
  26. package/src/components/pager/Label.astro +9 -0
  27. package/src/components/pager/Link.astro +12 -0
  28. package/src/components/pager/Root.astro +12 -0
  29. package/src/components/pager/Title.astro +9 -0
  30. package/src/components/pager/index.ts +4 -0
  31. package/src/components/search/Count.astro +10 -0
  32. package/src/components/search/Dialog.astro +14 -0
  33. package/src/components/search/Empty.astro +10 -0
  34. package/src/components/search/Error.astro +10 -0
  35. package/src/components/search/Found.astro +10 -0
  36. package/src/components/search/Idle.astro +10 -0
  37. package/src/components/search/Input.astro +26 -0
  38. package/src/components/search/Loading.astro +10 -0
  39. package/src/components/search/Query.astro +10 -0
  40. package/src/components/search/Results.astro +20 -0
  41. package/src/components/search/Root.astro +15 -0
  42. package/src/components/search/Shortcut.astro +14 -0
  43. package/src/components/search/Trigger.astro +28 -0
  44. package/src/components/search/index.ts +13 -0
  45. package/src/components/sidebar/Group.astro +45 -0
  46. package/src/components/sidebar/Item.astro +9 -0
  47. package/src/components/sidebar/Label.astro +9 -0
  48. package/src/components/sidebar/Link.astro +23 -0
  49. package/src/components/sidebar/List.astro +11 -0
  50. package/src/components/sidebar/Root.astro +24 -0
  51. package/src/components/sidebar/Trigger.astro +29 -0
  52. package/src/components/sidebar/index.ts +7 -0
  53. package/src/components/toc/Item.astro +9 -0
  54. package/src/components/toc/Link.astro +12 -0
  55. package/src/components/toc/List.astro +11 -0
  56. package/src/components/toc/Root.astro +20 -0
  57. package/src/components/toc/index.ts +4 -0
  58. package/src/styles/base.css +42 -0
  59. package/src/styles/callout.css +58 -0
  60. package/src/styles/layout.css +406 -0
  61. package/src/styles/pager.css +56 -0
  62. package/src/styles/prose.css +218 -0
  63. package/src/styles/search.css +207 -0
  64. package/src/styles/sidebar.css +162 -0
  65. package/src/styles/toc.css +79 -0
  66. package/src/styles.css +9 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,275 @@
1
+ import { t as contentOptions } from "./routing-D28b92w8.mjs";
2
+ import { a as registry, c as renderConfigTypes, i as namedCollections, n as remarkContentLinks, o as findUnbuiltPages, r as satteriContentLinks } from "./links-Dp7d5R-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
+ base: trim(config.base),
179
+ trailingSlash: config.trailingSlash === "always" || config.trailingSlash === "ignore" && config.build.format === "directory",
180
+ i18n: resolveI18n(config.i18n, options),
181
+ site: config.site ? config.site.replace(/\/+$/, "") : null
182
+ };
183
+ }
184
+ function resolveI18n(i18n, options) {
185
+ if (!i18n) {
186
+ if (options.fallback) throw new Error("[minidocs] `fallback` requires `i18n` in your Astro config.");
187
+ return null;
188
+ }
189
+ 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.");
190
+ if (i18n.routing === "manual") throw new Error("[minidocs] `i18n.routing: \"manual\"` is not supported yet.");
191
+ const locales = i18n.locales.map((locale) => {
192
+ if (typeof locale !== "string") throw new Error("[minidocs] Locales with custom paths are not supported yet. Use plain locale strings.");
193
+ return locale;
194
+ });
195
+ const fallback = options.fallback ?? {};
196
+ 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(", ")}).`);
197
+ return {
198
+ locales,
199
+ defaultLocale: i18n.defaultLocale,
200
+ prefixDefaultLocale: i18n.routing?.prefixDefaultLocale ?? false,
201
+ fallback
202
+ };
203
+ }
204
+ //#endregion
205
+ //#region src/index.ts
206
+ const CONFIG_ID = "virtual:minidocs/config";
207
+ const RENDERERS_ID = "virtual:minidocs/renderers";
208
+ function minidocs(options = {}) {
209
+ let hasMdx = false;
210
+ let isStatic = 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
+ collections: Object.fromEntries(Object.entries(namedCollections()).map(([name, collection]) => [name, {
227
+ base: collection.base,
228
+ prefix: collection.prefix,
229
+ pageIds: pageIds(collection)
230
+ }]))
231
+ })};` : void 0
232
+ }, {
233
+ name: "minidocs:renderers",
234
+ resolveId: (id) => id === RENDERERS_ID ? `\0${RENDERERS_ID}` : void 0,
235
+ load: (id) => id === `\0${RENDERERS_ID}` ? hasMdx ? `import { getContainerRenderer } from '@astrojs/mdx/container-renderer';\nexport default [getContainerRenderer()];` : "export default [];" : void 0
236
+ }],
237
+ ssr: { noExternal: ["@minidocs/astro", "@minidocs/core"] }
238
+ } });
239
+ },
240
+ "astro:config:done"({ config, injectTypes }) {
241
+ registry.settings = resolveSettings(config, options);
242
+ registry.root = fileURLToPath(config.root);
243
+ hasMdx = config.integrations.some(({ name }) => name === "@astrojs/mdx");
244
+ isStatic = config.output === "static" && !config.adapter;
245
+ const injected = injectTypes({
246
+ filename: "types.d.ts",
247
+ content: renderConfigTypes(registry.settings)
248
+ });
249
+ registry.typesFile = new URL("./collections.d.ts", injected);
250
+ const path = fileURLToPath(registry.typesFile);
251
+ if (!existsSync(path)) {
252
+ mkdirSync(dirname(path), { recursive: true });
253
+ writeFileSync(path, "export {};\n");
254
+ }
255
+ },
256
+ "astro:build:done"({ pages }) {
257
+ if (!isStatic || !registry.settings) return;
258
+ const built = new Set(pages.map(({ pathname }) => pathname.replace(/^\/+|\/+$/g, "")));
259
+ const missing = findUnbuiltPages(registry.settings, namedCollections(), built);
260
+ if (missing.length === 0) return;
261
+ const shown = missing.slice(0, 10).map((page) => ` ${page}`);
262
+ if (missing.length > shown.length) shown.push(` and ${missing.length - shown.length} more`);
263
+ throw new Error(`[minidocs] No route built these pages, so links to them are broken. Check that each collection's route file sits in the folder its prefix names.\n${shown.join("\n")}`);
264
+ }
265
+ }
266
+ };
267
+ }
268
+ /** Ids of every page in a collection, in any locale. */
269
+ function pageIds(collection) {
270
+ if (!registry.settings) return [];
271
+ const { locales, localeDirs } = contentOptions(registry.settings, collection.base);
272
+ return [...new Set(collection.filePaths.map((filePath) => parseFilePath(filePath, collection.base, locales, localeDirs).id))];
273
+ }
274
+ //#endregion
275
+ export { minidocs as default };
@@ -0,0 +1,242 @@
1
+ import { i as markdownHref, n as href, o as routePath, t as contentOptions } from "./routing-D28b92w8.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
+ const byName = (collections) => Object.entries(collections).sort(([a], [b]) => a < b ? -1 : 1);
12
+ /** Every page a collection needs a route for, with the locale its URL is built with. */
13
+ function pagesOf(settings, collection) {
14
+ return listPages(collection.filePaths.map((filePath) => ({ filePath })), contentOptions(settings, collection.base)).map((page) => ({
15
+ ...page,
16
+ urlLocale: settings.i18n ? page.locale : void 0
17
+ }));
18
+ }
19
+ /** Renders the site-wide types, known as soon as the config is resolved. */
20
+ function renderConfigTypes(settings) {
21
+ return [
22
+ "// Generated by minidocs. Do not edit.",
23
+ "/// <reference path=\"./collections.d.ts\" />",
24
+ "declare module '@minidocs/astro/runtime' {",
25
+ `\tinterface Config { ${settings.i18n ? `i18n: true; locale: ${union(settings.i18n.locales)};` : "i18n: false;"} }`,
26
+ "}",
27
+ "export {};",
28
+ ""
29
+ ].join("\n");
30
+ }
31
+ /** Renders the declaration file that types page ids and directories per collection, and per locale for multilingual sites. */
32
+ function renderTypes(settings, collections) {
33
+ const lines = [
34
+ "// Generated by minidocs. Do not edit.",
35
+ "declare module '@minidocs/astro/runtime' {",
36
+ " interface Collections {"
37
+ ];
38
+ for (const [name, collection] of byName(collections)) {
39
+ const options = contentOptions(settings, collection.base);
40
+ const pages = pagesOf(settings, collection);
41
+ const idsAndDirs = (locale) => {
42
+ const inLocale = pages.filter((page) => page.locale === locale);
43
+ const dirs = /* @__PURE__ */ new Set([""]);
44
+ for (const page of inLocale) {
45
+ const { dir } = parseFilePath(page.entry.filePath, collection.base, options.locales, options.localeDirs);
46
+ const segments = dir.split("/").filter(Boolean);
47
+ for (let i = 1; i <= segments.length; i++) dirs.add(segments.slice(0, i).join("/"));
48
+ }
49
+ return {
50
+ ids: union(inLocale.map((page) => page.id)),
51
+ dirs: union(dirs)
52
+ };
53
+ };
54
+ lines.push(`\t\t${JSON.stringify(name)}: {`);
55
+ if (settings.i18n) {
56
+ const perLocale = options.locales.map((locale) => [JSON.stringify(locale), idsAndDirs(locale)]);
57
+ lines.push(" pages: {", ...perLocale.map(([locale, { ids }]) => `\t\t\t\t${locale}: ${ids};`), " };");
58
+ lines.push(" dirs: {", ...perLocale.map(([locale, { dirs }]) => `\t\t\t\t${locale}: ${dirs};`), " };");
59
+ } else {
60
+ const { ids, dirs } = idsAndDirs(options.locales[0]);
61
+ lines.push(`\t\t\tpages: ${ids};`, `\t\t\tdirs: ${dirs};`);
62
+ }
63
+ lines.push(" };");
64
+ }
65
+ lines.push(" }", "}", "export {};", "");
66
+ return lines.join("\n");
67
+ }
68
+ /**
69
+ * Finds pages that would be served at the same URL, across all collections, for HTML pages and Markdown versions alike.
70
+ * Returns one message per pair of files.
71
+ */
72
+ function findUrlConflicts(settings, collections) {
73
+ const owners = /* @__PURE__ */ new Map();
74
+ const conflicts = /* @__PURE__ */ new Map();
75
+ for (const [name, collection] of byName(collections)) for (const page of pagesOf(settings, collection)) {
76
+ const owner = `${page.entry.filePath} (${name})`;
77
+ const urls = [href(settings, collection, page.id, page.urlLocale), markdownHref(settings, collection, page.id, page.urlLocale)];
78
+ for (const url of urls) {
79
+ const other = owners.get(url);
80
+ if (other === void 0) owners.set(url, owner);
81
+ else if (!conflicts.has(`${other}\n${owner}`)) conflicts.set(`${other}\n${owner}`, `"${url}" is the URL of two pages:\n ${other}\n ${owner}`);
82
+ }
83
+ }
84
+ return [...conflicts.values()];
85
+ }
86
+ /**
87
+ * Finds pages that no route generated, given the paths of built pages below `base`.
88
+ * A route file outside the folder that `prefix` names builds its pages elsewhere, and every link to them breaks.
89
+ */
90
+ function findUnbuiltPages(settings, collections, built) {
91
+ const missing = [];
92
+ for (const [name, collection] of byName(collections)) for (const page of pagesOf(settings, collection)) {
93
+ const path = routePath(settings, collection, page.id, page.urlLocale) ?? "";
94
+ if (!built.has(path)) missing.push(`/${path} for ${page.entry.filePath} (${name}, prefix "${collection.prefix}")`);
95
+ }
96
+ return missing;
97
+ }
98
+ //#endregion
99
+ //#region src/registry.ts
100
+ const key = Symbol.for("minidocs.registry");
101
+ const scope = globalThis;
102
+ scope[key] ??= {
103
+ collections: {},
104
+ pendingLinks: []
105
+ };
106
+ const registry = scope[key];
107
+ /** Collections whose loader has run, keyed by collection name. */
108
+ function namedCollections() {
109
+ return Object.fromEntries(Object.values(registry.collections).filter((state) => state.name !== void 0).map((state) => [state.name, state]));
110
+ }
111
+ //#endregion
112
+ //#region src/links.ts
113
+ /** Link to another page from a page in the same locale, relative so it stays in whatever locale the page is shown in. */
114
+ function relativeHref(settings, from, to, locale) {
115
+ const fromHref = href(settings, from.collection, from.id, locale);
116
+ const toHref = href(settings, to.collection, to.id, locale);
117
+ const fromDir = fromHref.slice(0, fromHref.lastIndexOf("/") + 1);
118
+ if (toHref.endsWith("/")) return `${posix.relative(fromDir, toHref) || "."}/`;
119
+ return posix.join(posix.relative(fromDir, posix.dirname(toHref)), posix.basename(toHref));
120
+ }
121
+ /** The collection whose root holds a project-relative file, the innermost one if roots are nested. */
122
+ function collectionOf(collections, file) {
123
+ return collections.filter(({ base }) => file.startsWith(`${base}/`)).reduce((best, collection) => best && best.base.length > collection.base.length ? best : collection, void 0);
124
+ }
125
+ const EXCLUDED = "points to a page that the collection's filter excludes.";
126
+ /**
127
+ * Rewrites a relative link to a content file, in any collection, into the URL of its page.
128
+ * Links within one locale become relative URLs, so pages shown through fallback keep linking within the shown locale.
129
+ * Returns `undefined` for links that are not to `.md` or `.mdx` files. Throws when the target does not exist.
130
+ */
131
+ function resolveContentLink(settings, source, url, root) {
132
+ const match = /^([^#?]+\.mdx?)(#.*)?$/i.exec(url);
133
+ if (!match || /^([a-z][a-z\d+.-]*:|\/)/i.test(url)) return void 0;
134
+ const [, path, hash = ""] = match;
135
+ const absolute = resolve(root, dirname(source.file), decodeURI(path));
136
+ const target = relative(root, absolute).split(sep).join("/");
137
+ const collection = collectionOf(source.collections, target);
138
+ if (!collection) throw new Error(`"${url}" points outside every collection loaded with minidocs' glob, so it has no page.`);
139
+ if (!existsSync(absolute)) throw new Error(`"${url}" points to a file that does not exist.`);
140
+ if (source.isExcluded?.(target, collection)) throw new Error(`"${url}" ${EXCLUDED}`);
141
+ const parse = (file, of) => {
142
+ const { locales, localeDirs } = contentOptions(settings, of.base);
143
+ return parseFilePath(file, of.base, locales, localeDirs);
144
+ };
145
+ const from = parse(source.file, source.collection);
146
+ const to = parse(target, collection);
147
+ return (from.locale === to.locale ? relativeHref(settings, {
148
+ collection: source.collection,
149
+ id: from.id
150
+ }, {
151
+ collection,
152
+ id: to.id
153
+ }, settings.i18n ? from.locale : void 0) : href(settings, collection, to.id, to.locale)) + hash;
154
+ }
155
+ /**
156
+ * Resolves a link found in a Markdown file, or returns `undefined` to leave it alone.
157
+ * Only files inside a collection loaded with minidocs' glob are rewritten.
158
+ */
159
+ function rewrite(filePath, url) {
160
+ const { settings, root } = registry;
161
+ if (!settings || !root) return void 0;
162
+ const file = relative(root, filePath).split(sep).join("/");
163
+ const collections = Object.values(registry.collections);
164
+ const collection = collectionOf(collections, file);
165
+ if (!collection) return void 0;
166
+ const ready = (state) => state.loaded && !state.loading;
167
+ const isExcluded = (target, to) => {
168
+ if (!ready(collection) || !ready(to)) {
169
+ registry.pendingLinks.push({
170
+ file,
171
+ url,
172
+ target,
173
+ source: collection,
174
+ to
175
+ });
176
+ return false;
177
+ }
178
+ return to.excluded.has(target);
179
+ };
180
+ return resolveContentLink(settings, {
181
+ file,
182
+ collection,
183
+ collections,
184
+ isExcluded
185
+ }, url, root);
186
+ }
187
+ /** Checks links rendered while a collection involved was loading, once both collections have loaded. */
188
+ function checkPendingLinks() {
189
+ const ready = (state) => state.loaded && !state.loading;
190
+ const due = registry.pendingLinks.filter(({ source, to }) => ready(source) && ready(to));
191
+ registry.pendingLinks = registry.pendingLinks.filter((link) => !due.includes(link));
192
+ const broken = due.filter(({ file, target, source, to }) => !source.excluded.has(file) && to.excluded.has(target));
193
+ if (broken.length === 0) return;
194
+ const root = registry.root ?? "";
195
+ throw new Error(broken.map(({ file, url }) => failure(resolve(root, file), url, /* @__PURE__ */ new Error(`"${url}" ${EXCLUDED}`)).message).join("\n"));
196
+ }
197
+ /** Points at the line of the source file with the link. Processors may count lines from after the frontmatter. */
198
+ function failure(filePath, url, error) {
199
+ let line;
200
+ try {
201
+ const index = readFileSync(filePath, "utf8").split("\n").findIndex((text) => text.includes(url));
202
+ if (index !== -1) line = index + 1;
203
+ } catch {}
204
+ const location = `${relative(registry.root ?? "", filePath)}${line ? `:${line}` : ""}`;
205
+ return /* @__PURE__ */ new Error(`[minidocs] ${location}: ${error.message}`);
206
+ }
207
+ /** Plugin for Sätteri, Astro's default Markdown processor. */
208
+ const satteriContentLinks = (() => {
209
+ const visit = (node, context) => {
210
+ if (!context.fileURL) return;
211
+ const filePath = fileURLToPath(context.fileURL);
212
+ try {
213
+ const url = rewrite(filePath, node.url);
214
+ if (url !== void 0) context.setProperty(node, "url", url);
215
+ } catch (error) {
216
+ throw failure(filePath, node.url, error);
217
+ }
218
+ };
219
+ return {
220
+ name: "minidocs-content-links",
221
+ link: visit,
222
+ definition: visit
223
+ };
224
+ })();
225
+ /** Remark plugin for the unified processor from `@astrojs/markdown-remark`. */
226
+ function remarkContentLinks() {
227
+ return (tree, file) => {
228
+ const filePath = file.path;
229
+ if (!filePath) return;
230
+ const visit = (node) => {
231
+ if ((node.type === "link" || node.type === "definition") && node.url) try {
232
+ node.url = rewrite(filePath, node.url) ?? node.url;
233
+ } catch (error) {
234
+ throw failure(filePath, node.url, error);
235
+ }
236
+ for (const child of node.children ?? []) visit(child);
237
+ };
238
+ visit(tree);
239
+ };
240
+ }
241
+ //#endregion
242
+ export { registry as a, renderConfigTypes as c, namedCollections as i, renderTypes as l, remarkContentLinks as n, findUnbuiltPages as o, satteriContentLinks as r, findUrlConflicts as s, checkPendingLinks as t };
@@ -0,0 +1,25 @@
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
+ export interface MinidocsGlobOptions {
11
+ /** Path segment the pages live under, e.g. `docs` for `/docs/...`. Defaults to the site root. */
12
+ prefix?: string;
13
+ /** Entries for which it returns `false` never reach the collection. */
14
+ filter?: (entry: FilterEntry) => boolean;
15
+ }
16
+ /**
17
+ * Astro's `glob()` loader, plus recording of the collection root and URL prefix so minidocs can
18
+ * derive locales, page ids and URLs, and generate types for them.
19
+ * Entries rejected by `filter` never reach the collection, so types, links and pages all agree on what exists.
20
+ */
21
+ export declare function glob({ prefix, filter, ...options }: GlobOptions & {
22
+ base: string | URL;
23
+ } & MinidocsGlobOptions): Loader;
24
+ //#endregion
25
+ export {};
@@ -0,0 +1,88 @@
1
+ import { a as registry, i as namedCollections, l as renderTypes, s as findUrlConflicts, t as checkPendingLinks } from "./links-Dp7d5R-O.mjs";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { glob as glob$1 } from "astro/loaders";
6
+ //#region src/loaders.ts
7
+ /** Collection root relative to the project, as Astro's glob resolves `base`. */
8
+ const relativeBase = (base, root) => relative(root, base instanceof URL ? fileURLToPath(base) : resolve(root, base)).replaceAll("\\", "/");
9
+ function register(base, declared) {
10
+ registry.collections[base] ??= {
11
+ base,
12
+ ...declared,
13
+ filePaths: [],
14
+ excluded: /* @__PURE__ */ new Set(),
15
+ loading: false,
16
+ loaded: false
17
+ };
18
+ return Object.assign(registry.collections[base], declared);
19
+ }
20
+ /**
21
+ * Astro's `glob()` loader, plus recording of the collection root and URL prefix so minidocs can
22
+ * derive locales, page ids and URLs, and generate types for them.
23
+ * Entries rejected by `filter` never reach the collection, so types, links and pages all agree on what exists.
24
+ */
25
+ function glob({ prefix = "", filter, ...options }) {
26
+ const inner = glob$1(options);
27
+ const declared = { prefix: prefix.replace(/^\/+|\/+$/g, "") };
28
+ if (registry.root) register(relativeBase(options.base, registry.root), declared);
29
+ return {
30
+ ...inner,
31
+ name: "minidocs-glob",
32
+ async load(context) {
33
+ const state = register(relativeBase(options.base, fileURLToPath(context.config.root)), declared);
34
+ if (state.name !== void 0 && state.name !== context.collection) throw new Error(`[minidocs] Collections "${state.name}" and "${context.collection}" are both loaded from ${state.base}.`);
35
+ state.name = context.collection;
36
+ let scheduled;
37
+ const update = () => {
38
+ clearTimeout(scheduled);
39
+ scheduled = setTimeout(() => writeTypes(context, state, (message) => context.logger.error(message)), 50);
40
+ };
41
+ const store = new Proxy(context.store, { get(target, property, receiver) {
42
+ const value = Reflect.get(target, property, receiver);
43
+ if (property !== "set" && property !== "delete" && property !== "clear") return value;
44
+ return (...args) => {
45
+ update();
46
+ if (property === "set" && filter) {
47
+ const entry = args[0];
48
+ if (!filter(entry)) {
49
+ if (entry.filePath) state.excluded.add(entry.filePath);
50
+ if (target.has(entry.id)) target.delete(entry.id);
51
+ return false;
52
+ }
53
+ if (entry.filePath) state.excluded.delete(entry.filePath);
54
+ }
55
+ return value.apply(target, args);
56
+ };
57
+ } });
58
+ state.loading = true;
59
+ try {
60
+ await inner.load({
61
+ ...context,
62
+ store
63
+ });
64
+ } finally {
65
+ state.loading = false;
66
+ }
67
+ state.loaded = true;
68
+ clearTimeout(scheduled);
69
+ writeTypes(context, state, (message) => {
70
+ throw new Error(message);
71
+ });
72
+ checkPendingLinks();
73
+ }
74
+ };
75
+ }
76
+ function writeTypes(context, state, report) {
77
+ const { settings, typesFile } = registry;
78
+ if (!settings || !typesFile) throw new Error("[minidocs] Add the minidocs() integration to your Astro config to use glob from '@minidocs/astro/loaders'.");
79
+ state.filePaths = context.store.values().map((entry) => entry.filePath).filter((filePath) => Boolean(filePath));
80
+ const collections = namedCollections();
81
+ const path = fileURLToPath(typesFile);
82
+ mkdirSync(dirname(path), { recursive: true });
83
+ writeFileSync(path, renderTypes(settings, collections));
84
+ const conflicts = findUrlConflicts(settings, collections);
85
+ if (conflicts.length > 0) report(conflicts.map((conflict) => `[minidocs] ${conflict}`).join("\n"));
86
+ }
87
+ //#endregion
88
+ export { glob };