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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { o as validateLintOptions, r as suggest, t as IMPLEMENTED_CODES } from "./rules-DnAP-j89.js";
2
- import { t as withStrictKeys } from "./strict-keys-BiXiT3pq.js";
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 href = joinHref(hrefPrefix, entry.id);
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: entry.data.sidebar?.label ?? entry.data.title,
318
- href,
319
- isCurrent: toRouteKey(currentPath) === toRouteKey(href),
237
+ label,
238
+ href: internalHref,
239
+ isCurrent: toRouteKey(currentPath) === toRouteKey(internalHref),
320
240
  badge,
321
- order: entry.data.sidebar?.order ?? Number.MAX_VALUE
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 groupLabel = indexEntry?.data.sidebar?.label ?? formatLabel(dirSegment);
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 children = [];
390
- if (indexEntry) children.push(createLink(indexEntry, currentPath, hrefPrefix));
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: indexEntry?.data.sidebar?.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)) return item.children;
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. Used by
512
- * `Header.astro` to render the section tab strip. Caller must pass the
513
- * *un-scoped* tree (the result of `buildSidebarTree`, not `getSidebar`);
514
- * otherwise only the current section's children are visible and the
515
- * derivation collapses to a single item.
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
- const links = flattenLinks(item.children);
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 ?? links[0].href),
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: replace groups whose index has hideChildren=true
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 indexKey = toRouteKey(canonicalEntryUrl("", item._indexId));
579
- const indexLink = item.children.find((c) => c.type === "link" && toRouteKey(c.href) === indexKey);
580
- if (indexLink) {
581
- const link = {
582
- ...indexLink,
583
- label: item.label
584
- };
585
- result.push(link);
586
- continue;
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
- /** Flatten sidebar tree into a list of links (for pagination) */
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") flat.push(...flattenSidebar(item.children));
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
  })] }