nimbus-docs 0.1.6 → 0.1.8
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/README.md +23 -0
- package/dist/cli/index.js +20 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/content.d.ts +62 -6
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +11 -5
- package/dist/content.js.map +1 -1
- package/dist/{diagnostic-C6OaBe_o.d.ts → diagnostic-ewiZxpSO.d.ts} +4 -1
- package/dist/diagnostic-ewiZxpSO.d.ts.map +1 -0
- package/dist/index.d.ts +52 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +349 -159
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +350 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +782 -0
- package/dist/react.js.map +1 -0
- package/dist/{rules-DnAP-j89.js → rules-B7o0k3TA.js} +249 -2
- package/dist/rules-B7o0k3TA.js.map +1 -0
- package/dist/schemas.d.ts +85 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +32 -6
- package/dist/schemas.js.map +1 -1
- package/dist/strict-keys-fbKKxxKL.js +141 -0
- package/dist/strict-keys-fbKKxxKL.js.map +1 -0
- package/dist/types.d.ts +49 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +37 -19
- package/dist/diagnostic-C6OaBe_o.d.ts.map +0 -1
- package/dist/rules-DnAP-j89.js.map +0 -1
- package/dist/strict-keys-BiXiT3pq.js +0 -35
- package/dist/strict-keys-BiXiT3pq.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as validateLintOptions, r as suggest, t as IMPLEMENTED_CODES } from "./rules-
|
|
2
|
-
import { t as withStrictKeys } from "./strict-keys-
|
|
1
|
+
import { o as validateLintOptions, r as suggest, t as IMPLEMENTED_CODES } from "./rules-B7o0k3TA.js";
|
|
2
|
+
import { i as toRouteKey, n as isAbsoluteUrl, r as toBrowserHref, t as withStrictKeys } from "./strict-keys-fbKKxxKL.js";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import fs from "node:fs";
|
|
@@ -162,112 +162,6 @@ function canonicalEntryUrl(prefix, entryId) {
|
|
|
162
162
|
return `${prefix}/${slug}`;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//#endregion
|
|
166
|
-
//#region src/_internal/url.ts
|
|
167
|
-
/**
|
|
168
|
-
* Internal URL helpers — one shape for matching, one shape for rendering.
|
|
169
|
-
*
|
|
170
|
-
* Static hosts that serve `page/index.html` (Astro's default `build.format:
|
|
171
|
-
* "directory"`) canonicalize to a trailing-slash URL. If framework helpers
|
|
172
|
-
* emit slashless hrefs, every sidebar click costs a 307 redirect before
|
|
173
|
-
* Astro's client router can pick up the page. The fix splits href shape
|
|
174
|
-
* into two forms:
|
|
175
|
-
*
|
|
176
|
-
* - `toRouteKey(href)` — slashless canonical form. Used wherever the
|
|
177
|
-
* framework compares paths for identity (active sidebar state,
|
|
178
|
-
* prev/next lookup, validation against the indexed route set).
|
|
179
|
-
*
|
|
180
|
-
* - `toBrowserHref(href)` — what we emit into `<a href>` / `<link>` for
|
|
181
|
-
* HTML document routes. Adds a trailing slash so the URL matches the
|
|
182
|
-
* directory-index page the host serves directly.
|
|
183
|
-
*
|
|
184
|
-
* Asset URLs (`.md`, `.png`, `.txt`, …), external URLs, and anchor-only
|
|
185
|
-
* hrefs are returned unchanged by `toBrowserHref` — they aren't HTML
|
|
186
|
-
* document routes and adding a slash would break them.
|
|
187
|
-
*
|
|
188
|
-
* Keep these out of the public API: starter components consume hrefs the
|
|
189
|
-
* framework already shaped. Authors don't (and shouldn't) call these
|
|
190
|
-
* directly.
|
|
191
|
-
*/
|
|
192
|
-
/**
|
|
193
|
-
* True for hrefs that point off-site — anything with a URI scheme
|
|
194
|
-
* (`https:`, `mailto:`, `data:`, …) or a protocol-relative `//cdn.…`
|
|
195
|
-
* prefix. Bare relative paths like `"cli"` and `"./foo"` are NOT external
|
|
196
|
-
* — they resolve against the current page and the framework shouldn't
|
|
197
|
-
* second-guess them.
|
|
198
|
-
*/
|
|
199
|
-
function isAbsoluteUrl(href) {
|
|
200
|
-
return /^([a-z][a-z0-9+\-.]*:|\/\/)/i.test(href);
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Detect whether the final path segment looks like a file (has an
|
|
204
|
-
* extension). HTML document routes don't carry an extension under
|
|
205
|
-
* `build.format: "directory"`; assets like `/og/card.png`,
|
|
206
|
-
* `/llms.txt`, and `/cli/index.md` do.
|
|
207
|
-
*
|
|
208
|
-
* Conservative: only treats short, ASCII-letter-only extensions as files,
|
|
209
|
-
* so paths with dots inside a segment (`/v1.2/foo`, version slugs) still
|
|
210
|
-
* count as document routes.
|
|
211
|
-
*/
|
|
212
|
-
function hasFileExtension(pathname) {
|
|
213
|
-
const lastSegment = pathname.slice(pathname.lastIndexOf("/") + 1);
|
|
214
|
-
const dot = lastSegment.lastIndexOf(".");
|
|
215
|
-
if (dot <= 0) return false;
|
|
216
|
-
const ext = lastSegment.slice(dot + 1);
|
|
217
|
-
return ext.length > 0 && ext.length <= 6 && /^[a-zA-Z0-9]+$/.test(ext);
|
|
218
|
-
}
|
|
219
|
-
/** Split an href into `[pathname, suffix]` where `suffix` is the `?…#…` tail. */
|
|
220
|
-
function splitSuffix(href) {
|
|
221
|
-
const queryAt = href.indexOf("?");
|
|
222
|
-
const hashAt = href.indexOf("#");
|
|
223
|
-
const cutAt = queryAt === -1 ? hashAt : hashAt === -1 ? queryAt : Math.min(queryAt, hashAt);
|
|
224
|
-
if (cutAt === -1) return [href, ""];
|
|
225
|
-
return [href.slice(0, cutAt), href.slice(cutAt)];
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Slashless canonical form for path comparisons.
|
|
229
|
-
*
|
|
230
|
-
* /cli → /cli
|
|
231
|
-
* /cli/ → /cli
|
|
232
|
-
* /cli/?x=1#y → /cli
|
|
233
|
-
* / → /
|
|
234
|
-
* /guides/setup/ → /guides/setup
|
|
235
|
-
*
|
|
236
|
-
* Strips query and hash so callers can compare two hrefs that differ only
|
|
237
|
-
* in their tail. Root stays `"/"` — that's identity, not a trailing-slash
|
|
238
|
-
* artifact.
|
|
239
|
-
*/
|
|
240
|
-
function toRouteKey(href) {
|
|
241
|
-
const [pathname] = splitSuffix(href);
|
|
242
|
-
if (pathname.length <= 1) return pathname || "/";
|
|
243
|
-
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Trailing-slash form for browser-facing hrefs to HTML document routes.
|
|
247
|
-
* Preserves query and hash; root, external URLs, anchor-only hrefs, and
|
|
248
|
-
* asset URLs (paths with a file extension) are returned unchanged.
|
|
249
|
-
*
|
|
250
|
-
* /cli → /cli/
|
|
251
|
-
* /cli/ → /cli/
|
|
252
|
-
* /cli#install → /cli/#install
|
|
253
|
-
* /cli?v=1 → /cli/?v=1
|
|
254
|
-
* / → /
|
|
255
|
-
* /og/card.png → /og/card.png (asset, unchanged)
|
|
256
|
-
* /cli/index.md → /cli/index.md (asset, unchanged)
|
|
257
|
-
* https://x.com/a → https://x.com/a (external, unchanged)
|
|
258
|
-
* #anchor → #anchor (anchor-only, unchanged)
|
|
259
|
-
*/
|
|
260
|
-
function toBrowserHref(href) {
|
|
261
|
-
if (isAbsoluteUrl(href)) return href;
|
|
262
|
-
if (href.startsWith("#") || href.startsWith("?")) return href;
|
|
263
|
-
if (!href.startsWith("/")) return href;
|
|
264
|
-
const [pathname, suffix] = splitSuffix(href);
|
|
265
|
-
if (pathname === "/") return href;
|
|
266
|
-
if (hasFileExtension(pathname)) return href;
|
|
267
|
-
if (pathname.endsWith("/")) return href;
|
|
268
|
-
return `${pathname}/${suffix}`;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
165
|
//#endregion
|
|
272
166
|
//#region src/_internal/sidebar.ts
|
|
273
167
|
const sortKeyByItem = /* @__PURE__ */ new WeakMap();
|
|
@@ -307,18 +201,44 @@ function joinHref(hrefPrefix, entryId) {
|
|
|
307
201
|
return toBrowserHref(canonicalEntryUrl(hrefPrefix.replace(/\/$/, ""), entryId));
|
|
308
202
|
}
|
|
309
203
|
function createLink(entry, currentPath, hrefPrefix = "") {
|
|
310
|
-
const
|
|
204
|
+
const internalHref = joinHref(hrefPrefix, entry.id);
|
|
311
205
|
const badge = entry.data.draft ? entry.data.sidebar?.badge ?? {
|
|
312
206
|
text: "Draft",
|
|
313
207
|
variant: "warning"
|
|
314
208
|
} : entry.data.sidebar?.badge;
|
|
209
|
+
const label = entry.data.sidebar?.label ?? entry.data.title;
|
|
210
|
+
const order = entry.data.sidebar?.order ?? Number.MAX_VALUE;
|
|
211
|
+
const externalLink = entry.data.external_link;
|
|
212
|
+
if (externalLink) {
|
|
213
|
+
if (isAbsoluteUrl(externalLink)) {
|
|
214
|
+
const ext = {
|
|
215
|
+
type: "external",
|
|
216
|
+
label,
|
|
217
|
+
href: externalLink,
|
|
218
|
+
badge,
|
|
219
|
+
order
|
|
220
|
+
};
|
|
221
|
+
sortKeyByItem.set(ext, entry.id);
|
|
222
|
+
return ext;
|
|
223
|
+
}
|
|
224
|
+
const link = {
|
|
225
|
+
type: "link",
|
|
226
|
+
label,
|
|
227
|
+
href: toBrowserHref(externalLink),
|
|
228
|
+
isCurrent: false,
|
|
229
|
+
badge,
|
|
230
|
+
order
|
|
231
|
+
};
|
|
232
|
+
sortKeyByItem.set(link, entry.id);
|
|
233
|
+
return link;
|
|
234
|
+
}
|
|
315
235
|
const link = {
|
|
316
236
|
type: "link",
|
|
317
|
-
label
|
|
318
|
-
href,
|
|
319
|
-
isCurrent: toRouteKey(currentPath) === toRouteKey(
|
|
237
|
+
label,
|
|
238
|
+
href: internalHref,
|
|
239
|
+
isCurrent: toRouteKey(currentPath) === toRouteKey(internalHref),
|
|
320
240
|
badge,
|
|
321
|
-
order
|
|
241
|
+
order
|
|
322
242
|
};
|
|
323
243
|
sortKeyByItem.set(link, entry.id);
|
|
324
244
|
return link;
|
|
@@ -329,8 +249,13 @@ function buildFilesystemTree(entries, currentPath, directory, hrefPrefix = "") {
|
|
|
329
249
|
function buildLevel(parentPath) {
|
|
330
250
|
const result = [];
|
|
331
251
|
const groupsAtLevel = /* @__PURE__ */ new Map();
|
|
252
|
+
if (directory && parentPath === directory) {
|
|
253
|
+
const dirIndex = byId.get(directory);
|
|
254
|
+
if (dirIndex) result.push(createLink(dirIndex, currentPath, hrefPrefix));
|
|
255
|
+
}
|
|
332
256
|
for (const entry of scoped) {
|
|
333
257
|
if (entry.id === "index") continue;
|
|
258
|
+
if (entry.id === directory) continue;
|
|
334
259
|
const id = entry.id;
|
|
335
260
|
const relativeTo = directory ?? "";
|
|
336
261
|
const relativeId = relativeTo ? id === relativeTo ? "" : id.slice(relativeTo.length + 1) : id;
|
|
@@ -375,26 +300,40 @@ function buildFilesystemTree(entries, currentPath, directory, hrefPrefix = "") {
|
|
|
375
300
|
for (const [groupPath, group] of groupsAtLevel) {
|
|
376
301
|
const nestedChildren = buildLevel(groupPath);
|
|
377
302
|
group.children = [...group.children, ...nestedChildren].sort(sortSidebarItems);
|
|
378
|
-
if (group.children.length > 0)
|
|
379
|
-
const minChildOrder = Math.min(...group.children.map((item) => item.order));
|
|
380
|
-
group.order = Math.min(group.order, minChildOrder);
|
|
381
|
-
}
|
|
303
|
+
if (group.order === Number.MAX_VALUE && group.children.length > 0) group.order = Math.min(...group.children.map((item) => item.order));
|
|
382
304
|
}
|
|
383
305
|
return result.sort(sortSidebarItems);
|
|
384
306
|
}
|
|
385
307
|
function createGroupFromEntry(dirPath, indexEntry, currentPath, _byId) {
|
|
386
308
|
const dirSegment = dirPath.split("/").pop();
|
|
387
|
-
const
|
|
309
|
+
const groupConfig = indexEntry?.data.sidebar?.group;
|
|
310
|
+
const groupLabel = groupConfig?.label ?? indexEntry?.data.sidebar?.label ?? indexEntry?.data.title ?? formatLabel(dirSegment);
|
|
388
311
|
const groupOrder = indexEntry?.data.sidebar?.order ?? Number.MAX_VALUE;
|
|
389
|
-
const
|
|
390
|
-
|
|
312
|
+
const groupBadge = groupConfig?.badge ?? indexEntry?.data.sidebar?.badge;
|
|
313
|
+
let indexHref;
|
|
314
|
+
let indexIsCurrent = false;
|
|
315
|
+
let indexIsExternal = false;
|
|
316
|
+
if (indexEntry) {
|
|
317
|
+
const externalLink = indexEntry.data.external_link;
|
|
318
|
+
if (externalLink !== void 0) if (isAbsoluteUrl(externalLink)) {
|
|
319
|
+
indexHref = externalLink;
|
|
320
|
+
indexIsExternal = true;
|
|
321
|
+
} else indexHref = toBrowserHref(externalLink);
|
|
322
|
+
else {
|
|
323
|
+
indexHref = joinHref(hrefPrefix, indexEntry.id);
|
|
324
|
+
indexIsCurrent = toRouteKey(currentPath) === toRouteKey(indexHref);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
391
327
|
const group = {
|
|
392
328
|
type: "group",
|
|
393
329
|
label: groupLabel,
|
|
394
330
|
order: groupOrder,
|
|
395
|
-
badge:
|
|
396
|
-
children,
|
|
397
|
-
_indexId: indexEntry?.id
|
|
331
|
+
badge: groupBadge,
|
|
332
|
+
children: [],
|
|
333
|
+
_indexId: indexEntry?.id,
|
|
334
|
+
indexHref,
|
|
335
|
+
indexIsCurrent: indexIsCurrent || void 0,
|
|
336
|
+
indexIsExternal: indexIsExternal || void 0
|
|
398
337
|
};
|
|
399
338
|
sortKeyByItem.set(group, dirPath);
|
|
400
339
|
return group;
|
|
@@ -494,45 +433,80 @@ function resolveConfigItems(configItems, entriesByCollection, primaryCollection,
|
|
|
494
433
|
* Return only the children of the top-level group containing the current
|
|
495
434
|
* page. Falls back to the full tree if the current page isn't inside any
|
|
496
435
|
* group (e.g. a top-level link, or a path that doesn't resolve).
|
|
436
|
+
*
|
|
437
|
+
* Under structural separation the group's landing page lives on
|
|
438
|
+
* `indexHref` rather than in `children`. When the active group has a
|
|
439
|
+
* landing, we prepend a synthetic link (or external item) for it so
|
|
440
|
+
* the section landing remains reachable from the scoped rail — without
|
|
441
|
+
* this, on `/api/` the rail shows `/api/users`, `/api/orders`, … but
|
|
442
|
+
* the section's own overview page would be missing from the rail.
|
|
497
443
|
*/
|
|
498
444
|
function scopeToCurrentSection(items, currentPath) {
|
|
499
445
|
if (!currentPath.split("/").filter(Boolean)[0]) return items;
|
|
500
446
|
for (const item of items) if (item.type === "group") {
|
|
501
|
-
if (hasActivePage(item, currentPath))
|
|
447
|
+
if (hasActivePage(item, currentPath)) {
|
|
448
|
+
if (!item.indexHref) return item.children;
|
|
449
|
+
return [item.indexIsExternal ? {
|
|
450
|
+
type: "external",
|
|
451
|
+
label: item.label,
|
|
452
|
+
href: item.indexHref,
|
|
453
|
+
badge: item.badge,
|
|
454
|
+
order: Number.NEGATIVE_INFINITY
|
|
455
|
+
} : {
|
|
456
|
+
type: "link",
|
|
457
|
+
label: item.label,
|
|
458
|
+
href: item.indexHref,
|
|
459
|
+
isCurrent: item.indexIsCurrent === true,
|
|
460
|
+
badge: item.badge,
|
|
461
|
+
order: Number.NEGATIVE_INFINITY
|
|
462
|
+
}, ...item.children];
|
|
463
|
+
}
|
|
502
464
|
}
|
|
503
465
|
return items;
|
|
504
466
|
}
|
|
505
467
|
function hasActivePage(item, currentPath) {
|
|
506
468
|
if (item.type === "link") return item.isCurrent === true;
|
|
507
469
|
if (item.type === "external") return false;
|
|
470
|
+
if (item.indexIsCurrent === true) return true;
|
|
508
471
|
return item.children.some((child) => hasActivePage(child, currentPath));
|
|
509
472
|
}
|
|
510
473
|
/**
|
|
511
|
-
* Derive one section per top-level group in the sidebar tree
|
|
512
|
-
* `Header.astro` to render
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
474
|
+
* Derive one section per top-level group in the sidebar tree, scoped
|
|
475
|
+
* to *cross-collection* navigation. Used by `Header.astro` to render
|
|
476
|
+
* the section tab strip.
|
|
477
|
+
*
|
|
478
|
+
* Filter rule: only groups whose `_prefix` is set become sections. The
|
|
479
|
+
* `_prefix` field is populated exclusively by `autogenerate: { collection: <non-primary> }`
|
|
480
|
+
* config items (see `resolveConfigItems`). This is the structural
|
|
481
|
+
* signal that the group represents a *separate collection mounted at a
|
|
482
|
+
* URL prefix* — e.g. `Components` mounted at `/components/` —
|
|
483
|
+
* rather than a sub-directory of the primary docs collection.
|
|
484
|
+
*
|
|
485
|
+
* Why this matters: under the previous unconditional behavior, every
|
|
486
|
+
* top-level group in the sidebar (including `wip/`, `lab/`, and other
|
|
487
|
+
* docs-collection subdirectories) was promoted to a header tab. The
|
|
488
|
+
* header rail is meant for "other collections" navigation, not for
|
|
489
|
+
* sub-sections of the default collection — those belong in the
|
|
490
|
+
* sidebar's own collapsible tree.
|
|
491
|
+
*
|
|
492
|
+
* Caller must pass the *un-scoped* tree (the result of
|
|
493
|
+
* `buildSidebarTree`, not `getSidebar`); otherwise only the current
|
|
494
|
+
* section's children are visible and the derivation collapses to a
|
|
495
|
+
* single item.
|
|
516
496
|
*/
|
|
517
497
|
function deriveSidebarSections(items) {
|
|
518
498
|
return items.flatMap((item) => {
|
|
519
499
|
if (item.type !== "group") return [];
|
|
520
|
-
|
|
500
|
+
if (!item._prefix) return [];
|
|
501
|
+
const links = flattenSidebar(item.children);
|
|
521
502
|
if (links.length === 0) return [];
|
|
522
503
|
return [{
|
|
523
504
|
label: item.label,
|
|
524
|
-
href: toBrowserHref(item._prefix
|
|
505
|
+
href: toBrowserHref(item._prefix),
|
|
525
506
|
isActive: links.some((link) => link.isCurrent === true)
|
|
526
507
|
}];
|
|
527
508
|
});
|
|
528
509
|
}
|
|
529
|
-
/** Depth-first walk; collect every internal link descendant. */
|
|
530
|
-
function flattenLinks(items) {
|
|
531
|
-
const out = [];
|
|
532
|
-
for (const item of items) if (item.type === "link") out.push(item);
|
|
533
|
-
else if (item.type === "group") out.push(...flattenLinks(item.children));
|
|
534
|
-
return out;
|
|
535
|
-
}
|
|
536
510
|
/**
|
|
537
511
|
* Build the un-scoped sidebar tree from config + content entries.
|
|
538
512
|
*
|
|
@@ -557,11 +531,54 @@ function buildSidebarTree(entriesByCollection, primaryCollection, currentPath, c
|
|
|
557
531
|
else items = buildFilesystemTree(primaryEntries, currentPath, void 0, primaryPrefix);
|
|
558
532
|
const pooledEntries = Object.values(entriesByCollection).flat();
|
|
559
533
|
items = processHideChildren(items, pooledEntries);
|
|
534
|
+
if (config?.overviewLabel) {
|
|
535
|
+
const label = typeof config.overviewLabel === "string" ? config.overviewLabel : "Overview";
|
|
536
|
+
items = applyOverviewLabel(items, label);
|
|
537
|
+
}
|
|
538
|
+
if (config?.defaultCollapsed) applyDefaultCollapsed(items);
|
|
539
|
+
return items;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Walk every group and stamp `collapsed: true` where no explicit value
|
|
543
|
+
* was set. Used by the `sidebar.defaultCollapsed` opt-in. Recurses into
|
|
544
|
+
* nested children so a deeply-structured tree collapses at every level.
|
|
545
|
+
*/
|
|
546
|
+
function applyDefaultCollapsed(items) {
|
|
547
|
+
for (const item of items) if (item.type === "group") {
|
|
548
|
+
if (item.collapsed === void 0) item.collapsed = true;
|
|
549
|
+
applyDefaultCollapsed(item.children);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* @deprecated Effective no-op under structural separation. Pre-2026
|
|
554
|
+
* Nimbus rendered the group's index as the first child link and used
|
|
555
|
+
* this to rename that link to "Overview". The index is now exposed via
|
|
556
|
+
* `SidebarGroupItem.indexHref` (the group label IS the link), so there's
|
|
557
|
+
* no first-child index to rename. The function is kept so older configs
|
|
558
|
+
* that set `sidebar.overviewLabel` don't blow up; future major can drop it.
|
|
559
|
+
*
|
|
560
|
+
* Renamed only when the first link IS the group's index (matched via the
|
|
561
|
+
* `sortKeyByItem` WeakMap) — under structural separation that condition
|
|
562
|
+
* never holds, so this silently returns the input unchanged.
|
|
563
|
+
*/
|
|
564
|
+
function applyOverviewLabel(items, label) {
|
|
565
|
+
for (const item of items) if (item.type === "group") {
|
|
566
|
+
if (item._indexId) {
|
|
567
|
+
const firstLink = item.children.find((child) => child.type === "link");
|
|
568
|
+
if (firstLink && sortKeyByItem.get(firstLink) === item._indexId) firstLink.label = label;
|
|
569
|
+
}
|
|
570
|
+
applyOverviewLabel(item.children, label);
|
|
571
|
+
}
|
|
560
572
|
return items;
|
|
561
573
|
}
|
|
562
574
|
/**
|
|
563
|
-
* Process hideChildren:
|
|
564
|
-
* with a single link to the index page.
|
|
575
|
+
* Process `sidebar.hideChildren: true` on a group's index entry:
|
|
576
|
+
* replace the entire group with a single flat link to the index page.
|
|
577
|
+
*
|
|
578
|
+
* Under structural separation the group already exposes its landing
|
|
579
|
+
* page via `indexHref` and never adds the index as a child, so this
|
|
580
|
+
* function reads `indexHref` directly when collapsing — no need to
|
|
581
|
+
* search through `children` for an index link that isn't there.
|
|
565
582
|
*/
|
|
566
583
|
function processHideChildren(items, entries) {
|
|
567
584
|
const entryById = /* @__PURE__ */ new Map();
|
|
@@ -573,18 +590,25 @@ function processHideChildren(items, entries) {
|
|
|
573
590
|
result.push(item);
|
|
574
591
|
continue;
|
|
575
592
|
}
|
|
576
|
-
if (item._indexId) {
|
|
593
|
+
if (item._indexId && item.indexHref) {
|
|
577
594
|
if (entryById.get(item._indexId)?.data.sidebar?.hideChildren) {
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
595
|
+
const replacement = item.indexIsExternal ? {
|
|
596
|
+
type: "external",
|
|
597
|
+
label: item.label,
|
|
598
|
+
href: item.indexHref,
|
|
599
|
+
badge: item.badge,
|
|
600
|
+
order: item.order
|
|
601
|
+
} : {
|
|
602
|
+
type: "link",
|
|
603
|
+
label: item.label,
|
|
604
|
+
href: item.indexHref,
|
|
605
|
+
isCurrent: item.indexIsCurrent === true,
|
|
606
|
+
badge: item.badge,
|
|
607
|
+
order: item.order
|
|
608
|
+
};
|
|
609
|
+
sortKeyByItem.set(replacement, item._indexId);
|
|
610
|
+
result.push(replacement);
|
|
611
|
+
continue;
|
|
588
612
|
}
|
|
589
613
|
}
|
|
590
614
|
item.children = process(item.children);
|
|
@@ -618,11 +642,33 @@ function collectSidebarCollectionRefs(items) {
|
|
|
618
642
|
walk(items);
|
|
619
643
|
return [...found];
|
|
620
644
|
}
|
|
621
|
-
/**
|
|
645
|
+
/**
|
|
646
|
+
* Flatten sidebar tree into a list of links (for pagination).
|
|
647
|
+
*
|
|
648
|
+
* Groups with a landing page (`indexHref` set; structural-separation
|
|
649
|
+
* model) contribute a synthetic link at the group's position so that
|
|
650
|
+
* `getPrevNext` includes the directory-index page in the prev/next
|
|
651
|
+
* walk. Without this, navigating *to* or *from* a group's index page
|
|
652
|
+
* skips it entirely (e.g. on `/api/`, "prev" jumps over the section
|
|
653
|
+
* landing to the previous group's last child).
|
|
654
|
+
*
|
|
655
|
+
* External landing pages (`indexIsExternal`) are excluded — they're
|
|
656
|
+
* off-site destinations, not part of the in-site pagination ring.
|
|
657
|
+
*/
|
|
622
658
|
function flattenSidebar(items) {
|
|
623
659
|
const flat = [];
|
|
624
660
|
for (const item of items) if (item.type === "link") flat.push(item);
|
|
625
|
-
else if (item.type === "group")
|
|
661
|
+
else if (item.type === "group") {
|
|
662
|
+
if (item.indexHref && !item.indexIsExternal) flat.push({
|
|
663
|
+
type: "link",
|
|
664
|
+
label: item.label,
|
|
665
|
+
href: item.indexHref,
|
|
666
|
+
isCurrent: item.indexIsCurrent === true,
|
|
667
|
+
badge: item.badge,
|
|
668
|
+
order: item.order
|
|
669
|
+
});
|
|
670
|
+
flat.push(...flattenSidebar(item.children));
|
|
671
|
+
}
|
|
626
672
|
return flat;
|
|
627
673
|
}
|
|
628
674
|
function formatLabel(segment) {
|
|
@@ -969,6 +1015,131 @@ async function getLastUpdatedFromGit(filePath) {
|
|
|
969
1015
|
return result;
|
|
970
1016
|
}
|
|
971
1017
|
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/_internal/admonition-transform.ts
|
|
1020
|
+
/** Built-in MyST / Docusaurus / CF admonition types and their Aside mapping. */
|
|
1021
|
+
const BUILTIN_TYPES = {
|
|
1022
|
+
note: "note",
|
|
1023
|
+
info: "note",
|
|
1024
|
+
tip: "tip",
|
|
1025
|
+
caution: "caution",
|
|
1026
|
+
warning: "caution",
|
|
1027
|
+
important: "caution",
|
|
1028
|
+
danger: "danger"
|
|
1029
|
+
};
|
|
1030
|
+
/**
|
|
1031
|
+
* Transform a single MDX source string. Idempotent — running the
|
|
1032
|
+
* transform twice produces the same output as running it once.
|
|
1033
|
+
*/
|
|
1034
|
+
function transformAdmonitions(source, options = {}) {
|
|
1035
|
+
const typeMap = {
|
|
1036
|
+
...BUILTIN_TYPES,
|
|
1037
|
+
...options.typeAliases ?? {}
|
|
1038
|
+
};
|
|
1039
|
+
const { frontmatter, body, bodyOffset: _ } = splitFrontmatter(source);
|
|
1040
|
+
const { stashed, restore } = stashCodeBlocks(body);
|
|
1041
|
+
return frontmatter + restore(stashed.replace(ADMONITION_PATTERN, (match, rawType, rawTitle, rawContent) => {
|
|
1042
|
+
const aside = typeMap[String(rawType).toLowerCase()];
|
|
1043
|
+
if (!aside) return match;
|
|
1044
|
+
const title = typeof rawTitle === "string" ? rawTitle.trim() : "";
|
|
1045
|
+
const content = String(rawContent).trim();
|
|
1046
|
+
return `\n\n<Aside type="${aside}"${title ? ` title=${JSON.stringify(title)}` : ""}>\n\n${content}\n\n</Aside>\n\n`;
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Match `:::type[optional title] body :::` with non-greedy body.
|
|
1051
|
+
*
|
|
1052
|
+
* Components:
|
|
1053
|
+
* - `:::` literal opener
|
|
1054
|
+
* - `([a-zA-Z]+)` type token (captured, case-insensitive lookup at use site)
|
|
1055
|
+
* - `(?:\[(...)\])?` optional bracketed title; brackets stripped from capture
|
|
1056
|
+
* - `\s+|\n` at least one whitespace before content (avoids matching
|
|
1057
|
+
* `:::foo:::` directly)
|
|
1058
|
+
* - `([\s\S]*?)` non-greedy body, may span newlines
|
|
1059
|
+
* - `\n?\s*:::` closer, possibly with leading whitespace
|
|
1060
|
+
*
|
|
1061
|
+
* Non-greedy body + global flag means adjacent admonitions don't merge
|
|
1062
|
+
* (the engine finds the *nearest* `:::` closer for each opener).
|
|
1063
|
+
*/
|
|
1064
|
+
const ADMONITION_PATTERN = /:::([a-zA-Z]+)(?:\[([^\]]*)\])?[ \t]*(?:\n|[ \t]+)([\s\S]*?)\n?[ \t]*:::/g;
|
|
1065
|
+
function splitFrontmatter(source) {
|
|
1066
|
+
const match = source.match(/^---\n[\s\S]*?\n---\n?/);
|
|
1067
|
+
if (!match) return {
|
|
1068
|
+
frontmatter: "",
|
|
1069
|
+
body: source,
|
|
1070
|
+
bodyOffset: 0
|
|
1071
|
+
};
|
|
1072
|
+
return {
|
|
1073
|
+
frontmatter: match[0],
|
|
1074
|
+
body: source.slice(match[0].length),
|
|
1075
|
+
bodyOffset: match[0].length
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Replace fenced code blocks with opaque placeholders so the admonition
|
|
1080
|
+
* regex doesn't reach `:::` tokens inside code samples. The `restore()`
|
|
1081
|
+
* function reinstates the originals after the transform.
|
|
1082
|
+
*
|
|
1083
|
+
* Order matters: stash the longest fence flavors first (``` and ~~~)
|
|
1084
|
+
* so the placeholders themselves don't get re-stashed. Inline backtick
|
|
1085
|
+
* code spans are NOT stashed — a `:::` inside a single-line `code` span
|
|
1086
|
+
* is rare and would have to be on the same line as both fences anyway.
|
|
1087
|
+
*/
|
|
1088
|
+
function stashCodeBlocks(body) {
|
|
1089
|
+
const blocks = [];
|
|
1090
|
+
const PLACEHOLDER = "\0NIMBUS_CODEBLOCK_";
|
|
1091
|
+
const PLACEHOLDER_END = "\0";
|
|
1092
|
+
const stashed = body.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, (match) => {
|
|
1093
|
+
const index = blocks.length;
|
|
1094
|
+
blocks.push(match);
|
|
1095
|
+
return `${PLACEHOLDER}${index}${PLACEHOLDER_END}`;
|
|
1096
|
+
});
|
|
1097
|
+
function restore(src) {
|
|
1098
|
+
return src.replace(new RegExp(`${PLACEHOLDER}(\\d+)${PLACEHOLDER_END}`, "g"), (_match, index) => blocks[Number(index)] ?? "");
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
stashed,
|
|
1102
|
+
restore
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region src/_internal/admonition-vite-plugin.ts
|
|
1108
|
+
/**
|
|
1109
|
+
* Vite plugin: intercept `.mdx` (and `.md`) loads under the project's
|
|
1110
|
+
* content directories, rewrite `:::admonition` directives to `<Aside>`
|
|
1111
|
+
* components, hand the transformed source to the next loader in the
|
|
1112
|
+
* chain. Sits in front of @astrojs/mdx and Sätteri.
|
|
1113
|
+
*
|
|
1114
|
+
* `enforce: "pre"` is load-bearing — the MDX integration's own transform
|
|
1115
|
+
* registers without an `enforce` and runs in the default mid-pipeline
|
|
1116
|
+
* slot. Pre-stage runs before that, so by the time MDX parses the file,
|
|
1117
|
+
* the directive syntax has already been rewritten to JSX.
|
|
1118
|
+
*
|
|
1119
|
+
* Scope is restricted to the project's content directories so we don't
|
|
1120
|
+
* touch unrelated `.md` files in `node_modules/` or vendored MDX.
|
|
1121
|
+
*/
|
|
1122
|
+
function admonitionPlugin(options) {
|
|
1123
|
+
const normalizedDirs = options.contentDirs.map((d) => path.resolve(d));
|
|
1124
|
+
return {
|
|
1125
|
+
name: "nimbus-docs:admonitions",
|
|
1126
|
+
enforce: "pre",
|
|
1127
|
+
transform(code, id) {
|
|
1128
|
+
const [pathOnly] = id.split("?", 1);
|
|
1129
|
+
if (!pathOnly) return null;
|
|
1130
|
+
if (!pathOnly.endsWith(".mdx") && !pathOnly.endsWith(".md")) return null;
|
|
1131
|
+
const absolute = path.resolve(pathOnly);
|
|
1132
|
+
if (!normalizedDirs.some((dir) => absolute === dir || absolute.startsWith(dir + path.sep))) return null;
|
|
1133
|
+
if (options.skip?.(absolute)) return null;
|
|
1134
|
+
if (!code.includes(":::")) return null;
|
|
1135
|
+
return {
|
|
1136
|
+
code: transformAdmonitions(code, { typeAliases: options.typeAliases }),
|
|
1137
|
+
map: null
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
972
1143
|
//#endregion
|
|
973
1144
|
//#region src/_internal/parse-components-registry.ts
|
|
974
1145
|
/**
|
|
@@ -1764,7 +1935,7 @@ function parseImports(body) {
|
|
|
1764
1935
|
* inside code samples doesn't trip the validator.
|
|
1765
1936
|
*/
|
|
1766
1937
|
function stripCodeBlocks(body) {
|
|
1767
|
-
return body.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length)).replace(/~~~[\s\S]*?~~~/g, (m) => " ".repeat(m.length)).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length));
|
|
1938
|
+
return body.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length)).replace(/~~~[\s\S]*?~~~/g, (m) => " ".repeat(m.length)).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length)).replace(/=\s*"[^"\n]*"/g, (m) => "=" + " ".repeat(m.length - 1)).replace(/=\s*'[^'\n]*'/g, (m) => "=" + " ".repeat(m.length - 1));
|
|
1768
1939
|
}
|
|
1769
1940
|
/**
|
|
1770
1941
|
* Find PascalCase JSX-like tags. Matches `<Capital...` at the start of
|
|
@@ -1811,8 +1982,11 @@ const headElementSchema = z.object({
|
|
|
1811
1982
|
"meta",
|
|
1812
1983
|
"link",
|
|
1813
1984
|
"script",
|
|
1814
|
-
"style"
|
|
1815
|
-
|
|
1985
|
+
"style",
|
|
1986
|
+
"title",
|
|
1987
|
+
"noscript",
|
|
1988
|
+
"base"
|
|
1989
|
+
], { error: "head element \"tag\" must be one of: meta, link, script, style, title, noscript, base" }),
|
|
1816
1990
|
attrs: z.record(z.string(), z.string()).default({}),
|
|
1817
1991
|
content: z.string().optional()
|
|
1818
1992
|
});
|
|
@@ -2409,22 +2583,38 @@ function nimbus(rawConfig, options = {}) {
|
|
|
2409
2583
|
}
|
|
2410
2584
|
integrationsToAdd.push(mdx(options.mdx ?? {}));
|
|
2411
2585
|
if (options.sitemap !== false && Boolean(config.site)) integrationsToAdd.push(sitemap());
|
|
2586
|
+
const admonitionVitePlugins = [];
|
|
2587
|
+
if (options.admonitions !== false) {
|
|
2588
|
+
const admoOpts = typeof options.admonitions === "object" ? options.admonitions : {};
|
|
2589
|
+
const projectRoot = fileURLToPath(astroConfig.root);
|
|
2590
|
+
const contentDirs = (admoOpts.contentDirs ?? ["src/content"]).map((d) => path.isAbsolute(d) ? d : path.join(projectRoot, d));
|
|
2591
|
+
admonitionVitePlugins.push(admonitionPlugin({
|
|
2592
|
+
contentDirs,
|
|
2593
|
+
typeAliases: admoOpts.typeAliases,
|
|
2594
|
+
skip: admoOpts.skip
|
|
2595
|
+
}));
|
|
2596
|
+
}
|
|
2412
2597
|
updateConfig({
|
|
2413
2598
|
...config.site ? { site: config.site } : {},
|
|
2414
2599
|
integrations: integrationsToAdd,
|
|
2415
2600
|
markdown: {
|
|
2416
|
-
processor: satteri(),
|
|
2601
|
+
processor: options.markdown?.processor ?? satteri(),
|
|
2417
2602
|
shikiConfig: {
|
|
2418
2603
|
themes: {
|
|
2419
2604
|
light: "github-light",
|
|
2420
2605
|
dark: "github-dark"
|
|
2421
2606
|
},
|
|
2422
2607
|
defaultColor: false,
|
|
2423
|
-
transformers: defaultCodeTransformers()
|
|
2608
|
+
transformers: defaultCodeTransformers(),
|
|
2609
|
+
langAlias: {
|
|
2610
|
+
curl: "bash",
|
|
2611
|
+
console: "bash",
|
|
2612
|
+
shellsession: "shellscript"
|
|
2613
|
+
}
|
|
2424
2614
|
}
|
|
2425
2615
|
},
|
|
2426
2616
|
...versionRedirects.length > 0 ? { redirects: Object.fromEntries(versionRedirects.map(({ from, to }) => [from, to])) } : {},
|
|
2427
|
-
vite: { plugins: [virtualConfigPlugin(config, {
|
|
2617
|
+
vite: { plugins: [...admonitionVitePlugins, virtualConfigPlugin(config, {
|
|
2428
2618
|
indexedCollections,
|
|
2429
2619
|
versionAlternates
|
|
2430
2620
|
})] }
|