nimbus-docs 0.1.10 → 0.1.12
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/cli/index.js +26 -17
- package/dist/cli/index.js.map +1 -1
- package/dist/content.d.ts +7 -7
- package/dist/{diagnostic-ewiZxpSO.d.ts → diagnostic-CnxJwVpT.d.ts} +5 -6
- package/dist/diagnostic-CnxJwVpT.d.ts.map +1 -0
- package/dist/index.d.ts +122 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +487 -132
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +7 -7
- package/dist/react.js +6 -6
- package/dist/react.js.map +1 -1
- package/dist/{rules-DDDvKkyJ.js → rules-CzB-afEb.js} +7 -11
- package/dist/rules-CzB-afEb.js.map +1 -0
- package/dist/schemas.d.ts +6 -6
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +10 -22
- package/dist/schemas.js.map +1 -1
- package/dist/types.d.ts +71 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/components/NimbusHead.astro +1 -1
- package/dist/diagnostic-ewiZxpSO.d.ts.map +0 -1
- package/dist/rules-DDDvKkyJ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as validateLintOptions, r as suggest, t as IMPLEMENTED_CODES } from "./rules-
|
|
1
|
+
import { o as validateLintOptions, r as suggest, t as IMPLEMENTED_CODES } from "./rules-CzB-afEb.js";
|
|
2
2
|
import { i as toRouteKey, n as isAbsoluteUrl, r as toBrowserHref, t as withStrictKeys } from "./strict-keys-fbKKxxKL.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
@@ -11,7 +11,7 @@ import { satteri } from "@astrojs/markdown-satteri";
|
|
|
11
11
|
import sitemap from "@astrojs/sitemap";
|
|
12
12
|
import fs$1, { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
13
13
|
import { z } from "astro/zod";
|
|
14
|
-
import {
|
|
14
|
+
import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight } from "@shikijs/transformers";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
|
|
17
17
|
//#region src/_internal/runtime-config.ts
|
|
@@ -118,28 +118,16 @@ function slug(value, maintainCase) {
|
|
|
118
118
|
* Mirror of Astro's content-layer slug normalization, used by every
|
|
119
119
|
* framework URL builder that derives a URL from an `entry.id`.
|
|
120
120
|
*
|
|
121
|
-
* Astro's `glob` content loader
|
|
122
|
-
* `partialsCollection` factories) runs each path segment through
|
|
121
|
+
* Astro's `glob` content loader runs each path segment through
|
|
123
122
|
* `github-slugger.slug()` and strips a trailing `/index`. That output is
|
|
124
|
-
* what `entry.id` becomes
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* clean way to read Astro's resolved routes. The honest architectural fix
|
|
133
|
-
* is to refactor those builders to consume the route manifest at
|
|
134
|
-
* `astro:routes:resolved` (sitemap/llms) or via a build-emitted lookup
|
|
135
|
-
* table (sidebar). Until that work lands, this helper keeps the URLs
|
|
136
|
-
* correct.
|
|
137
|
-
*
|
|
138
|
-
* Mirror caveat: this matches `github-slugger.slug()` and the trailing-
|
|
139
|
-
* /index strip — the documented public-library behaviors Astro inherits
|
|
140
|
-
* — not Astro's private routing internals. If a user supplies a custom
|
|
141
|
-
* `generateId` to the content loader, or a `data.slug` override in
|
|
142
|
-
* frontmatter, this helper doesn't see it. Both are uncommon.
|
|
123
|
+
* what `entry.id` becomes and what `params.slug` substitutes into
|
|
124
|
+
* `[...slug].astro` routes, so anything building a URL from the raw
|
|
125
|
+
* filesystem path must apply the same normalization to match what Astro
|
|
126
|
+
* serves.
|
|
127
|
+
*
|
|
128
|
+
* Caveat: this matches `github-slugger.slug()` and the trailing-/index
|
|
129
|
+
* strip, not a custom loader `generateId` or a `data.slug` frontmatter
|
|
130
|
+
* override — neither of which this helper sees.
|
|
143
131
|
*/
|
|
144
132
|
/** Canonicalize one entry id the way Astro's content layer does. */
|
|
145
133
|
function canonicalSlug(entryId) {
|
|
@@ -431,6 +419,13 @@ function resolveConfigItems(configItems, entriesByCollection, primaryCollection,
|
|
|
431
419
|
badge: item.badge,
|
|
432
420
|
children
|
|
433
421
|
};
|
|
422
|
+
const landing = item.landing;
|
|
423
|
+
const segment = item.segment;
|
|
424
|
+
if (segment !== void 0) group.segment = segment;
|
|
425
|
+
if (landing !== void 0) {
|
|
426
|
+
group.indexHref = toBrowserHref(landing);
|
|
427
|
+
group.indexIsCurrent = toRouteKey(currentPath) === toRouteKey(landing) || void 0;
|
|
428
|
+
}
|
|
434
429
|
result.push(group);
|
|
435
430
|
}
|
|
436
431
|
}
|
|
@@ -471,6 +466,70 @@ function scopeToCurrentSection(items, currentPath) {
|
|
|
471
466
|
}
|
|
472
467
|
return items;
|
|
473
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Return the ancestor chain from the top of the tree to the node that owns
|
|
471
|
+
* `currentPath` (inclusive). Matches by route key, so a chain can be
|
|
472
|
+
* resolved for any path (e.g. a catalog route's section) regardless of the
|
|
473
|
+
* page the tree was built for. A group that only *contains* the match is
|
|
474
|
+
* included with no href (a non-interactive crumb). Returns `[]` on no match.
|
|
475
|
+
*/
|
|
476
|
+
function findActivePath(items, currentPath) {
|
|
477
|
+
const key = toRouteKey(currentPath);
|
|
478
|
+
function search(nodes) {
|
|
479
|
+
for (const item of nodes) if (item.type === "link") {
|
|
480
|
+
if (toRouteKey(item.href) === key) return [item];
|
|
481
|
+
} else if (item.type === "group") {
|
|
482
|
+
if (item.indexHref && !item.indexIsExternal && toRouteKey(item.indexHref) === key) return [item];
|
|
483
|
+
const childPath = search(item.children);
|
|
484
|
+
if (childPath) return [item, ...childPath];
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
return search(items) ?? [];
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Descend a section-scoped tree to the sub-tree under the current path's
|
|
492
|
+
* boundary. A glob like `"guides/*"` (`*` = one segment) sets the prefix
|
|
493
|
+
* depth; the rail is replaced by the children of the shallowest group
|
|
494
|
+
* fully contained under that prefix. Matching is by descendant href, so it
|
|
495
|
+
* works for index-less section folders. Returns the input unchanged on no
|
|
496
|
+
* match.
|
|
497
|
+
*/
|
|
498
|
+
function isolateToBoundary(items, currentPath, boundaries) {
|
|
499
|
+
const segs = toRouteKey(currentPath).split("/").filter(Boolean);
|
|
500
|
+
for (const glob of boundaries) {
|
|
501
|
+
const globSegs = glob.split("/").filter(Boolean);
|
|
502
|
+
if (segs.length < globSegs.length || globSegs.length === 0) continue;
|
|
503
|
+
if (!globSegs.every((g, i) => g === "*" || g === segs[i])) continue;
|
|
504
|
+
const group = findBoundaryGroup(items, toBrowserHref("/" + segs.slice(0, globSegs.length).join("/")));
|
|
505
|
+
if (group) return group.children;
|
|
506
|
+
}
|
|
507
|
+
return items;
|
|
508
|
+
}
|
|
509
|
+
/** Shallowest group whose every flattened descendant href is under `prefix`. */
|
|
510
|
+
function findBoundaryGroup(items, prefix) {
|
|
511
|
+
for (const item of items) {
|
|
512
|
+
if (item.type !== "group") continue;
|
|
513
|
+
const flat = flattenSidebar([item]);
|
|
514
|
+
if (flat.length > 0 && flat.every((l) => l.href.startsWith(prefix))) return item;
|
|
515
|
+
const nested = findBoundaryGroup(item.children, prefix);
|
|
516
|
+
if (nested) return nested;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Context for the `getSidebar` transform: `sectionSlug` (seg0), `module`
|
|
521
|
+
* (seg1), and `indexEntryId` — the landing entry id of the first group on
|
|
522
|
+
* the active path, or `undefined` for an index-less section.
|
|
523
|
+
*/
|
|
524
|
+
function deriveTransformCtx(fullTree, currentSlug) {
|
|
525
|
+
const segs = currentSlug.split("/").filter(Boolean);
|
|
526
|
+
const sectionGroup = findActivePath(fullTree, currentSlug).find((n) => n.type === "group");
|
|
527
|
+
return {
|
|
528
|
+
sectionSlug: segs[0] ?? "",
|
|
529
|
+
module: segs[1],
|
|
530
|
+
indexEntryId: sectionGroup?._indexId
|
|
531
|
+
};
|
|
532
|
+
}
|
|
474
533
|
function hasActivePage(item, currentPath) {
|
|
475
534
|
if (item.type === "link") return item.isCurrent === true;
|
|
476
535
|
if (item.type === "external") return false;
|
|
@@ -489,12 +548,9 @@ function hasActivePage(item, currentPath) {
|
|
|
489
548
|
* URL prefix* — e.g. `Components` mounted at `/components/` —
|
|
490
549
|
* rather than a sub-directory of the primary docs collection.
|
|
491
550
|
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
* header rail is meant for "other collections" navigation, not for
|
|
496
|
-
* sub-sections of the default collection — those belong in the
|
|
497
|
-
* sidebar's own collapsible tree.
|
|
551
|
+
* Sub-directories of the primary collection (`wip/`, `lab/`, etc.) are
|
|
552
|
+
* deliberately excluded — the header rail is for cross-collection
|
|
553
|
+
* navigation; sub-sections belong in the sidebar's own tree.
|
|
498
554
|
*
|
|
499
555
|
* Caller must pass the *un-scoped* tree (the result of
|
|
500
556
|
* `buildSidebarTree`, not `getSidebar`); otherwise only the current
|
|
@@ -557,16 +613,13 @@ function applyDefaultCollapsed(items) {
|
|
|
557
613
|
}
|
|
558
614
|
}
|
|
559
615
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
* Renamed only when the first link IS the group's index (matched via the
|
|
568
|
-
* `sortKeyByItem` WeakMap) — under structural separation that condition
|
|
569
|
-
* never holds, so this silently returns the input unchanged.
|
|
616
|
+
* Relabel a section's landing link to the `overviewLabel` string (default
|
|
617
|
+
* "Overview"). Applies to a `directory:` autogenerate's leading landing
|
|
618
|
+
* link wherever it surfaces (tracked via `directoryIndexLinks`), and to a
|
|
619
|
+
* group whose first child link IS the group's own index (matched via the
|
|
620
|
+
* `sortKeyByItem` WeakMap). Config groups expose their index as the group
|
|
621
|
+
* label itself (`SidebarGroupItem.indexHref`), so those aren't relabelled
|
|
622
|
+
* here — there's no separate child link to rename.
|
|
570
623
|
*/
|
|
571
624
|
function applyOverviewLabel(items, label) {
|
|
572
625
|
for (const item of items) if (item.type === "link" && directoryIndexLinks.has(item)) item.label = label;
|
|
@@ -599,7 +652,8 @@ function processHideChildren(items, entries) {
|
|
|
599
652
|
continue;
|
|
600
653
|
}
|
|
601
654
|
if (item._indexId && item.indexHref) {
|
|
602
|
-
|
|
655
|
+
const entry = entryById.get(item._indexId);
|
|
656
|
+
if (entry?.data.sidebar?.hideChildren || entry?.data.hideChildren) {
|
|
603
657
|
const replacement = item.indexIsExternal ? {
|
|
604
658
|
type: "external",
|
|
605
659
|
label: item.label,
|
|
@@ -889,7 +943,68 @@ function renderEntryAsMarkdown(entry, options = {}) {
|
|
|
889
943
|
|
|
890
944
|
//#endregion
|
|
891
945
|
//#region src/_internal/navigation.ts
|
|
892
|
-
|
|
946
|
+
/**
|
|
947
|
+
* The in-site href a node links to, or `undefined` for a non-interactive
|
|
948
|
+
* crumb (index-less groups and off-site landings).
|
|
949
|
+
*/
|
|
950
|
+
function nodeHref(node) {
|
|
951
|
+
if (node.type === "link") return node.href;
|
|
952
|
+
if (node.type === "external") return void 0;
|
|
953
|
+
return node.indexIsExternal ? void 0 : node.indexHref;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Build the trail from the root crumb and per-node labels. `labels[i]`
|
|
957
|
+
* pairs with `path[i]`: `null` drops the crumb, `undefined` keeps the
|
|
958
|
+
* node label. Deduplicated by href (first wins); hrefless crumbs are
|
|
959
|
+
* never merged.
|
|
960
|
+
*/
|
|
961
|
+
function assembleBreadcrumbs(root, path, labels) {
|
|
962
|
+
const crumbs = [{
|
|
963
|
+
label: root.label,
|
|
964
|
+
href: root.href
|
|
965
|
+
}];
|
|
966
|
+
path.forEach((node, i) => {
|
|
967
|
+
const override = labels[i];
|
|
968
|
+
if (override === null) return;
|
|
969
|
+
const label = override ?? node.label;
|
|
970
|
+
const href = nodeHref(node);
|
|
971
|
+
crumbs.push(href ? {
|
|
972
|
+
label,
|
|
973
|
+
href
|
|
974
|
+
} : { label });
|
|
975
|
+
});
|
|
976
|
+
const seen = /* @__PURE__ */ new Set();
|
|
977
|
+
return crumbs.filter((c) => {
|
|
978
|
+
if (c.href === void 0) return true;
|
|
979
|
+
if (seen.has(c.href)) return false;
|
|
980
|
+
seen.add(c.href);
|
|
981
|
+
return true;
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Append `trail` items to a section's ancestry crumbs (a leaf with no
|
|
986
|
+
* `href` is the current page) and deduplicate by href, so a trail crumb
|
|
987
|
+
* that repeats an ancestry crumb's URL collapses to one (first wins). The
|
|
988
|
+
* pure core of `getRouteNavigation`.
|
|
989
|
+
*/
|
|
990
|
+
function composeRouteBreadcrumbs(sectionCrumbs, trail) {
|
|
991
|
+
const combined = [...sectionCrumbs, ...trail.map((t) => t.href ? {
|
|
992
|
+
label: t.label,
|
|
993
|
+
href: t.href
|
|
994
|
+
} : { label: t.label })];
|
|
995
|
+
const seen = /* @__PURE__ */ new Set();
|
|
996
|
+
return combined.filter((c) => {
|
|
997
|
+
if (c.href === void 0) return true;
|
|
998
|
+
if (seen.has(c.href)) return false;
|
|
999
|
+
seen.add(c.href);
|
|
1000
|
+
return true;
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* URL-segment fallback for pages with no node in the tree, so a stray
|
|
1005
|
+
* page still gets a root-anchored trail.
|
|
1006
|
+
*/
|
|
1007
|
+
function breadcrumbsFromUrl(slug, homeLabel = "Home") {
|
|
893
1008
|
const parts = slug.split("/").filter(Boolean);
|
|
894
1009
|
const crumbs = [{
|
|
895
1010
|
label: homeLabel,
|
|
@@ -1025,7 +1140,7 @@ async function getLastUpdatedFromGit(filePath) {
|
|
|
1025
1140
|
|
|
1026
1141
|
//#endregion
|
|
1027
1142
|
//#region src/_internal/admonition-transform.ts
|
|
1028
|
-
/** Built-in MyST / Docusaurus
|
|
1143
|
+
/** Built-in MyST / Docusaurus admonition types and their Aside mapping. */
|
|
1029
1144
|
const BUILTIN_TYPES = {
|
|
1030
1145
|
note: "note",
|
|
1031
1146
|
info: "note",
|
|
@@ -1046,30 +1161,48 @@ function transformAdmonitions(source, options = {}) {
|
|
|
1046
1161
|
};
|
|
1047
1162
|
const { frontmatter, body, bodyOffset: _ } = splitFrontmatter(source);
|
|
1048
1163
|
const { stashed, restore } = stashCodeBlocks(body);
|
|
1049
|
-
return frontmatter + restore(stashed.replace(ADMONITION_PATTERN, (match, rawType, rawTitle, rawContent) => {
|
|
1164
|
+
return frontmatter + restore(stashed.replace(ADMONITION_PATTERN, (match, rawIndent, rawType, rawTitle, rawContent) => {
|
|
1050
1165
|
const aside = typeMap[String(rawType).toLowerCase()];
|
|
1051
1166
|
if (!aside) return match;
|
|
1167
|
+
const indent = typeof rawIndent === "string" ? rawIndent : "";
|
|
1052
1168
|
const title = typeof rawTitle === "string" ? rawTitle.trim() : "";
|
|
1053
|
-
|
|
1054
|
-
return `\n\n<Aside type="${aside}"${title ? ` title=${JSON.stringify(title)}` : ""}>\n\n${content}\n\n</Aside>\n\n`;
|
|
1169
|
+
return `\n\n${indent}<Aside type="${aside}"${title ? ` title=${JSON.stringify(title)}` : ""}>\n\n${reindentBody(String(rawContent), indent)}\n\n${indent}</Aside>\n\n`;
|
|
1055
1170
|
}));
|
|
1056
1171
|
}
|
|
1057
1172
|
/**
|
|
1058
1173
|
* Match `:::type[optional title] body :::` with non-greedy body.
|
|
1059
1174
|
*
|
|
1060
1175
|
* Components:
|
|
1176
|
+
* - `^([ \t]*)` leading indentation of the opener line (captured) so the
|
|
1177
|
+
* emitted `<Aside>` can be re-indented to the same depth —
|
|
1178
|
+
* load-bearing for directives nested inside indented JSX
|
|
1179
|
+
* (e.g. `:::note` inside a `<TabItem>`). The `m` flag makes
|
|
1180
|
+
* `^`/`$` match line boundaries. Line-anchoring also stops
|
|
1181
|
+
* a stray mid-line `:::` from being treated as an opener.
|
|
1061
1182
|
* - `:::` literal opener
|
|
1062
1183
|
* - `([a-zA-Z]+)` type token (captured, case-insensitive lookup at use site)
|
|
1063
1184
|
* - `(?:\[(...)\])?` optional bracketed title; brackets stripped from capture
|
|
1064
|
-
* - `\
|
|
1185
|
+
* - `\n|[ \t]+` at least one whitespace before content (avoids matching
|
|
1065
1186
|
* `:::foo:::` directly)
|
|
1066
1187
|
* - `([\s\S]*?)` non-greedy body, may span newlines
|
|
1067
|
-
* - `\n
|
|
1188
|
+
* - `\n?[ \t]*:::[ \t]*$` closer, possibly indented, at end of its line
|
|
1068
1189
|
*
|
|
1069
1190
|
* Non-greedy body + global flag means adjacent admonitions don't merge
|
|
1070
1191
|
* (the engine finds the *nearest* `:::` closer for each opener).
|
|
1071
1192
|
*/
|
|
1072
|
-
const ADMONITION_PATTERN =
|
|
1193
|
+
const ADMONITION_PATTERN = /^([ \t]*):::([a-zA-Z]+)(?:\[([^\]]*)\])?[ \t]*(?:\n|[ \t]+)([\s\S]*?)\n?[ \t]*:::[ \t]*$/gm;
|
|
1194
|
+
/**
|
|
1195
|
+
* Dedent the captured admonition body to a common baseline (preserving
|
|
1196
|
+
* relative structure like nested lists / JSX), then re-prefix every
|
|
1197
|
+
* non-blank line with the directive's own indentation so the emitted
|
|
1198
|
+
* `<Aside>…</Aside>` block sits at the same depth as the directive.
|
|
1199
|
+
*/
|
|
1200
|
+
function reindentBody(content, indent) {
|
|
1201
|
+
const lines = content.replace(/^\n+/, "").replace(/\n+$/, "").split("\n");
|
|
1202
|
+
const widths = lines.filter((l) => l.trim() !== "").map((l) => l.match(/^[ \t]*/)?.[0].length ?? 0);
|
|
1203
|
+
const common = widths.length ? Math.min(...widths) : 0;
|
|
1204
|
+
return lines.map((l) => l.trim() === "" ? "" : indent + l.slice(common)).join("\n");
|
|
1205
|
+
}
|
|
1073
1206
|
function splitFrontmatter(source) {
|
|
1074
1207
|
const match = source.match(/^---\n[\s\S]*?\n---\n?/);
|
|
1075
1208
|
if (!match) return {
|
|
@@ -1729,6 +1862,118 @@ function parseTitle(meta) {
|
|
|
1729
1862
|
return (meta.match(/\btitle="([^"]+)"/) ?? meta.match(/\btitle='([^']+)'/))?.[1];
|
|
1730
1863
|
}
|
|
1731
1864
|
/**
|
|
1865
|
+
* Expand a (possibly space-padded) line-range spec like `5-16, 21-40` or
|
|
1866
|
+
* `1,3-5` into an explicit list of 1-based line numbers. Tolerates spaces
|
|
1867
|
+
* anywhere — `{5-16, 21-40}` expands to the same set as `{5-16,21-40}`.
|
|
1868
|
+
*/
|
|
1869
|
+
function expandRanges(spec) {
|
|
1870
|
+
const out = [];
|
|
1871
|
+
for (const partRaw of spec.split(",")) {
|
|
1872
|
+
const part = partRaw.trim();
|
|
1873
|
+
if (!part) continue;
|
|
1874
|
+
const range = part.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
1875
|
+
if (range) {
|
|
1876
|
+
const a = Number.parseInt(range[1], 10);
|
|
1877
|
+
const b = Number.parseInt(range[2], 10);
|
|
1878
|
+
const lo = Math.min(a, b);
|
|
1879
|
+
const hi = Math.max(a, b);
|
|
1880
|
+
for (let i = lo; i <= hi; i++) out.push(i);
|
|
1881
|
+
} else if (/^\d+$/.test(part)) out.push(Number.parseInt(part, 10));
|
|
1882
|
+
}
|
|
1883
|
+
return out;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Parse an EC-style fence meta string into {@link NimbusMeta}.
|
|
1887
|
+
*
|
|
1888
|
+
* Order matters — each step consumes (blanks out) what it matched so later,
|
|
1889
|
+
* looser patterns can't re-grab it:
|
|
1890
|
+
* 1. `frame="…"` (quoted)
|
|
1891
|
+
* 2. `title="…"` (quoted; value resolved separately)
|
|
1892
|
+
* 3. `ins="…"` / `del="…"` (quoted tokens)
|
|
1893
|
+
* 4. `ins={…}` / `del={…}` / `collapse={…}` (brace ranges)
|
|
1894
|
+
* 5. standalone `"…"`/`'…'` (search words — what's left of the quotes)
|
|
1895
|
+
* 6. `wrap` (bare keyword)
|
|
1896
|
+
* 7. bare `{…}` (plain line-highlight — only what survives)
|
|
1897
|
+
*/
|
|
1898
|
+
function parseNimbusMeta(raw) {
|
|
1899
|
+
const meta = {
|
|
1900
|
+
highlightLines: /* @__PURE__ */ new Set(),
|
|
1901
|
+
insLines: /* @__PURE__ */ new Set(),
|
|
1902
|
+
delLines: /* @__PURE__ */ new Set(),
|
|
1903
|
+
insTokens: [],
|
|
1904
|
+
delTokens: [],
|
|
1905
|
+
searchWords: [],
|
|
1906
|
+
collapseLines: /* @__PURE__ */ new Set(),
|
|
1907
|
+
wrap: false,
|
|
1908
|
+
frame: void 0
|
|
1909
|
+
};
|
|
1910
|
+
if (!raw) return meta;
|
|
1911
|
+
let s = raw;
|
|
1912
|
+
s = s.replace(/\bframe=(?:"([^"]*)"|'([^']*)')/g, (_m, d, sg) => {
|
|
1913
|
+
const v = d ?? sg;
|
|
1914
|
+
if (v) meta.frame = v;
|
|
1915
|
+
return " ";
|
|
1916
|
+
});
|
|
1917
|
+
s = s.replace(/\btitle=(?:"([^"]*)"|'([^']*)')/g, () => " ");
|
|
1918
|
+
s = s.replace(/\bins=(?:"([^"]*)"|'([^']*)')/g, (_m, d, sg) => {
|
|
1919
|
+
const v = d ?? sg;
|
|
1920
|
+
if (v) meta.insTokens.push(v);
|
|
1921
|
+
return " ";
|
|
1922
|
+
});
|
|
1923
|
+
s = s.replace(/\bdel=(?:"([^"]*)"|'([^']*)')/g, (_m, d, sg) => {
|
|
1924
|
+
const v = d ?? sg;
|
|
1925
|
+
if (v) meta.delTokens.push(v);
|
|
1926
|
+
return " ";
|
|
1927
|
+
});
|
|
1928
|
+
s = s.replace(/\bins=\{([^}]*)\}/g, (_m, spec) => {
|
|
1929
|
+
for (const n of expandRanges(spec)) meta.insLines.add(n);
|
|
1930
|
+
return " ";
|
|
1931
|
+
});
|
|
1932
|
+
s = s.replace(/\bdel=\{([^}]*)\}/g, (_m, spec) => {
|
|
1933
|
+
for (const n of expandRanges(spec)) meta.delLines.add(n);
|
|
1934
|
+
return " ";
|
|
1935
|
+
});
|
|
1936
|
+
s = s.replace(/\bcollapse=\{([^}]*)\}/g, (_m, spec) => {
|
|
1937
|
+
for (const n of expandRanges(spec)) meta.collapseLines.add(n);
|
|
1938
|
+
return " ";
|
|
1939
|
+
});
|
|
1940
|
+
for (const m of s.matchAll(/"([^"]*)"|'([^']*)'/g)) {
|
|
1941
|
+
const v = m[1] ?? m[2];
|
|
1942
|
+
if (v) meta.searchWords.push(v);
|
|
1943
|
+
}
|
|
1944
|
+
s = s.replace(/"[^"]*"|'[^']*'/g, " ");
|
|
1945
|
+
s = s.replace(/\bwrap\b/g, () => {
|
|
1946
|
+
meta.wrap = true;
|
|
1947
|
+
return " ";
|
|
1948
|
+
});
|
|
1949
|
+
s = s.replace(/\{([^}]*)\}/g, (_m, spec) => {
|
|
1950
|
+
for (const n of expandRanges(spec)) meta.highlightLines.add(n);
|
|
1951
|
+
return " ";
|
|
1952
|
+
});
|
|
1953
|
+
return meta;
|
|
1954
|
+
}
|
|
1955
|
+
/** Collect the plain-text content of a hast line node (concatenates spans). */
|
|
1956
|
+
function lineText(node) {
|
|
1957
|
+
if (node.type === "text") return node.value ?? "";
|
|
1958
|
+
let out = "";
|
|
1959
|
+
if ("children" in node && node.children) for (const child of node.children) out += lineText(child);
|
|
1960
|
+
return out;
|
|
1961
|
+
}
|
|
1962
|
+
/** Find every (non-overlapping) start index of `substr` in `str`. */
|
|
1963
|
+
function findAllSubstringIndexes(str, substr) {
|
|
1964
|
+
const out = [];
|
|
1965
|
+
if (!substr) return out;
|
|
1966
|
+
let cursor = 0;
|
|
1967
|
+
for (;;) {
|
|
1968
|
+
const index = str.indexOf(substr, cursor);
|
|
1969
|
+
if (index === -1) break;
|
|
1970
|
+
out.push(index);
|
|
1971
|
+
cursor = index + substr.length;
|
|
1972
|
+
}
|
|
1973
|
+
return out;
|
|
1974
|
+
}
|
|
1975
|
+
const META_SYMBOL = Symbol("nimbus-meta");
|
|
1976
|
+
/**
|
|
1732
1977
|
* The canonical Shiki transformer chain for Nimbus. Returns a fresh
|
|
1733
1978
|
* array each call so callers don't accidentally mutate a shared list.
|
|
1734
1979
|
*
|
|
@@ -1745,11 +1990,57 @@ function defaultCodeTransformers() {
|
|
|
1745
1990
|
transformerNotationFocus(),
|
|
1746
1991
|
transformerNotationErrorLevel(),
|
|
1747
1992
|
transformerNotationWordHighlight(),
|
|
1748
|
-
|
|
1749
|
-
transformerMetaWordHighlight(),
|
|
1993
|
+
nimbusMetaTransformer(),
|
|
1750
1994
|
titleAndLangTransformer()
|
|
1751
1995
|
];
|
|
1752
1996
|
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Nimbus-owned fence-meta transformer. Owns bare-brace highlight
|
|
1999
|
+
* (space-tolerant), `ins=`/`del=` (brace + quoted-string forms),
|
|
2000
|
+
* quoted-search word highlight, `wrap`, `collapse` (neutral), and a
|
|
2001
|
+
* `frame=` hook. Replaces the stock meta-highlight + meta-word-highlight
|
|
2002
|
+
* transformers, which double-fired and hijacked braces.
|
|
2003
|
+
*/
|
|
2004
|
+
function nimbusMetaTransformer() {
|
|
2005
|
+
function getMeta(ctx) {
|
|
2006
|
+
const carrier = ctx.meta ?? {};
|
|
2007
|
+
if (!carrier[META_SYMBOL]) carrier[META_SYMBOL] = parseNimbusMeta(ctx.options.meta?.__raw);
|
|
2008
|
+
return carrier[META_SYMBOL];
|
|
2009
|
+
}
|
|
2010
|
+
return {
|
|
2011
|
+
name: "nimbus:meta",
|
|
2012
|
+
preprocess(code, options) {
|
|
2013
|
+
if (!this.options.meta?.__raw) return;
|
|
2014
|
+
const meta = getMeta(this);
|
|
2015
|
+
if (meta.searchWords.length === 0) return;
|
|
2016
|
+
options.decorations ||= [];
|
|
2017
|
+
for (const word of meta.searchWords) for (const index of findAllSubstringIndexes(code, word)) options.decorations.push({
|
|
2018
|
+
start: index,
|
|
2019
|
+
end: index + word.length,
|
|
2020
|
+
properties: { class: "highlighted-word" }
|
|
2021
|
+
});
|
|
2022
|
+
},
|
|
2023
|
+
line(node, lineNumber) {
|
|
2024
|
+
if (!this.options.meta?.__raw) return;
|
|
2025
|
+
const meta = getMeta(this);
|
|
2026
|
+
if (meta.highlightLines.has(lineNumber)) this.addClassToHast(node, "highlighted");
|
|
2027
|
+
if (meta.insLines.has(lineNumber)) this.addClassToHast(node, "diff add");
|
|
2028
|
+
if (meta.delLines.has(lineNumber)) this.addClassToHast(node, "diff remove");
|
|
2029
|
+
if (meta.insTokens.length || meta.delTokens.length) {
|
|
2030
|
+
const text = lineText(node);
|
|
2031
|
+
if (meta.insTokens.some((t) => text.includes(t))) this.addClassToHast(node, "diff add");
|
|
2032
|
+
if (meta.delTokens.some((t) => text.includes(t))) this.addClassToHast(node, "diff remove");
|
|
2033
|
+
}
|
|
2034
|
+
},
|
|
2035
|
+
pre(preNode) {
|
|
2036
|
+
if (!this.options.meta?.__raw) return;
|
|
2037
|
+
const meta = getMeta(this);
|
|
2038
|
+
preNode.properties = preNode.properties ?? {};
|
|
2039
|
+
if (meta.wrap) preNode.properties["data-nb-wrap"] = "";
|
|
2040
|
+
if (meta.frame) preNode.properties["data-nb-frame"] = meta.frame;
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
1753
2044
|
function titleAndLangTransformer() {
|
|
1754
2045
|
return {
|
|
1755
2046
|
name: "nimbus:title-and-lang",
|
|
@@ -1759,6 +2050,8 @@ function titleAndLangTransformer() {
|
|
|
1759
2050
|
const title = parseTitle(meta);
|
|
1760
2051
|
preNode.properties = preNode.properties ?? {};
|
|
1761
2052
|
preNode.properties["data-nb-lang"] = lang;
|
|
2053
|
+
const wrap = preNode.properties["data-nb-wrap"] !== void 0;
|
|
2054
|
+
const frame = preNode.properties["data-nb-frame"];
|
|
1762
2055
|
const children = [];
|
|
1763
2056
|
if (title) children.push({
|
|
1764
2057
|
type: "element",
|
|
@@ -1783,13 +2076,16 @@ function titleAndLangTransformer() {
|
|
|
1783
2076
|
}]
|
|
1784
2077
|
});
|
|
1785
2078
|
children.push(preNode);
|
|
2079
|
+
const figureProps = {
|
|
2080
|
+
class: title ? "nb-code-figure nb-code-figure-titled" : "nb-code-figure",
|
|
2081
|
+
"data-nb-lang": lang
|
|
2082
|
+
};
|
|
2083
|
+
if (wrap) figureProps["data-nb-wrap"] = "";
|
|
2084
|
+
if (typeof frame === "string") figureProps["data-nb-frame"] = frame;
|
|
1786
2085
|
return {
|
|
1787
2086
|
type: "element",
|
|
1788
2087
|
tagName: "figure",
|
|
1789
|
-
properties:
|
|
1790
|
-
class: title ? "nb-code-figure nb-code-figure-titled" : "nb-code-figure",
|
|
1791
|
-
"data-nb-lang": lang
|
|
1792
|
-
},
|
|
2088
|
+
properties: figureProps,
|
|
1793
2089
|
children
|
|
1794
2090
|
};
|
|
1795
2091
|
}
|
|
@@ -1856,9 +2152,9 @@ async function validateMdxContent(options) {
|
|
|
1856
2152
|
* Format a list of failures into a single multi-line error message
|
|
1857
2153
|
* suitable for `throw new Error(...)`.
|
|
1858
2154
|
*/
|
|
1859
|
-
function formatFailures(failures
|
|
2155
|
+
function formatFailures(failures) {
|
|
1860
2156
|
const lines = failures.map((f) => {
|
|
1861
|
-
const fix = f.hint ? `Did you mean <${f.hint} />?` :
|
|
2157
|
+
const fix = f.hint ? `Did you mean <${f.hint} />?` : `Register it in src/components.ts, or add an explicit \`import\` at the top of this file.`;
|
|
1862
2158
|
return ` ${f.filePath}:${f.line}:${f.column} <${f.tag} /> → ${fix}`;
|
|
1863
2159
|
});
|
|
1864
2160
|
return `[nimbus-docs] Unknown MDX component ${failures.length === 1 ? "tag" : "tags"}:\n` + lines.join("\n") + "\n\nA PascalCase tag in MDX must either be registered in src/components.ts (the global registry) or imported at the top of the file. Without either, MDX renders the tag as literal text on the page — a silent failure this validator turns into a build error.";
|
|
@@ -2086,7 +2382,7 @@ function validateNimbusConfig(input) {
|
|
|
2086
2382
|
const tail = received === null ? "" : `\n received: ${received}`;
|
|
2087
2383
|
return ` - ${display}: ${issue.message}${tail}`;
|
|
2088
2384
|
}).join("\n");
|
|
2089
|
-
throw new Error(`Invalid nimbus.config — fix these issues:\n${issues}\n\nSee https://nimbus-docs.
|
|
2385
|
+
throw new Error(`Invalid nimbus.config — fix these issues:\n${issues}\n\nSee https://nimbus-docs.com/config for the full config schema.`);
|
|
2090
2386
|
}
|
|
2091
2387
|
/**
|
|
2092
2388
|
* Resolve the value at `path` inside the raw input and format it for an
|
|
@@ -2134,18 +2430,11 @@ function virtualConfigPlugin(config, extras) {
|
|
|
2134
2430
|
* inside `.md` / `.mdx` files. Output feeds `shikiConfig.langs` so Shiki
|
|
2135
2431
|
* eager-loads every grammar at startup instead of lazy-loading on first use.
|
|
2136
2432
|
*
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2142
|
-
* without highlighting.
|
|
2143
|
-
*
|
|
2144
|
-
* Eager loading also gives non-incremental users a small predictability win:
|
|
2145
|
-
* the highlighter behaves the same regardless of which file is processed
|
|
2146
|
-
* first.
|
|
2147
|
-
*
|
|
2148
|
-
* Cost: ~1s on a 7k-file bench. Acceptable.
|
|
2433
|
+
* Needed because incremental builds skip cached MDX files: those never
|
|
2434
|
+
* enter the markdown pipeline, so a language appearing only in cached
|
|
2435
|
+
* files would never trigger Shiki's lazy grammar load, and a non-cached
|
|
2436
|
+
* file using it would render without highlighting. Eager loading also
|
|
2437
|
+
* keeps highlighting independent of which file is processed first.
|
|
2149
2438
|
*/
|
|
2150
2439
|
const FENCE_RE = /^[ \t]*```([a-zA-Z][a-zA-Z0-9_+\-]*)/gm;
|
|
2151
2440
|
async function* walkMdx(dir) {
|
|
@@ -2200,10 +2489,10 @@ async function scanCodeBlockLanguages(projectRoot, langAlias = {}) {
|
|
|
2200
2489
|
* pages/<aa>/<full-hash>.html — cached HTML body for a page, sharded
|
|
2201
2490
|
* by the first 2 hex chars of the hash
|
|
2202
2491
|
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2205
|
-
*
|
|
2206
|
-
*
|
|
2492
|
+
* Atomic per-file writes. A manifest-level `namespace` field provides
|
|
2493
|
+
* PR-vs-main isolation; resolution lives in `namespace.ts`. Framework/Node
|
|
2494
|
+
* version is folded into `globalHash` via `computeGlobalHash` already, so
|
|
2495
|
+
* it doesn't need a separate field.
|
|
2207
2496
|
*/
|
|
2208
2497
|
const SCHEMA_VERSION = 2;
|
|
2209
2498
|
var Cache = class {
|
|
@@ -2272,14 +2561,13 @@ var Cache = class {
|
|
|
2272
2561
|
/**
|
|
2273
2562
|
* Snapshot a *bounded subset* of `dist/_astro/` into the cache.
|
|
2274
2563
|
*
|
|
2275
|
-
*
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
* the set of asset rel-paths that some cached HTML actually references —
|
|
2564
|
+
* Bounded so the cache doesn't grow forever: Vite emits new bundle
|
|
2565
|
+
* hashes whenever the module graph differs between builds. The caller
|
|
2566
|
+
* passes the asset rel-paths that some cached HTML actually references;
|
|
2279
2567
|
* anything outside that set gets dropped.
|
|
2280
2568
|
*
|
|
2281
2569
|
* `referencedRelPaths` should be the union of every `/_astro/...` URL
|
|
2282
|
-
* extracted from cached HTML — see `
|
|
2570
|
+
* extracted from cached HTML — see `collectReferencedAssets` in index.ts.
|
|
2283
2571
|
*/
|
|
2284
2572
|
async snapshotAssets(distAstroDir, referencedRelPaths) {
|
|
2285
2573
|
const target = resolve(this.root, "assets");
|
|
@@ -2448,10 +2736,9 @@ async function writeAtomic(path, data) {
|
|
|
2448
2736
|
* - pageHash: sha256(page bytes + globalHash). Determines whether a
|
|
2449
2737
|
* given page's cached HTML is still valid.
|
|
2450
2738
|
*
|
|
2451
|
-
*
|
|
2452
|
-
*
|
|
2453
|
-
*
|
|
2454
|
-
* to capture `<Render file="…">` references.
|
|
2739
|
+
* Current scope deliberately omits data-collection tracking and
|
|
2740
|
+
* component-graph tracking. Partial-dependency tracking folds the partial
|
|
2741
|
+
* registry into the page hash (see `partial-refs.ts`).
|
|
2455
2742
|
*/
|
|
2456
2743
|
const TRACKED_DIRS = ["src", "public"];
|
|
2457
2744
|
const TRACKED_FILES = [
|
|
@@ -2514,7 +2801,7 @@ async function walk$1(dir, root) {
|
|
|
2514
2801
|
* - Node major version (minor diffs occasionally affect bundling)
|
|
2515
2802
|
* - Platform + arch (some asset emission is platform-sensitive)
|
|
2516
2803
|
*
|
|
2517
|
-
* Including provenance closes
|
|
2804
|
+
* Including provenance closes a class of staleness bug: a framework upgrade
|
|
2518
2805
|
* (or Node bump, or OS change) silently changed rendered output but the
|
|
2519
2806
|
* old global hash matched, so warm builds served stale entries from a
|
|
2520
2807
|
* different version of the world.
|
|
@@ -2583,7 +2870,7 @@ async function readDepVersion(projectRoot, dep) {
|
|
|
2583
2870
|
}
|
|
2584
2871
|
}
|
|
2585
2872
|
/**
|
|
2586
|
-
*
|
|
2873
|
+
* Per-page hash with transitive partial dependencies folded in.
|
|
2587
2874
|
*
|
|
2588
2875
|
* Same shape as `computePageHash` but additionally absorbs the bytes of
|
|
2589
2876
|
* every partial the page transitively embeds. Sorted by path so two
|
|
@@ -2667,14 +2954,14 @@ async function resolveCacheNamespace(projectRoot) {
|
|
|
2667
2954
|
//#endregion
|
|
2668
2955
|
//#region src/_internal/incremental/partial-refs.ts
|
|
2669
2956
|
/**
|
|
2670
|
-
*
|
|
2957
|
+
* Partial dependency tracking.
|
|
2671
2958
|
*
|
|
2672
2959
|
* Walks MDX content to find `<Render file="…" />` and `<Render slug="…" />`
|
|
2673
2960
|
* references, then builds a per-page transitive closure: "pathname X
|
|
2674
2961
|
* embeds partials A, B, C — where A in turn embeds D, and B in turn
|
|
2675
2962
|
* embeds E and F." Folding all of those partials' bytes into the page's
|
|
2676
|
-
* hash gives us the property
|
|
2677
|
-
*
|
|
2963
|
+
* hash gives us the property we want: edit one partial, exactly the pages
|
|
2964
|
+
* that transitively embed it re-render.
|
|
2678
2965
|
*
|
|
2679
2966
|
* Scope (v1):
|
|
2680
2967
|
* - Only string-literal `file` / `slug` props get captured. Dynamic
|
|
@@ -2683,16 +2970,17 @@ async function resolveCacheNamespace(projectRoot) {
|
|
|
2683
2970
|
* v1 limitation; the `partialResolver` hook (deferred) gives sites
|
|
2684
2971
|
* an escape valve.
|
|
2685
2972
|
* - Default resolver: `<Render file="topic/slug" />` resolves to
|
|
2686
|
-
* `src/content/partials/topic/slug.mdx`.
|
|
2687
|
-
*
|
|
2688
|
-
*
|
|
2973
|
+
* `src/content/partials/topic/slug.mdx`. Sites with a multi-prop
|
|
2974
|
+
* convention (e.g. a resolver that prepends a `product` prop) need a
|
|
2975
|
+
* custom resolver.
|
|
2689
2976
|
* - Cycles in the partial graph are handled (visited set).
|
|
2690
2977
|
*/
|
|
2691
2978
|
/**
|
|
2692
2979
|
* Check `candidate` is a normalised path under `rootWithSep`. Cheap
|
|
2693
2980
|
* defense against `../` traversal escaping the partials root. We use a
|
|
2694
2981
|
* trailing-sep marker on root to avoid false-matching `partialsRoot` with
|
|
2695
|
-
*
|
|
2982
|
+
* sibling directories that share its name as a prefix (e.g.
|
|
2983
|
+
* `partialsRoot-shared/`).
|
|
2696
2984
|
*/
|
|
2697
2985
|
function isInside(candidate, rootWithSep) {
|
|
2698
2986
|
return candidate.startsWith(rootWithSep) || candidate === rootWithSep.slice(0, -1);
|
|
@@ -2715,7 +3003,7 @@ const ATTR_RE = /([a-zA-Z][a-zA-Z0-9_]*)\s*=\s*["']([^"']*)["']/g;
|
|
|
2715
3003
|
* extensions or use plain Markdown for partials.
|
|
2716
3004
|
*
|
|
2717
3005
|
* `partialsBase` lets callers point the resolver at a non-default partials
|
|
2718
|
-
* collection base
|
|
3006
|
+
* collection base. Default: `src/content/partials`.
|
|
2719
3007
|
*/
|
|
2720
3008
|
function makeDefaultPartialResolver(projectRoot, partialsBase = "src/content/partials") {
|
|
2721
3009
|
const partialsRoot = resolve(projectRoot, partialsBase);
|
|
@@ -2884,7 +3172,7 @@ async function partialsDirExists(projectRoot, partialsBase = "src/content/partia
|
|
|
2884
3172
|
//#endregion
|
|
2885
3173
|
//#region src/_internal/incremental/index.ts
|
|
2886
3174
|
/**
|
|
2887
|
-
* Incremental builds
|
|
3175
|
+
* Incremental builds.
|
|
2888
3176
|
*
|
|
2889
3177
|
* Wires the cache layer into Astro's prerenderer. On warm build, pages whose
|
|
2890
3178
|
* source bytes (and the global hash) haven't changed since the last build
|
|
@@ -2893,16 +3181,14 @@ async function partialsDirExists(projectRoot, partialsBase = "src/content/partia
|
|
|
2893
3181
|
*
|
|
2894
3182
|
* Astro sees every route in `getStaticPaths` either way — cache hits flow
|
|
2895
3183
|
* through `astro:build:generated`, adapter writers, route-headers accounting
|
|
2896
|
-
* exactly like fresh renders. This is
|
|
2897
|
-
* `getStaticPaths
|
|
2898
|
-
*
|
|
3184
|
+
* exactly like fresh renders. This is by design — rather than filtering
|
|
3185
|
+
* cached routes out of `getStaticPaths`, which would hide them from
|
|
3186
|
+
* downstream hooks.
|
|
2899
3187
|
*
|
|
2900
|
-
*
|
|
2901
|
-
* - Partial-dependency tracking. Edit a partial → still full rebuild today.
|
|
3188
|
+
* Out of scope for now:
|
|
2902
3189
|
* - Data-collection scoping.
|
|
2903
3190
|
* - Component-graph tracking. Any tracked-file change → full rebuild.
|
|
2904
|
-
* -
|
|
2905
|
-
* - `nimbus build --explain` and structured build reports. Console log only.
|
|
3191
|
+
* - `nimbus build --explain` and structured build reports.
|
|
2906
3192
|
*/
|
|
2907
3193
|
/**
|
|
2908
3194
|
* Normalise a request URL to its canonical pathname (no trailing slash,
|
|
@@ -2921,7 +3207,7 @@ function canonicalisePathname(input) {
|
|
|
2921
3207
|
}
|
|
2922
3208
|
/**
|
|
2923
3209
|
* Build a map from pathname → MDX file bytes by walking the docs collection
|
|
2924
|
-
* directory.
|
|
3210
|
+
* directory. Only the primary `docs` collection is handled.
|
|
2925
3211
|
*
|
|
2926
3212
|
* Pathname derivation: `src/content/docs/<entry.id>.mdx` → `/<entry.id>`,
|
|
2927
3213
|
* mirroring `getDocsStaticPaths` which uses `entry.id` verbatim as slug.
|
|
@@ -3019,9 +3305,9 @@ async function collectDocsPages(projectRoot, docsBase = "src/content/docs") {
|
|
|
3019
3305
|
* Computes per-page hashes, reads prior manifest, determines which pages
|
|
3020
3306
|
* are cache-hits.
|
|
3021
3307
|
*
|
|
3022
|
-
*
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3308
|
+
* The page hash includes the bytes of every partial the page transitively
|
|
3309
|
+
* embeds, so editing a partial invalidates exactly the pages that reference
|
|
3310
|
+
* it (directly or transitively) and nothing else.
|
|
3025
3311
|
*/
|
|
3026
3312
|
async function setupIncrementalContext(projectRoot, cacheDir, logger, partialResolver) {
|
|
3027
3313
|
const cache = new Cache(cacheDir ? resolve(cacheDir, "nimbus") : resolve(projectRoot, ".nimbus/cache"));
|
|
@@ -3081,7 +3367,7 @@ async function setupIncrementalContext(projectRoot, cacheDir, logger, partialRes
|
|
|
3081
3367
|
/**
|
|
3082
3368
|
* Wrap an Astro prerenderer with the cache.
|
|
3083
3369
|
*
|
|
3084
|
-
* Strategy (
|
|
3370
|
+
* Strategy (chosen empirically over the "wrap Response" approach
|
|
3085
3371
|
* because Astro's per-route work outside `render` is the actual dominant cost,
|
|
3086
3372
|
* not MDX→HTML conversion):
|
|
3087
3373
|
*
|
|
@@ -3094,12 +3380,12 @@ async function setupIncrementalContext(projectRoot, cacheDir, logger, partialRes
|
|
|
3094
3380
|
* `dist/<pathname>/index.html` for the filtered cached routes — Astro
|
|
3095
3381
|
* never wrote them, so we do.
|
|
3096
3382
|
*
|
|
3097
|
-
* Trade-off vs. the
|
|
3383
|
+
* Trade-off vs. the "wrap Response in render" design: downstream
|
|
3098
3384
|
* Astro hooks (`astro:build:generated`, adapter writers, route accounting)
|
|
3099
3385
|
* don't see cached routes. For Cloudflare adapter sites or anything that
|
|
3100
3386
|
* depends on every route being visible to those hooks, this matters.
|
|
3101
3387
|
* For static SSG sites where the rendered HTML *is* the output, it's fine.
|
|
3102
|
-
* Documented as a limitation
|
|
3388
|
+
* Documented as a known limitation.
|
|
3103
3389
|
*/
|
|
3104
3390
|
function wrapPrerenderer(defaultPrerenderer, ctx) {
|
|
3105
3391
|
return {
|
|
@@ -3176,7 +3462,7 @@ async function restoreCachedPagesToDist(ctx, outDir) {
|
|
|
3176
3462
|
* (so the snapshot is the union of fresh + previously-cached assets the
|
|
3177
3463
|
* cached HTML still references).
|
|
3178
3464
|
*
|
|
3179
|
-
*
|
|
3465
|
+
* Bounded to assets actually referenced by cached HTML. We
|
|
3180
3466
|
* walk every cached page's bytes, regex-extract `/_astro/...` URLs,
|
|
3181
3467
|
* dedupe — and only persist those. Without this the snapshot grew
|
|
3182
3468
|
* unboundedly because vite produces new bundle hashes on every warm
|
|
@@ -3193,7 +3479,7 @@ const ASSET_REF_RE = /\/_astro\/([^"')\s>]+)/g;
|
|
|
3193
3479
|
* Strip query string and hash from an extracted asset path. Without
|
|
3194
3480
|
* this, `/_astro/foo.js?v=1` would record `foo.js?v=1` as the file
|
|
3195
3481
|
* name — the snapshot would skip it because no such file exists in
|
|
3196
|
-
* `_astro/`, leaving the warm build with a broken reference
|
|
3482
|
+
* `_astro/`, leaving the warm build with a broken reference.
|
|
3197
3483
|
*/
|
|
3198
3484
|
function normaliseAssetRef(raw) {
|
|
3199
3485
|
if (!raw) return null;
|
|
@@ -3213,10 +3499,9 @@ function normaliseAssetRef(raw) {
|
|
|
3213
3499
|
*
|
|
3214
3500
|
* The single regex matches `/_astro/...` anywhere in the HTML —
|
|
3215
3501
|
* straightforward for `href="..."`, `src="..."`, `url(...)` in inline
|
|
3216
|
-
* styles, and individual `srcset` URLs alike. (
|
|
3217
|
-
*
|
|
3218
|
-
*
|
|
3219
|
-
* catches them all.)
|
|
3502
|
+
* styles, and individual `srcset` URLs alike. (An earlier regex
|
|
3503
|
+
* anchored on a quote/paren prefix and missed the second+nth URL
|
|
3504
|
+
* inside a `srcset` value; the unanchored form here catches them all.)
|
|
3220
3505
|
*
|
|
3221
3506
|
* We scan the dist output rather than the in-memory cache because dist
|
|
3222
3507
|
* is the source of truth for what's currently referenced — after the
|
|
@@ -3335,8 +3620,8 @@ function mdxSkipPlugin(ctx) {
|
|
|
3335
3620
|
* - sitemap-0.xml carries all entries (we don't split until >45k urls)
|
|
3336
3621
|
* - sitemap-index.xml lists sitemap-0.xml only
|
|
3337
3622
|
*
|
|
3338
|
-
*
|
|
3339
|
-
*
|
|
3623
|
+
* Scope: an optional `serialize` hook per URL, but no `lastmod`,
|
|
3624
|
+
* `changefreq`, `priority`, and no image/video sitemaps. Matches
|
|
3340
3625
|
* `@astrojs/sitemap` *default* output for sites that don't override.
|
|
3341
3626
|
*/
|
|
3342
3627
|
const URLSET_XMLNS = "xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\" xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\" xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"";
|
|
@@ -3487,8 +3772,7 @@ function extractFrontmatter(source) {
|
|
|
3487
3772
|
const rest = source.slice(afterFirstMarker + 1);
|
|
3488
3773
|
const closingMatch = rest.match(/(^|\n)---\s*(\n|$)/);
|
|
3489
3774
|
if (!closingMatch || closingMatch.index === void 0) return null;
|
|
3490
|
-
|
|
3491
|
-
return rest.slice(0, endIndex);
|
|
3775
|
+
return rest.slice(0, closingMatch.index);
|
|
3492
3776
|
}
|
|
3493
3777
|
/**
|
|
3494
3778
|
* Find a top-level boolean field in YAML frontmatter. Returns the
|
|
@@ -3519,9 +3803,8 @@ function parseBoolField(yaml, field) {
|
|
|
3519
3803
|
* - `string[]` for either array form
|
|
3520
3804
|
* - `undefined` if absent
|
|
3521
3805
|
*
|
|
3522
|
-
*
|
|
3523
|
-
*
|
|
3524
|
-
* dropped valid block lists at build time. The schema validates the
|
|
3806
|
+
* All three forms are accepted: scalar, inline array, and the multiline
|
|
3807
|
+
* block list (canonical YAML list syntax). The schema validates the
|
|
3525
3808
|
* post-parse shape; the scanner has to match it.
|
|
3526
3809
|
*/
|
|
3527
3810
|
function parsePreviousSlugField(yaml) {
|
|
@@ -3824,7 +4107,7 @@ function nimbus(rawConfig, options = {}) {
|
|
|
3824
4107
|
skip: validateOpts.skip,
|
|
3825
4108
|
projectRoot
|
|
3826
4109
|
});
|
|
3827
|
-
if (failures.length > 0) throw new Error(formatFailures(failures
|
|
4110
|
+
if (failures.length > 0) throw new Error(formatFailures(failures));
|
|
3828
4111
|
logger.info(`MDX validation passed — ${globals.length} global component${globals.length === 1 ? "" : "s"} registered, ${contentDirs.length} content dir${contentDirs.length === 1 ? "" : "s"} scanned.`);
|
|
3829
4112
|
}
|
|
3830
4113
|
}
|
|
@@ -4075,9 +4358,9 @@ function runPagefind(siteDir) {
|
|
|
4075
4358
|
/**
|
|
4076
4359
|
* Main entry for `nimbus-docs`.
|
|
4077
4360
|
*
|
|
4078
|
-
* Exports the Astro integration (default), config helper,
|
|
4079
|
-
*
|
|
4080
|
-
*
|
|
4361
|
+
* Exports the Astro integration (default), config helper, the data helpers
|
|
4362
|
+
* (sidebar, prev/next, breadcrumbs, TOC), and the page composition helpers
|
|
4363
|
+
* (`getDocsStaticPaths`, `getDocsPageProps`).
|
|
4081
4364
|
*
|
|
4082
4365
|
* Helpers read the user's config from `virtual:nimbus/config` (provided
|
|
4083
4366
|
* by our Vite plugin) and content entries from `astro:content`. Both
|
|
@@ -4232,8 +4515,19 @@ async function getIndexedTopLevel() {
|
|
|
4232
4515
|
*/
|
|
4233
4516
|
async function getSidebar(currentSlug, options) {
|
|
4234
4517
|
const config = await loadNimbusConfig();
|
|
4235
|
-
const
|
|
4236
|
-
|
|
4518
|
+
const fullTree = await buildFullSidebarTree(currentSlug, options?.collection);
|
|
4519
|
+
let tree = config.sidebar?.scope === "section" ? scopeToCurrentSection(fullTree, currentSlug) : fullTree;
|
|
4520
|
+
const boundaries = config.sidebar?.isolate?.boundaries;
|
|
4521
|
+
if (boundaries && boundaries.length > 0) tree = isolateToBoundary(tree, currentSlug, boundaries);
|
|
4522
|
+
if (options?.transform) {
|
|
4523
|
+
const ctx = deriveTransformCtx(fullTree, currentSlug);
|
|
4524
|
+
tree = await options.transform({
|
|
4525
|
+
tree,
|
|
4526
|
+
currentSlug,
|
|
4527
|
+
...ctx
|
|
4528
|
+
});
|
|
4529
|
+
}
|
|
4530
|
+
return tree;
|
|
4237
4531
|
}
|
|
4238
4532
|
/**
|
|
4239
4533
|
* Derive one section per top-level group in the sidebar — used by
|
|
@@ -4324,13 +4618,74 @@ async function getPrevNext(currentSlug, options) {
|
|
|
4324
4618
|
return getPrevNext$1(currentSlug, tree, options?.overrides, validInternalLinks);
|
|
4325
4619
|
}
|
|
4326
4620
|
/**
|
|
4327
|
-
* Build breadcrumb trail from
|
|
4328
|
-
*
|
|
4329
|
-
*
|
|
4330
|
-
*
|
|
4621
|
+
* Build the breadcrumb trail from the active node's ancestry in the nav
|
|
4622
|
+
* tree. Labels come from nav nodes, hrefs from each node's landing — so a
|
|
4623
|
+
* section crumb links to its real landing page and segments with no node
|
|
4624
|
+
* never appear. Index-less folders render as non-interactive crumbs.
|
|
4625
|
+
*
|
|
4626
|
+
* - `collection` — the page's Astro collection; pass `entry.collection` so
|
|
4627
|
+
* versioned pages get version-prefixed hrefs.
|
|
4628
|
+
* - `root` — the leading crumb (default `{ label: "Home", href: "/" }`).
|
|
4629
|
+
* - `resolveLabel` — override a crumb label, or return `null` to drop it.
|
|
4630
|
+
*
|
|
4631
|
+
* Falls back to URL-segment derivation when the page has no node in the
|
|
4632
|
+
* tree, so a stray page still gets a root-anchored trail.
|
|
4331
4633
|
*/
|
|
4332
4634
|
async function getBreadcrumbs(currentSlug, options) {
|
|
4333
|
-
|
|
4635
|
+
const path = findActivePath(await buildFullSidebarTree(currentSlug, options?.collection), currentSlug);
|
|
4636
|
+
if (path.length > 0) return assembleBreadcrumbs(options?.root ?? {
|
|
4637
|
+
label: "Home",
|
|
4638
|
+
href: "/"
|
|
4639
|
+
}, path, await Promise.all(path.map((node) => Promise.resolve(options?.resolveLabel?.({
|
|
4640
|
+
node,
|
|
4641
|
+
slug: currentSlug
|
|
4642
|
+
})))));
|
|
4643
|
+
return breadcrumbsFromUrl(currentSlug, options?.root?.label ?? "Home");
|
|
4644
|
+
}
|
|
4645
|
+
/**
|
|
4646
|
+
* Resolve a section's display title(s) for the current page, decoupled so
|
|
4647
|
+
* the rail header and the breadcrumb can differ.
|
|
4648
|
+
*
|
|
4649
|
+
* Derives `sectionSlug` (seg0) and `module` (seg1) from the slug and passes
|
|
4650
|
+
* them to a caller-supplied resolver. The resolver is an argument rather
|
|
4651
|
+
* than config because config is JSON-serialized and cannot carry functions.
|
|
4652
|
+
* `indexEntryId` is currently always `undefined`.
|
|
4653
|
+
*/
|
|
4654
|
+
async function getSectionTitle(currentSlug, resolve) {
|
|
4655
|
+
const segs = currentSlug.split("/").filter(Boolean);
|
|
4656
|
+
const sectionSlug = segs[0];
|
|
4657
|
+
if (!sectionSlug) return void 0;
|
|
4658
|
+
return resolve({
|
|
4659
|
+
sectionSlug,
|
|
4660
|
+
module: segs[1],
|
|
4661
|
+
indexEntryId: void 0
|
|
4662
|
+
});
|
|
4663
|
+
}
|
|
4664
|
+
/**
|
|
4665
|
+
* Navigation (breadcrumbs, sidebar active-state, optional prev/next) for a
|
|
4666
|
+
* data-driven route with no content entry of its own — e.g. a catalog page
|
|
4667
|
+
* under `src/pages/[...].astro`.
|
|
4668
|
+
*
|
|
4669
|
+
* Builds the breadcrumb trail to `section` (a real nav node) and appends
|
|
4670
|
+
* `trail` (the leaf). The sidebar is built with `section` as the active
|
|
4671
|
+
* path, so the section node highlights even though the leaf is not in the
|
|
4672
|
+
* tree — the leaf is never injected, keeping the tree and prev/next clean.
|
|
4673
|
+
*/
|
|
4674
|
+
async function getRouteNavigation(options) {
|
|
4675
|
+
const { section, trail = [], prevNext = false, collection, resolveLabel } = options;
|
|
4676
|
+
const sidebar = await getSidebar(section, { collection });
|
|
4677
|
+
const breadcrumbs = composeRouteBreadcrumbs(await getBreadcrumbs(section, {
|
|
4678
|
+
collection,
|
|
4679
|
+
resolveLabel
|
|
4680
|
+
}), trail);
|
|
4681
|
+
let pn;
|
|
4682
|
+
if (prevNext) pn = await getPrevNext(section, { sidebarTree: sidebar });
|
|
4683
|
+
return {
|
|
4684
|
+
breadcrumbs,
|
|
4685
|
+
sidebar,
|
|
4686
|
+
activeHref: section,
|
|
4687
|
+
prevNext: pn
|
|
4688
|
+
};
|
|
4334
4689
|
}
|
|
4335
4690
|
/**
|
|
4336
4691
|
* Build an edit URL for a content entry using `config.editPattern`.
|
|
@@ -4682,5 +5037,5 @@ async function getVersionStatus(collectionId) {
|
|
|
4682
5037
|
}
|
|
4683
5038
|
|
|
4684
5039
|
//#endregion
|
|
4685
|
-
export { nimbus as default, defaultCodeTransformers, defineConfig, getBreadcrumbs, getCanonicalUrl, getCollectionLlmsUrl, getCollectionPageProps, getCollectionStaticPaths, getCurrentVersion, getDocsPageProps, getDocsStaticPaths, getEditUrl, getIndexedEntries, getIndexedTopLevel, getLastUpdated, getPrevNext, getSidebar, getSidebarSections, getTOC, getVersionAlternates, getVersionLandingUrl, getVersionStatus, getVersions, getVisibleEntries, renderEntryAsMarkdown, sidebarHash };
|
|
5040
|
+
export { nimbus as default, defaultCodeTransformers, defineConfig, getBreadcrumbs, getCanonicalUrl, getCollectionLlmsUrl, getCollectionPageProps, getCollectionStaticPaths, getCurrentVersion, getDocsPageProps, getDocsStaticPaths, getEditUrl, getIndexedEntries, getIndexedTopLevel, getLastUpdated, getPrevNext, getRouteNavigation, getSectionTitle, getSidebar, getSidebarSections, getTOC, getVersionAlternates, getVersionLandingUrl, getVersionStatus, getVersions, getVisibleEntries, renderEntryAsMarkdown, sidebarHash };
|
|
4686
5041
|
//# sourceMappingURL=index.js.map
|