blume 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +23 -14
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +29 -8
- package/src/components/layout/PageLayout.astro +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +18 -2
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +226 -35
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +65 -11
- package/src/theme/fonts.ts +189 -16
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
package/src/core/links.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { basename, dirname, join } from "pathe";
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from "pathe";
|
|
4
4
|
|
|
5
5
|
import { stripBasePath, withBasePath } from "./base-path.ts";
|
|
6
6
|
import {
|
|
@@ -37,6 +37,34 @@ interface LinkSite {
|
|
|
37
37
|
line: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Message context for a link written inside an included partial whose verdict
|
|
42
|
+
* depends on *which* page splices it (relative paths resolve against the
|
|
43
|
+
* including page's route; bare anchors against its headings). Without it the
|
|
44
|
+
* same partial line can be reported broken while being valid from another
|
|
45
|
+
* includer — with no way to tell the reports apart.
|
|
46
|
+
*/
|
|
47
|
+
const includedBySuffix = (link: PageLink, page: PageRecord): string =>
|
|
48
|
+
link.file ? ` (included by ${basename(page.sourcePath ?? page.id)})` : "";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Invert the include image rebase for display: expansion rewrote a partial's
|
|
52
|
+
* relative image target into the including page's directory so it resolves,
|
|
53
|
+
* but the diagnostic points at the partial — report the path the author
|
|
54
|
+
* actually wrote there, not the rebased one they never typed.
|
|
55
|
+
*/
|
|
56
|
+
const authoredImageTarget = (
|
|
57
|
+
target: string,
|
|
58
|
+
pagePath: string,
|
|
59
|
+
partialPath: string
|
|
60
|
+
): string => {
|
|
61
|
+
const authored = relative(
|
|
62
|
+
dirname(partialPath),
|
|
63
|
+
resolve(dirname(pagePath), target)
|
|
64
|
+
);
|
|
65
|
+
return authored.startsWith(".") ? authored : `./${authored}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
40
68
|
/** An external link occurrence queued for a network probe. */
|
|
41
69
|
interface ExternalRef extends LinkSite {
|
|
42
70
|
url: string;
|
|
@@ -130,7 +158,10 @@ const buildAnchorIndex = (pages: PageRecord[]): Map<string, Set<string>> => {
|
|
|
130
158
|
for (const page of pages) {
|
|
131
159
|
anchors.set(
|
|
132
160
|
page.route,
|
|
133
|
-
new Set(
|
|
161
|
+
new Set([
|
|
162
|
+
...page.headings.map((heading) => heading.slug),
|
|
163
|
+
...page.anchors,
|
|
164
|
+
])
|
|
134
165
|
);
|
|
135
166
|
}
|
|
136
167
|
return anchors;
|
|
@@ -141,15 +172,19 @@ const checkAnchor = (
|
|
|
141
172
|
route: string,
|
|
142
173
|
fragment: string,
|
|
143
174
|
site: LinkSite,
|
|
144
|
-
ctx: LinkContext
|
|
175
|
+
ctx: LinkContext,
|
|
176
|
+
via = ""
|
|
145
177
|
): Diagnostic | null => {
|
|
146
|
-
|
|
178
|
+
const anchors = ctx.anchors.get(route);
|
|
179
|
+
// Exact first: a `[#custom-id]` pin may contain uppercase, which the
|
|
180
|
+
// lowercase fallback (lenient matching for slugger-generated ids) would miss.
|
|
181
|
+
if (anchors?.has(fragment) || anchors?.has(fragment.toLowerCase())) {
|
|
147
182
|
return null;
|
|
148
183
|
}
|
|
149
184
|
return {
|
|
150
185
|
...site,
|
|
151
186
|
code: "BLUME_BROKEN_ANCHOR",
|
|
152
|
-
message: `No
|
|
187
|
+
message: `No anchor target on ${route} matches #${fragment}${via}.`,
|
|
153
188
|
severity: "warning",
|
|
154
189
|
};
|
|
155
190
|
};
|
|
@@ -161,7 +196,8 @@ const checkPathLink = (
|
|
|
161
196
|
page: PageRecord,
|
|
162
197
|
link: PageLink,
|
|
163
198
|
site: LinkSite,
|
|
164
|
-
ctx: LinkContext
|
|
199
|
+
ctx: LinkContext,
|
|
200
|
+
via = ""
|
|
165
201
|
): LinkResult => {
|
|
166
202
|
// Page routes carry the site-wide base; an absolute author path is written
|
|
167
203
|
// as if mounted at root, so base it for the route lookup (idempotent — a
|
|
@@ -170,7 +206,7 @@ const checkPathLink = (
|
|
|
170
206
|
// (e.g. `/releases/v1.0`) isn't misread as a missing asset.
|
|
171
207
|
const route = toRoute(withBasePath(ctx.basePath, resolved));
|
|
172
208
|
if (ctx.routes.has(route)) {
|
|
173
|
-
return fragment ? checkAnchor(route, fragment, site, ctx) : null;
|
|
209
|
+
return fragment ? checkAnchor(route, fragment, site, ctx, via) : null;
|
|
174
210
|
}
|
|
175
211
|
// A custom `.astro` page or generated route serves this path, but its
|
|
176
212
|
// headings aren't indexed — accept any fragment rather than false-flag it.
|
|
@@ -203,10 +239,16 @@ const checkPathLink = (
|
|
|
203
239
|
if (resolveRelativeImage(dirname(page.sourcePath), link.target)) {
|
|
204
240
|
return null;
|
|
205
241
|
}
|
|
242
|
+
// A partial-origin image was rebased into the page's directory for
|
|
243
|
+
// resolution but reports against the partial — cite the path as
|
|
244
|
+
// authored there, so file, path, and "next to" agree.
|
|
245
|
+
const authored = link.file
|
|
246
|
+
? authoredImageTarget(link.target, page.sourcePath, link.file)
|
|
247
|
+
: link.target;
|
|
206
248
|
return {
|
|
207
249
|
...site,
|
|
208
250
|
code: "BLUME_BROKEN_ASSET",
|
|
209
|
-
message: `Image ${
|
|
251
|
+
message: `Image ${authored} was not found next to ${basename(link.file ?? page.sourcePath)}.`,
|
|
210
252
|
severity: "warning",
|
|
211
253
|
suggestion:
|
|
212
254
|
"Add the file next to the page source or fix the reference.",
|
|
@@ -228,7 +270,7 @@ const checkPathLink = (
|
|
|
228
270
|
return {
|
|
229
271
|
...site,
|
|
230
272
|
code: "BLUME_BROKEN_LINK",
|
|
231
|
-
message: `Broken link to ${link.target}: no page resolves to ${route}.`,
|
|
273
|
+
message: `Broken link to ${link.target}${via}: no page resolves to ${route}.`,
|
|
232
274
|
severity: "error",
|
|
233
275
|
suggestion: "Check the path, or create the target page.",
|
|
234
276
|
};
|
|
@@ -268,7 +310,9 @@ const classifyLink = (
|
|
|
268
310
|
const { target } = link;
|
|
269
311
|
const site: LinkSite = {
|
|
270
312
|
column: link.column,
|
|
271
|
-
|
|
313
|
+
// A link written inside an included partial reports against the partial
|
|
314
|
+
// file, so the author fixes it where it lives.
|
|
315
|
+
file: link.file ?? page.sourcePath ?? page.id,
|
|
272
316
|
line: link.line,
|
|
273
317
|
};
|
|
274
318
|
|
|
@@ -298,14 +342,19 @@ const classifyLink = (
|
|
|
298
342
|
}
|
|
299
343
|
rawPath = decodePercent(rawPath);
|
|
300
344
|
|
|
345
|
+
// A relative path (or a bare anchor) means something different from every
|
|
346
|
+
// including page; an absolute path means the same thing everywhere — so
|
|
347
|
+
// only includer-dependent targets carry the "included by" context, and
|
|
348
|
+
// identical absolute-target reports collapse in the final dedupe.
|
|
349
|
+
const via = rawPath.startsWith("/") ? "" : includedBySuffix(link, page);
|
|
301
350
|
if (rawPath === "") {
|
|
302
|
-
return fragment ? checkAnchor(page.route, fragment, site, ctx) : null;
|
|
351
|
+
return fragment ? checkAnchor(page.route, fragment, site, ctx, via) : null;
|
|
303
352
|
}
|
|
304
353
|
|
|
305
354
|
const resolved = rawPath.startsWith("/")
|
|
306
355
|
? rawPath
|
|
307
356
|
: resolveRelative(page.route, rawPath, isIndexPage(page));
|
|
308
|
-
return checkPathLink(resolved, fragment, page, link, site, ctx);
|
|
357
|
+
return checkPathLink(resolved, fragment, page, link, site, ctx, via);
|
|
309
358
|
};
|
|
310
359
|
|
|
311
360
|
/**
|
|
@@ -371,5 +420,22 @@ export const validateLinks = async (
|
|
|
371
420
|
diagnostics.push(...(await checkExternalLinks(external)));
|
|
372
421
|
}
|
|
373
422
|
|
|
374
|
-
|
|
423
|
+
// A partial spliced into several pages (or every locale of one) yields the
|
|
424
|
+
// same diagnostic once per including page record; byte-identical reports
|
|
425
|
+
// add noise without information, so only the first of each survives.
|
|
426
|
+
const seen = new Set<string>();
|
|
427
|
+
return diagnostics.filter((diagnostic) => {
|
|
428
|
+
const key = [
|
|
429
|
+
diagnostic.code,
|
|
430
|
+
diagnostic.file ?? "",
|
|
431
|
+
diagnostic.line ?? "",
|
|
432
|
+
diagnostic.column ?? "",
|
|
433
|
+
diagnostic.message,
|
|
434
|
+
].join("\n");
|
|
435
|
+
if (seen.has(key)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
seen.add(key);
|
|
439
|
+
return true;
|
|
440
|
+
});
|
|
375
441
|
};
|
package/src/core/meta.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { diagnosticsFromZod } from "./diagnostics.ts";
|
|
|
5
5
|
import { createModuleLoader } from "./load-module.ts";
|
|
6
6
|
import { folderMetaSchema } from "./schema.ts";
|
|
7
7
|
import type { FolderMeta } from "./schema.ts";
|
|
8
|
+
import { trimChar } from "./trim.ts";
|
|
8
9
|
import type { Diagnostic } from "./types.ts";
|
|
9
10
|
|
|
10
11
|
const META_FILES = [
|
|
@@ -50,7 +51,7 @@ export interface FolderMetaSource {
|
|
|
50
51
|
* source keys to `docs/provider`, exactly the group path navigation builds.
|
|
51
52
|
*/
|
|
52
53
|
const metaKeyFor = (prefix: string | undefined, dir: string): string => {
|
|
53
|
-
const clean = prefix ? prefix
|
|
54
|
+
const clean = prefix ? trimChar(prefix, "/") : "";
|
|
54
55
|
if (!clean) {
|
|
55
56
|
return dir;
|
|
56
57
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAssetIcon } from "../theme/icon-kind.ts";
|
|
2
2
|
import { hasIcon } from "../theme/icons.ts";
|
|
3
|
+
import { isInternalPath } from "./base-path.ts";
|
|
3
4
|
import type { Diagnostic, NavNode, Navigation, PageRecord } from "./types.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -120,12 +121,20 @@ export const validateNavTargets = (
|
|
|
120
121
|
label: link.label,
|
|
121
122
|
path: link.href,
|
|
122
123
|
})),
|
|
124
|
+
...(navigation.actions ?? []).map((action) => ({
|
|
125
|
+
label: action.label,
|
|
126
|
+
path: action.href,
|
|
127
|
+
})),
|
|
128
|
+
...(navigation.cta
|
|
129
|
+
? [{ label: navigation.cta.label, path: navigation.cta.href }]
|
|
130
|
+
: []),
|
|
123
131
|
];
|
|
124
132
|
const diagnostics: Diagnostic[] = [];
|
|
125
133
|
const seen = new Set<string>();
|
|
126
134
|
for (const { label, path } of targets) {
|
|
127
|
-
// Only internal, non-anchor paths can be checked against routes.
|
|
128
|
-
|
|
135
|
+
// Only internal, non-anchor paths can be checked against routes. A
|
|
136
|
+
// protocol-relative `//host/path` is external despite its leading slash.
|
|
137
|
+
if (!isInternalPath(path) || path.startsWith("/#") || seen.has(path)) {
|
|
129
138
|
continue;
|
|
130
139
|
}
|
|
131
140
|
if (!resolvesToPages(routes, path.split("#")[0] ?? path)) {
|
package/src/core/navigation.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import type {
|
|
10
10
|
Diagnostic,
|
|
11
11
|
FeaturedLink,
|
|
12
|
+
HeaderAction,
|
|
12
13
|
NavNode,
|
|
13
14
|
Navigation,
|
|
14
15
|
NavSelector,
|
|
@@ -788,6 +789,8 @@ const withTabHrefs = (tabs: NavTab[], sidebar: NavNode[]): NavTab[] =>
|
|
|
788
789
|
* their exact authored shape.
|
|
789
790
|
*/
|
|
790
791
|
interface NavChrome {
|
|
792
|
+
actions: HeaderAction[];
|
|
793
|
+
cta: HeaderAction | null;
|
|
791
794
|
featured: FeaturedLink[];
|
|
792
795
|
selectors: NavSelector[];
|
|
793
796
|
tabs: NavTab[];
|
|
@@ -796,26 +799,33 @@ interface NavChrome {
|
|
|
796
799
|
const rebaseNavChrome = (
|
|
797
800
|
basePath: string,
|
|
798
801
|
options: {
|
|
802
|
+
actions?: HeaderAction[];
|
|
803
|
+
cta?: HeaderAction | null;
|
|
799
804
|
featured?: FeaturedLink[];
|
|
800
805
|
selectors?: NavSelector[];
|
|
801
806
|
tabs?: NavTab[];
|
|
802
807
|
}
|
|
803
808
|
): NavChrome => {
|
|
809
|
+
const actions = options.actions ?? [];
|
|
810
|
+
const cta = options.cta ?? null;
|
|
804
811
|
const featured = options.featured ?? [];
|
|
805
812
|
const selectors = options.selectors ?? [];
|
|
806
813
|
const tabs = options.tabs ?? [];
|
|
807
814
|
if (!basePath) {
|
|
808
|
-
return { featured, selectors, tabs };
|
|
815
|
+
return { actions, cta, featured, selectors, tabs };
|
|
809
816
|
}
|
|
817
|
+
const rebaseHref = <T extends { href: string }>(item: T): T => ({
|
|
818
|
+
...item,
|
|
819
|
+
href: withBasePath(basePath, item.href),
|
|
820
|
+
});
|
|
810
821
|
const rebasePath = <T extends { path: string }>(item: T): T => ({
|
|
811
822
|
...item,
|
|
812
823
|
path: withBasePath(basePath, item.path),
|
|
813
824
|
});
|
|
814
825
|
return {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
})),
|
|
826
|
+
actions: actions.map(rebaseHref),
|
|
827
|
+
cta: cta ? rebaseHref(cta) : null,
|
|
828
|
+
featured: featured.map(rebaseHref),
|
|
819
829
|
selectors: selectors.map((selector) => ({
|
|
820
830
|
...selector,
|
|
821
831
|
items: selector.items.map(rebasePath),
|
|
@@ -838,8 +848,12 @@ const rebaseNavChrome = (
|
|
|
838
848
|
export const buildNavigation = (
|
|
839
849
|
pages: PageRecord[],
|
|
840
850
|
options: {
|
|
851
|
+
/** Plain links in the header, left of the icon buttons. */
|
|
852
|
+
actions?: HeaderAction[];
|
|
841
853
|
/** Site-wide route mount point (`""` or `/seg`); applied to config paths. */
|
|
842
854
|
basePath?: string;
|
|
855
|
+
/** The single primary call to action in the header. */
|
|
856
|
+
cta?: HeaderAction | null;
|
|
843
857
|
folderMeta: Map<string, FolderMeta>;
|
|
844
858
|
/** Global display mode for every sidebar group (default `flat`). */
|
|
845
859
|
display?: SidebarDisplay;
|
|
@@ -891,7 +905,10 @@ export const buildNavigation = (
|
|
|
891
905
|
// Content-derived sidebar routes are already based via `page.route`; the
|
|
892
906
|
// based tab paths also feed tab-scoping below, so they must agree with the
|
|
893
907
|
// based content routes.
|
|
894
|
-
const { featured, selectors, tabs } = rebaseNavChrome(
|
|
908
|
+
const { actions, cta, featured, selectors, tabs } = rebaseNavChrome(
|
|
909
|
+
basePath,
|
|
910
|
+
options
|
|
911
|
+
);
|
|
895
912
|
const byRoute = new Map(
|
|
896
913
|
pages.map((page) => [
|
|
897
914
|
options.refByLogical ? page.translationKey : page.route,
|
|
@@ -939,6 +956,8 @@ export const buildNavigation = (
|
|
|
939
956
|
basePath
|
|
940
957
|
);
|
|
941
958
|
return {
|
|
959
|
+
actions,
|
|
960
|
+
cta,
|
|
942
961
|
featured,
|
|
943
962
|
root: rootTabPath,
|
|
944
963
|
selectors,
|
|
@@ -960,6 +979,8 @@ export const buildNavigation = (
|
|
|
960
979
|
diagnostics
|
|
961
980
|
);
|
|
962
981
|
return {
|
|
982
|
+
actions,
|
|
983
|
+
cta,
|
|
963
984
|
featured,
|
|
964
985
|
root: rootTabPath,
|
|
965
986
|
selectors,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { relative } from "pathe";
|
|
1
|
+
import { isAbsolute, relative } from "pathe";
|
|
2
2
|
|
|
3
3
|
import { loadConfig } from "./config.ts";
|
|
4
4
|
import { applyDeploymentEnv } from "./deployment-env.ts";
|
|
5
5
|
import { buildContentGraph } from "./graph.ts";
|
|
6
6
|
import { i18nDiagnostics } from "./i18n.ts";
|
|
7
|
+
import { expandIncludes, hasIncludeStatements } from "./includes.ts";
|
|
7
8
|
import {
|
|
8
9
|
gitLastModifiedTimes,
|
|
10
|
+
gitRepositoryRoot,
|
|
9
11
|
lastModifiedShallowWarning,
|
|
10
12
|
resolveLastModifiedConfig,
|
|
11
13
|
} from "./last-modified.ts";
|
|
@@ -14,7 +16,7 @@ import { discoverFolderMeta } from "./meta.ts";
|
|
|
14
16
|
import type { FolderMetaSource } from "./meta.ts";
|
|
15
17
|
import { resolveProjectContext } from "./project.ts";
|
|
16
18
|
import type { ResolvedConfig } from "./schema.ts";
|
|
17
|
-
import { normalizeEntry } from "./sources/normalize.ts";
|
|
19
|
+
import { normalizeEntry, strippedLineOffset } from "./sources/normalize.ts";
|
|
18
20
|
import { resolveDocsCollection, resolveSources } from "./sources/resolve.ts";
|
|
19
21
|
import type { ContentSource, SourceLoadResult } from "./sources/types.ts";
|
|
20
22
|
import type {
|
|
@@ -42,6 +44,15 @@ export interface ConfigOverrides {
|
|
|
42
44
|
output?: ResolvedConfig["deployment"]["output"];
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Whether `path` sits at or under `dir`. A relative result that is itself
|
|
49
|
+
* absolute means a different drive root on Windows, which is outside too.
|
|
50
|
+
*/
|
|
51
|
+
export const isWithin = (dir: string, path: string): boolean => {
|
|
52
|
+
const rel = relative(dir, path);
|
|
53
|
+
return !(rel.startsWith("..") || isAbsolute(rel));
|
|
54
|
+
};
|
|
55
|
+
|
|
45
56
|
/** Apply CLI config overrides onto a resolved config (returns a new object). */
|
|
46
57
|
const applyConfigOverrides = (
|
|
47
58
|
config: ResolvedConfig,
|
|
@@ -268,6 +279,32 @@ export const scanProject = async (
|
|
|
268
279
|
}),
|
|
269
280
|
]);
|
|
270
281
|
|
|
282
|
+
// Expand `<include>` statements before normalization, so heading/link/
|
|
283
|
+
// component extraction sees the spliced content every render surface shows.
|
|
284
|
+
// Filesystem entries only: includes resolve as filesystem paths bounded by
|
|
285
|
+
// the owning source's content root. Unresolvable statements stay verbatim
|
|
286
|
+
// and surface as error diagnostics here.
|
|
287
|
+
const includeDiagnostics: Diagnostic[] = [];
|
|
288
|
+
await Promise.all(
|
|
289
|
+
loaded.flatMap(({ source, entries }) => {
|
|
290
|
+
if (source.staged || !source.contentRoot) {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
return entries.map(async (entry) => {
|
|
294
|
+
if (!entry.sourcePath || !hasIncludeStatements(entry.body.text)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const expansion = await expandIncludes(entry.body.text, {
|
|
298
|
+
contentRoot: source.contentRoot,
|
|
299
|
+
lineOffset: strippedLineOffset(entry.raw, entry.body.text),
|
|
300
|
+
sourcePath: entry.sourcePath,
|
|
301
|
+
});
|
|
302
|
+
entry.expanded = expansion;
|
|
303
|
+
includeDiagnostics.push(...expansion.errors);
|
|
304
|
+
});
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
|
|
271
308
|
// Folder meta contributed by the sources themselves (the OpenAPI source
|
|
272
309
|
// labels each tag directory with the spec's own tag name). It applies to
|
|
273
310
|
// every locale, so it merges into the shared map — beneath user-authored
|
|
@@ -300,13 +337,21 @@ export const scanProject = async (
|
|
|
300
337
|
const fsPaths = pages
|
|
301
338
|
.map((page) => page.sourcePath)
|
|
302
339
|
.filter((path): path is string => path !== undefined);
|
|
303
|
-
// The git pathspecs must cover where the pages actually live: each
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
340
|
+
// The git pathspecs must cover where the pages actually live: each local
|
|
341
|
+
// source's own on-disk root — a filesystem source's `root`, or a staged
|
|
342
|
+
// local source's tree (an Obsidian vault) — which diverges from the global
|
|
343
|
+
// `content.root`. A root outside the repository would fail the log outright
|
|
344
|
+
// and can never yield dates, so those are dropped up front.
|
|
345
|
+
const gitRoot = gitRepositoryRoot(context.root);
|
|
346
|
+
const contentRoots = sources
|
|
347
|
+
.flatMap((source) => (source.contentRoot ? [source.contentRoot] : []))
|
|
348
|
+
.filter((dir) => gitRoot !== null && isWithin(gitRoot, dir));
|
|
349
|
+
const gitTimes = gitLastModifiedTimes(
|
|
350
|
+
context.root,
|
|
351
|
+
contentRoots,
|
|
352
|
+
fsPaths,
|
|
353
|
+
gitRoot
|
|
308
354
|
);
|
|
309
|
-
const gitTimes = gitLastModifiedTimes(context.root, contentRoots, fsPaths);
|
|
310
355
|
for (const page of pages) {
|
|
311
356
|
if (!page.lastModified && page.sourcePath) {
|
|
312
357
|
page.lastModified = gitTimes.get(page.sourcePath);
|
|
@@ -314,8 +359,14 @@ export const scanProject = async (
|
|
|
314
359
|
}
|
|
315
360
|
// A shallow CI clone (Vercel, actions/checkout) silently drops most dates;
|
|
316
361
|
// surface that instead of letting production diverge from local builds.
|
|
362
|
+
// Only pages the log could have dated count — a page outside every covered
|
|
363
|
+
// root stays undated no matter how deep the clone is, and the warning's
|
|
364
|
+
// suggested fix cannot help it.
|
|
317
365
|
const undated = pages.filter(
|
|
318
|
-
(page) =>
|
|
366
|
+
(page) =>
|
|
367
|
+
page.sourcePath &&
|
|
368
|
+
!page.lastModified &&
|
|
369
|
+
contentRoots.some((dir) => isWithin(dir, page.sourcePath ?? ""))
|
|
319
370
|
).length;
|
|
320
371
|
lastModifiedWarnings.push(
|
|
321
372
|
...lastModifiedShallowWarning(context.root, undated)
|
|
@@ -344,6 +395,7 @@ export const scanProject = async (
|
|
|
344
395
|
context,
|
|
345
396
|
diagnostics: [
|
|
346
397
|
...contentDiagnostics,
|
|
398
|
+
...includeDiagnostics,
|
|
347
399
|
...folderMeta.diagnostics,
|
|
348
400
|
...entryIdDiagnostics(pages, resolveDocsCollection(config, context).base),
|
|
349
401
|
...graph.diagnostics,
|