@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
@@ -0,0 +1,60 @@
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
+ /** The URL segment of a locale, `undefined` for the default locale when it is not prefixed, as rest parameters expect. */
22
+ function localeParam(settings, locale) {
23
+ const { i18n } = settings;
24
+ if (!i18n || locale === void 0 || locale === i18n.defaultLocale && !i18n.prefixDefaultLocale) return void 0;
25
+ return normalizeLocale(locale);
26
+ }
27
+ /**
28
+ * Path of a page below `base`, e.g. `en/docs/getting-started`, without slashes around it.
29
+ * `undefined` stands for the site root.
30
+ */
31
+ function routePath(settings, collection, id, locale) {
32
+ return [
33
+ localeParam(settings, locale),
34
+ collection.prefix,
35
+ slugOf(id)
36
+ ].filter(Boolean).join("/") || void 0;
37
+ }
38
+ /** Link to a page, including `base` and the trailing slash Astro would use. */
39
+ function href(settings, collection, id, locale) {
40
+ const path = [settings.base, routePath(settings, collection, id, locale)].filter(Boolean).join("/");
41
+ if (!path) return "/";
42
+ return `/${path}${settings.trailingSlash ? "/" : ""}`;
43
+ }
44
+ /**
45
+ * Path of the Markdown version of a page below the locale segment, e.g. `docs/getting-started`.
46
+ * The root of a site without a prefix has no segment of its own, so it becomes `index`.
47
+ */
48
+ function markdownPath(collection, id) {
49
+ return [collection.prefix, slugOf(id)].filter(Boolean).join("/") || "index";
50
+ }
51
+ /** Link to the Markdown version of a page, including `base`. */
52
+ function markdownHref(settings, collection, id, locale) {
53
+ return `/${[
54
+ settings.base,
55
+ localeParam(settings, locale),
56
+ markdownPath(collection, id)
57
+ ].filter(Boolean).join("/")}.md`;
58
+ }
59
+ //#endregion
60
+ export { markdownPath as a, markdownHref as i, href as n, routePath as o, localeParam as r, slugOf as s, contentOptions as t };
@@ -0,0 +1,202 @@
1
+ import { TocNode, TocOptions, findPath, neighbors, tocTree } from "@minidocs/core";
2
+ import { AstroRenderer } from "astro";
3
+ //#region src/llms.d.ts
4
+ interface LlmsTxtOptions<E> {
5
+ /** Name of the site, the heading of the file. */
6
+ title: string;
7
+ /** Description of a page, shown after its link. The description of the `index` page also becomes the summary of the file. */
8
+ describe?: ((entry: E) => string | undefined) | undefined;
9
+ }
10
+ //#endregion
11
+ //#region src/runtime.d.ts
12
+ /** Site-wide types generated from the Astro config: `{ i18n: true; locale: ... }`, or `{ i18n: false }` for single-language sites. */
13
+ export interface Config {}
14
+ /** Per-collection types generated by `glob` from '@minidocs/astro/loaders'. Without them, ids fall back to `string`. */
15
+ export interface Collections {}
16
+ type IsI18n = Config extends {
17
+ i18n: true;
18
+ } ? true : false;
19
+ /** Locales of the site. */
20
+ export type Locale = Config extends {
21
+ locale: infer L extends string;
22
+ } ? L : string;
23
+ /** Names of the collections loaded with minidocs' glob, or any string before their types are generated. */
24
+ export type CollectionName = [keyof Collections] extends [never] ? string : Extract<keyof Collections, string>;
25
+ type Known<C extends string> = C extends keyof Collections ? Collections[C] : never;
26
+ type Lookup<T, L> = T extends string ? T : L extends keyof T ? Extract<T[L], string> : never;
27
+ type Field<C extends string, K extends string, L> = [Known<C>] extends [never] ? string : Known<C> extends Record<K, infer T> ? Lookup<T, L> : string;
28
+ /** Page ids that can be referenced in a locale: its own pages plus pages reachable through fallback. */
29
+ export type PageIdOf<C extends string, L = Locale> = Field<C, 'pages', L>;
30
+ /** Directories that contain pages in a locale. */
31
+ export type DirOf<C extends string, L = Locale> = Field<C, 'dirs', L>;
32
+ /** The minimal shape of a content collection entry minidocs works with. */
33
+ export interface DocEntry {
34
+ collection: string;
35
+ filePath?: string | undefined;
36
+ data: {
37
+ title: string;
38
+ };
39
+ }
40
+ type Id<E extends DocEntry> = PageIdOf<E['collection']>;
41
+ interface LocalizedPageRef<E extends DocEntry> {
42
+ id: Id<E>;
43
+ /** Locale of the tree the node belongs to. */
44
+ locale: Locale;
45
+ /** Locale `entry` comes from. Differs from `locale` only for fallback pages. */
46
+ contentLocale: Locale;
47
+ entry: E;
48
+ isFallback: boolean;
49
+ }
50
+ export interface DocNode<E extends DocEntry> {
51
+ label: string;
52
+ page?: IsI18n extends true ? LocalizedPageRef<E> : {
53
+ id: Id<E>;
54
+ entry: E;
55
+ };
56
+ /** External link target, never set together with `page`. */
57
+ href?: string;
58
+ children: DocNode<E>[];
59
+ }
60
+ interface TreeHelpers<E extends DocEntry, L> {
61
+ page: (id: PageIdOf<E['collection'], L>, children?: DocNode<E>[]) => DocNode<E>;
62
+ group: (label: string, children: DocNode<E>[]) => DocNode<E>;
63
+ /** Returns the contents of a directory, excluding the directory's own index page. */
64
+ dir: (path: DirOf<E['collection'], L>, options?: {
65
+ compare?: (a: DocNode<E>, b: DocNode<E>) => number;
66
+ }) => DocNode<E>[];
67
+ link: (label: string, href: string) => DocNode<E>;
68
+ }
69
+ /** For multilingual sites, one context per locale, so checking `locale` narrows which pages `page()` accepts. */
70
+ export type BuildContext<E extends DocEntry> = IsI18n extends true ? { [L in Locale]: TreeHelpers<E, L> & {
71
+ locale: L;
72
+ }; }[Locale] : TreeHelpers<E, Locale>;
73
+ /** One tree per locale for multilingual sites, a single tree otherwise. */
74
+ export type DocTree<E extends DocEntry> = IsI18n extends true ? Record<Locale, DocNode<E>[]> : DocNode<E>[];
75
+ interface PageInfo<E extends DocEntry> {
76
+ id: Id<E>;
77
+ entry: E;
78
+ /** Route parameters for `src/pages/docs/[...slug].astro`. `slug` is `undefined` for the root page of the collection. */
79
+ params: {
80
+ slug: string | undefined;
81
+ };
82
+ href: string;
83
+ /** Route parameters for `src/pages/[...path].md.ts`, which serves the Markdown versions of every collection. */
84
+ markdownParams: {
85
+ path: string;
86
+ };
87
+ /** Link to the Markdown version of the page. */
88
+ markdownHref: string;
89
+ }
90
+ interface LocalizedPageInfo<E extends DocEntry> extends PageInfo<E> {
91
+ /** Route parameters for `src/pages/[...locale]/docs/[...slug].astro`. `locale` is `undefined` for the default locale when it is not prefixed. */
92
+ params: {
93
+ locale: string | undefined;
94
+ slug: string | undefined;
95
+ };
96
+ /** Route parameters for `src/pages/[...locale]/[...path].md.ts`, which serves the Markdown versions of every collection. */
97
+ markdownParams: {
98
+ locale: string | undefined;
99
+ path: string;
100
+ };
101
+ /** Locale the page is shown in. */
102
+ locale: Locale;
103
+ /** Locale the content comes from. Differs from `locale` only for fallback pages. */
104
+ contentLocale: Locale;
105
+ isFallback: boolean;
106
+ /** Every locale this page exists in, translated or through fallback, including the current one. */
107
+ alternates: {
108
+ locale: Locale;
109
+ href: string;
110
+ }[];
111
+ }
112
+ export type DocPage<E extends DocEntry> = IsI18n extends true ? LocalizedPageInfo<E> : PageInfo<E>;
113
+ type LocaleArgs = IsI18n extends true ? [locale: Locale] : [];
114
+ /** Builds the navigation tree. Multilingual sites get one tree per locale, built by running `build` once for each. */
115
+ export declare function defineTree<E extends DocEntry>(entries: readonly E[], build: (context: BuildContext<E>) => DocNode<E>[] | Promise<DocNode<E>[]>): Promise<DocTree<E>>;
116
+ /** Link to a page, including `base` and the trailing slash Astro would use. Multilingual sites take a locale. */
117
+ export declare function toHref<C extends CollectionName>(collection: C, id: PageIdOf<C>, ...locale: LocaleArgs): string;
118
+ /**
119
+ * The `[...locale]` route parameter of a locale, for the site's own pages such as a home page or a 404 page.
120
+ * `undefined` for the default locale when it is not prefixed.
121
+ */
122
+ export declare function toLocaleParam(locale: Locale): string | undefined;
123
+ /** Whether a component renders for the Markdown version of a page, where it should output plain HTML that converts cleanly. */
124
+ export declare function isMarkdown(astro: {
125
+ locals: object;
126
+ }): boolean;
127
+ export interface MarkdownOptions {
128
+ /** Container renderers for UI framework components in the content, e.g. `getContainerRenderer()` from `@astrojs/react`. MDX is added automatically. */
129
+ renderers?: AstroRenderer[];
130
+ }
131
+ /** A page to convert: one from `listPages`, or the `page` of a tree node. */
132
+ export type MarkdownPage<E extends DocEntry> = IsI18n extends true ? {
133
+ id: Id<E>;
134
+ entry: E;
135
+ locale: Locale;
136
+ } : {
137
+ id: Id<E>;
138
+ entry: E;
139
+ };
140
+ /**
141
+ * Renders the content of a page and converts it into Markdown for agents, without a title.
142
+ * Components can check `isMarkdown(Astro)` to render a form that converts cleanly.
143
+ */
144
+ export declare function toMarkdown(page: MarkdownPage<DocEntry>, options?: MarkdownOptions): Promise<string>;
145
+ /** A node of a tree from any collection, so trees of several collections can be joined into one file. */
146
+ interface TreeNode {
147
+ label: string;
148
+ page?: {
149
+ id: string;
150
+ entry: DocEntry;
151
+ } | undefined;
152
+ href?: string | undefined;
153
+ children: readonly TreeNode[];
154
+ }
155
+ /**
156
+ * Entries of every collection found in a tree, which may join trees of several collections.
157
+ * A tree node is structurally the same as a group wrapping it, so the walk stops at a fixed depth instead.
158
+ */
159
+ type EntryOf<N, Depth extends unknown[] = []> = Depth['length'] extends 4 ? never : N extends {
160
+ page?: infer P;
161
+ children: readonly (infer C)[];
162
+ } ? (P extends {
163
+ entry: infer E;
164
+ } ? E : never) | EntryOf<C, [...Depth, unknown]> : never;
165
+ /**
166
+ * Renders a tree as llms.txt. Top-level pages come first, every top-level node with children becomes a section,
167
+ * and links point to the Markdown versions of pages. The `index` page's description becomes the summary.
168
+ */
169
+ export declare function toLlmsTxt<N extends TreeNode>(nodes: readonly N[], options: LlmsTxtOptions<EntryOf<N>>): string;
170
+ /**
171
+ * Every page of a tree in reading order, as Markdown under its title and source URL, for llms-full.txt.
172
+ * Like llms.txt, links and images are full URLs when `site` is set.
173
+ */
174
+ export declare function toLlmsFullTxt<N extends TreeNode>(nodes: readonly N[], options?: MarkdownOptions): Promise<string>;
175
+ /** Every page that needs a route. Multilingual sites include every locale and untranslated pages served through fallback. */
176
+ export declare function listPages<E extends DocEntry>(entries: readonly E[]): DocPage<E>[];
177
+ interface NavNode {
178
+ page?: {
179
+ id: string;
180
+ locale?: string;
181
+ entry: {
182
+ collection: string;
183
+ };
184
+ } | undefined;
185
+ href?: string | undefined;
186
+ children: NavNode[];
187
+ }
188
+ /** Named after the props of navigation components, so it can be spread onto them. */
189
+ export interface NodeState {
190
+ /** Link of a page or an external link, `undefined` for plain groups. */
191
+ href: string | undefined;
192
+ /** Whether the node is the current page. Only the first occurrence of a duplicated page counts. */
193
+ current: boolean;
194
+ /** Whether the current page is the node itself or below it. */
195
+ open: boolean;
196
+ }
197
+ /** Tracks where the current page sits in a tree. `of(node)` returns what navigation components need to render a node. */
198
+ export declare function navState<N extends NavNode>(nodes: N[], id: string): {
199
+ of(node: N): NodeState;
200
+ };
201
+ //#endregion
202
+ export { type LlmsTxtOptions, type TocNode, type TocOptions, findPath, neighbors, tocTree };
@@ -0,0 +1,327 @@
1
+ import { a as markdownPath, i as markdownHref, n as href, r as localeParam, s as slugOf, t as contentOptions } from "./routing-D28b92w8.mjs";
2
+ import { MissingPageError, createTree, findPath, listPages as listPages$1, navState as navState$1, neighbors, tocTree } from "@minidocs/core";
3
+ import config from "virtual:minidocs/config";
4
+ import { fromHtml } from "hast-util-from-html";
5
+ import { defaultHandlers, toMdast } from "hast-util-to-mdast";
6
+ import { gfmToMarkdown } from "mdast-util-gfm";
7
+ import { toMarkdown as toMarkdown$1 } from "mdast-util-to-markdown";
8
+ //#region src/llms.ts
9
+ const escapeLabel = (text) => text.replace(/[[\]\\]/g, "\\$&");
10
+ const oneLine = (text) => text.replace(/\s+/g, " ").trim();
11
+ /** Pages of a tree in reading order, each page once. Trees of several collections can be joined, so a page is its collection and id. */
12
+ function pagesInOrder(nodes) {
13
+ const pages = /* @__PURE__ */ new Map();
14
+ const visit = (node) => {
15
+ const key = node.page && `${node.page.entry.collection}\0${node.page.id}`;
16
+ if (key && !pages.has(key)) pages.set(key, node.page);
17
+ node.children.forEach(visit);
18
+ };
19
+ nodes.forEach(visit);
20
+ return [...pages.values()];
21
+ }
22
+ /**
23
+ * Renders a tree as llms.txt: the title, the `index` page's description as the summary,
24
+ * top-level pages as a list, then a section for every top-level node with children.
25
+ * `url` links a page to its Markdown version.
26
+ */
27
+ function renderLlmsTxt(nodes, options, url) {
28
+ const item = (node, depth, withChildren = true) => {
29
+ const description = node.page && options.describe?.(node.page.entry);
30
+ const line = node.page ? `[${escapeLabel(node.label)}](${url(node.page)})${description ? `: ${oneLine(description)}` : ""}` : node.href ? `[${escapeLabel(node.label)}](${node.href})` : node.label;
31
+ const children = withChildren ? node.children.flatMap((child) => item(child, depth + 1)) : [];
32
+ return [`${" ".repeat(depth)}- ${line}`, ...children];
33
+ };
34
+ const blocks = [`# ${options.title}`];
35
+ const index = pagesInOrder(nodes).find((page) => page.id === "index");
36
+ const summary = index && options.describe?.(index.entry);
37
+ if (summary) blocks.push(`> ${oneLine(summary)}`);
38
+ const loose = nodes.filter((node) => node.children.length === 0);
39
+ if (loose.length > 0) blocks.push(loose.flatMap((node) => item(node, 0)).join("\n"));
40
+ for (const node of nodes.filter((node) => node.children.length > 0)) {
41
+ const lines = [...node.page ? item(node, 0, false) : [], ...node.children.flatMap((child) => item(child, 0))];
42
+ blocks.push(`## ${node.label}\n\n${lines.join("\n")}`);
43
+ }
44
+ return `${blocks.join("\n\n")}\n`;
45
+ }
46
+ //#endregion
47
+ //#region src/serialize.ts
48
+ const ORIGIN = "http://minidocs.invalid";
49
+ /** URL path of a link, in the form `pathKey` compares with. */
50
+ function pathKey(href) {
51
+ return new URL(href, ORIGIN).pathname.replace(/\/+$/, "");
52
+ }
53
+ /**
54
+ * Turns a URL on the site into a path, pointing pages at their Markdown versions.
55
+ * Relative URLs resolve against the HTML page, since the Markdown file sits one level up.
56
+ * Returns `undefined` for URLs to other sites and links within the page.
57
+ */
58
+ function rewriteUrl(url, context) {
59
+ if (/^([a-z][a-z\d+.-]*:|\/\/|#)/i.test(url)) return void 0;
60
+ const resolved = new URL(url, ORIGIN + context.pageHref);
61
+ const target = context.markdownOf(pathKey(resolved.href));
62
+ const path = target ? target + resolved.hash : resolved.pathname + resolved.search + resolved.hash;
63
+ return context.resolve ? context.resolve(path) : path;
64
+ }
65
+ /** Hands an element to its default handler with one URL property rewritten. */
66
+ const withUrl = (property, handle, context) => (state, node, parent) => {
67
+ const url = node.properties[property];
68
+ const rewritten = typeof url === "string" ? rewriteUrl(url, context) : void 0;
69
+ return handle(state, rewritten ? {
70
+ ...node,
71
+ properties: {
72
+ ...node.properties,
73
+ [property]: rewritten
74
+ }
75
+ } : node, parent);
76
+ };
77
+ const isElement = (node, tagName) => node?.type === "element" && node.tagName === tagName;
78
+ const hasClass = (node, name) => [node.properties.className].flat().includes(name);
79
+ /** Callouts, from the Markdown syntax or the `Callout` component, become GitHub alerts with the title in bold. */
80
+ const callout = (state, node) => {
81
+ if (!hasClass(node, "md-callout") || typeof node.properties.dataType !== "string") return defaultHandlers.div(state, node);
82
+ const elements = node.children.filter((child) => child.type !== "text" || child.value.trim());
83
+ const title = isElement(elements[0], "p") && hasClass(elements[0], "md-callout-title") ? elements[0] : void 0;
84
+ const marker = {
85
+ type: "html",
86
+ value: `[!${node.properties.dataType.toUpperCase()}]`
87
+ };
88
+ const body = state.toFlow(state.all({
89
+ ...node,
90
+ children: title ? elements.slice(1) : elements
91
+ }));
92
+ const [first, ...rest] = body;
93
+ const head = title ? {
94
+ type: "paragraph",
95
+ children: [
96
+ marker,
97
+ {
98
+ type: "text",
99
+ value: "\n"
100
+ },
101
+ {
102
+ type: "strong",
103
+ children: state.all(title)
104
+ }
105
+ ]
106
+ } : first?.type === "paragraph" ? {
107
+ ...first,
108
+ children: [
109
+ marker,
110
+ {
111
+ type: "text",
112
+ value: "\n"
113
+ },
114
+ ...first.children
115
+ ]
116
+ } : void 0;
117
+ const quote = {
118
+ type: "blockquote",
119
+ children: head ? [head, ...title ? body : rest] : [{
120
+ type: "paragraph",
121
+ children: [marker]
122
+ }, ...body]
123
+ };
124
+ state.patch(node, quote);
125
+ return quote;
126
+ };
127
+ /** Shiki puts the language on `<pre>`, but the converter only reads it from a `language-*` class on `<code>`. */
128
+ const pre = (state, node) => {
129
+ const language = node.properties.dataLanguage;
130
+ const code = node.children.find((child) => isElement(child, "code"));
131
+ if (typeof language === "string" && language !== "plaintext" && code) code.properties.className = [`language-${language}`];
132
+ return defaultHandlers.pre(state, node);
133
+ };
134
+ /** Converts a page rendered by Astro into Markdown for agents. */
135
+ function htmlToMarkdown(html, context) {
136
+ const handlers = {
137
+ a: withUrl("href", defaultHandlers.a, context),
138
+ img: withUrl("src", defaultHandlers.img, context),
139
+ div: callout,
140
+ pre
141
+ };
142
+ const mdast = toMdast(fromHtml(html, { fragment: true }), { handlers });
143
+ return toMarkdown$1(mdast, {
144
+ extensions: [gfmToMarkdown()],
145
+ bullet: "-",
146
+ rule: "-",
147
+ fences: true,
148
+ listItemIndent: "one"
149
+ });
150
+ }
151
+ //#endregion
152
+ //#region src/runtime.ts
153
+ function collectionNamed(name) {
154
+ const collection = config.collections[name];
155
+ if (!collection) throw new Error(`[minidocs] Collection "${name}" must be loaded with glob from '@minidocs/astro/loaders'.`);
156
+ return collection;
157
+ }
158
+ function collectionOf(entries) {
159
+ const name = entries[0]?.collection;
160
+ if (!name) throw new Error("[minidocs] Expected at least one entry.");
161
+ if (entries.some((entry) => entry.collection !== name)) throw new Error("[minidocs] All entries must come from the same collection.");
162
+ return collectionNamed(name);
163
+ }
164
+ /** Drops locale details from nodes of single-language sites, where they carry no meaning. */
165
+ function withoutLocale(nodes) {
166
+ return nodes.map(({ page, children, ...node }) => ({
167
+ ...node,
168
+ ...page && { page: {
169
+ id: page.id,
170
+ entry: page.entry
171
+ } },
172
+ children: withoutLocale(children)
173
+ }));
174
+ }
175
+ /** Builds the navigation tree. Multilingual sites get one tree per locale, built by running `build` once for each. */
176
+ async function defineTree(entries, build) {
177
+ const collection = collectionOf(entries);
178
+ const options = contentOptions(config, collection.base);
179
+ const run = build;
180
+ let tree;
181
+ try {
182
+ tree = await createTree(entries, {
183
+ ...options,
184
+ label: (entry) => entry.data.title
185
+ }, config.i18n ? run : ({ locale: _, ...helpers }) => run(helpers));
186
+ } catch (error) {
187
+ if (error instanceof MissingPageError && collection.pageIds.includes(error.id)) throw new Error(`[minidocs] Page "${error.id}" is in the collection but not among the entries passed to defineTree. To leave pages out, use the filter option of glob from '@minidocs/astro/loaders' instead of filtering the entries.`, { cause: error });
188
+ throw error;
189
+ }
190
+ return config.i18n ? tree : withoutLocale(tree[options.locales[0]]);
191
+ }
192
+ /** Link to a page, including `base` and the trailing slash Astro would use. Multilingual sites take a locale. */
193
+ function toHref(collection, id, ...locale) {
194
+ return href(config, collectionNamed(collection), id, locale[0]);
195
+ }
196
+ /**
197
+ * The `[...locale]` route parameter of a locale, for the site's own pages such as a home page or a 404 page.
198
+ * `undefined` for the default locale when it is not prefixed.
199
+ */
200
+ function toLocaleParam(locale) {
201
+ if (!config.i18n) throw new Error("[minidocs] toLocaleParam requires `i18n` in your Astro config.");
202
+ return localeParam(config, locale);
203
+ }
204
+ const MARKDOWN = "minidocs:markdown";
205
+ /** Whether a component renders for the Markdown version of a page, where it should output plain HTML that converts cleanly. */
206
+ function isMarkdown(astro) {
207
+ return astro.locals[MARKDOWN] === true;
208
+ }
209
+ const containers = /* @__PURE__ */ new Map();
210
+ async function containerFor(extra) {
211
+ const { default: builtin } = await import("virtual:minidocs/renderers");
212
+ const renderers = [...builtin, ...extra];
213
+ const key = renderers.map(({ name }) => name).join(",");
214
+ let container = containers.get(key);
215
+ if (!container) {
216
+ container = (async () => {
217
+ const [{ experimental_AstroContainer }, { loadRenderers }] = await Promise.all([import("astro/container"), import("astro:container")]);
218
+ return experimental_AstroContainer.create({ renderers: await loadRenderers(renderers) });
219
+ })();
220
+ containers.set(key, container);
221
+ }
222
+ return container;
223
+ }
224
+ let markdownPages;
225
+ /** Markdown versions of every page on the site, keyed by the URL path of the HTML page, so links into any collection convert. */
226
+ function markdownPagesOnSite() {
227
+ if (!markdownPages) {
228
+ markdownPages = /* @__PURE__ */ new Map();
229
+ for (const collection of Object.values(config.collections)) {
230
+ const locales = config.i18n?.locales ?? [void 0];
231
+ for (const id of collection.pageIds) for (const locale of locales) markdownPages.set(pathKey(href(config, collection, id, locale)), markdownHref(config, collection, id, locale));
232
+ }
233
+ }
234
+ return markdownPages;
235
+ }
236
+ /** Full URL of a path when `site` is set, for text that is often pasted where paths do not resolve. */
237
+ const fullUrl = (path) => config.site ? new URL(path, config.site).href : path;
238
+ /**
239
+ * Renders the content of a page and converts it into Markdown for agents, without a title.
240
+ * Components can check `isMarkdown(Astro)` to render a form that converts cleanly.
241
+ */
242
+ function toMarkdown(page, options = {}) {
243
+ return renderMarkdown(page, options);
244
+ }
245
+ /** `toMarkdown`, with every URL on the site passed through `resolve`. */
246
+ async function renderMarkdown(page, options, resolve) {
247
+ const { id, entry } = page;
248
+ const locale = page.locale;
249
+ const [{ render }, container] = await Promise.all([import("astro:content"), containerFor(options.renderers ?? [])]);
250
+ const { Content } = await render(entry);
251
+ let html;
252
+ try {
253
+ html = await container.renderToString(Content, { locals: { [MARKDOWN]: true } });
254
+ } catch (error) {
255
+ if (error.name === "NoMatchingRenderer") throw new Error(`[minidocs] Page "${id}" uses a component the Markdown version cannot render. Pass its container renderer to toMarkdown, e.g. \`toMarkdown(page, { renderers: [getContainerRenderer()] })\` with getContainerRenderer from the framework's integration.`, { cause: error });
256
+ throw error;
257
+ }
258
+ return htmlToMarkdown(html, {
259
+ pageHref: href(config, collectionNamed(entry.collection), id, locale),
260
+ markdownOf: (path) => markdownPagesOnSite().get(path),
261
+ resolve
262
+ });
263
+ }
264
+ const markdownUrl = ({ id, locale, entry }) => fullUrl(markdownHref(config, collectionNamed(entry.collection), id, locale));
265
+ /**
266
+ * Renders a tree as llms.txt. Top-level pages come first, every top-level node with children becomes a section,
267
+ * and links point to the Markdown versions of pages. The `index` page's description becomes the summary.
268
+ */
269
+ function toLlmsTxt(nodes, options) {
270
+ return renderLlmsTxt(nodes, options, markdownUrl);
271
+ }
272
+ /**
273
+ * Every page of a tree in reading order, as Markdown under its title and source URL, for llms-full.txt.
274
+ * Like llms.txt, links and images are full URLs when `site` is set.
275
+ */
276
+ async function toLlmsFullTxt(nodes, options = {}) {
277
+ const pages = pagesInOrder(nodes);
278
+ return (await Promise.all(pages.map(async (page) => `# ${page.entry.data.title}\n\nSource: ${markdownUrl(page)}\n\n${await renderMarkdown(page, options, fullUrl)}`))).join("\n");
279
+ }
280
+ /** Every page that needs a route. Multilingual sites include every locale and untranslated pages served through fallback. */
281
+ function listPages(entries) {
282
+ const collection = collectionOf(entries);
283
+ const pages = listPages$1(entries, contentOptions(config, collection.base));
284
+ if (!config.i18n) return pages.map(({ id, entry }) => ({
285
+ id,
286
+ entry,
287
+ params: { slug: slugOf(id) || void 0 },
288
+ href: href(config, collection, id),
289
+ markdownParams: { path: markdownPath(collection, id) },
290
+ markdownHref: markdownHref(config, collection, id)
291
+ }));
292
+ const alternates = /* @__PURE__ */ new Map();
293
+ for (const page of pages) {
294
+ const list = alternates.get(page.id) ?? [];
295
+ list.push({
296
+ locale: page.locale,
297
+ href: href(config, collection, page.id, page.locale)
298
+ });
299
+ alternates.set(page.id, list);
300
+ }
301
+ return pages.map((page) => ({
302
+ ...page,
303
+ params: {
304
+ locale: localeParam(config, page.locale),
305
+ slug: slugOf(page.id) || void 0
306
+ },
307
+ href: href(config, collection, page.id, page.locale),
308
+ markdownParams: {
309
+ locale: localeParam(config, page.locale),
310
+ path: markdownPath(collection, page.id)
311
+ },
312
+ markdownHref: markdownHref(config, collection, page.id, page.locale),
313
+ alternates: alternates.get(page.id)
314
+ }));
315
+ }
316
+ /** Tracks where the current page sits in a tree. `of(node)` returns what navigation components need to render a node. */
317
+ function navState(nodes, id) {
318
+ const state = navState$1(nodes, id);
319
+ return { of(node) {
320
+ return {
321
+ href: node.page ? href(config, collectionNamed(node.page.entry.collection), node.page.id, node.page.locale) : node.href,
322
+ ...state.of(node)
323
+ };
324
+ } };
325
+ }
326
+ //#endregion
327
+ export { defineTree, findPath, isMarkdown, listPages, navState, neighbors, toHref, toLlmsFullTxt, toLlmsTxt, toLocaleParam, toMarkdown, tocTree };
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@minidocs/astro",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Astro integration for building documentation sites from composable primitives",
5
+ "keywords": [
6
+ "astro",
7
+ "astro-integration",
8
+ "withastro",
9
+ "documentation",
10
+ "docs"
11
+ ],
12
+ "homepage": "https://github.com/Ivan-1F/minidocs#readme",
13
+ "bugs": "https://github.com/Ivan-1F/minidocs/issues",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Ivan-1F/minidocs.git",
17
+ "directory": "packages/astro"
18
+ },
19
+ "license": "MIT",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.mts",
24
+ "default": "./dist/index.mjs"
25
+ },
26
+ "./loaders": {
27
+ "types": "./dist/loaders.d.mts",
28
+ "default": "./dist/loaders.mjs"
29
+ },
30
+ "./runtime": {
31
+ "types": "./dist/runtime.d.mts",
32
+ "default": "./dist/runtime.mjs"
33
+ },
34
+ "./behaviors": {
35
+ "types": "./dist/behaviors.d.mts",
36
+ "default": "./dist/behaviors.mjs"
37
+ },
38
+ "./components": "./src/components/index.ts",
39
+ "./styles.css": "./src/styles.css",
40
+ "./styles/*": "./src/styles/*"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "src/components/**/*.{astro,ts}",
48
+ "src/styles.css",
49
+ "src/styles",
50
+ "!**/*.test.ts"
51
+ ],
52
+ "peerDependencies": {
53
+ "astro": "^7.0.0"
54
+ },
55
+ "dependencies": {
56
+ "hast-util-from-html": "^2.0.3",
57
+ "hast-util-to-mdast": "^10.1.2",
58
+ "mdast-util-gfm": "^3.1.0",
59
+ "mdast-util-to-markdown": "^2.2.0",
60
+ "remark-directive": "^4.0.0",
61
+ "@minidocs/core": "0.2.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/hast": "^3.0.5",
65
+ "@types/mdast": "^4.0.4",
66
+ "@types/node": "^24",
67
+ "astro": "^7.4.0",
68
+ "happy-dom": "^21.0.4",
69
+ "rehype-stringify": "^10.0.1",
70
+ "remark-parse": "^11.0.0",
71
+ "remark-rehype": "^11.1.2",
72
+ "satteri": "0.10.5",
73
+ "tsdown": "^0.23.1",
74
+ "typescript": "^7.0.2",
75
+ "unified": "^11.0.5",
76
+ "vitest": "^5.0.5"
77
+ },
78
+ "scripts": {
79
+ "build": "tsdown",
80
+ "test": "vitest run --passWithNoTests",
81
+ "typecheck": "tsc --noEmit && tsc --noEmit -p test-d/i18n && tsc --noEmit -p test-d/single"
82
+ }
6
83
  }
@@ -0,0 +1,9 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+
4
+ type Props = HTMLAttributes<'article'>;
5
+
6
+ const { class: className, ...attrs } = Astro.props;
7
+ ---
8
+
9
+ <article class:list={['md-prose', className]} {...attrs}><slot /></article>