blume 0.5.4 → 0.6.1
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 +759 -406
- package/dist/cli/index.js.map +27 -25
- package/dist/types/core/config-input.d.ts +759 -0
- package/dist/types/core/config.d.ts +126 -3
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +50 -0
- package/dist/types/core/schema.d.ts +334 -62
- package/dist/types/core/types.d.ts +8 -0
- package/dist/types/index.d.ts +2 -1
- package/docs/advanced/changelog.mdx +10 -2
- package/docs/configuration/ai.mdx +56 -0
- package/docs/configuration/index.mdx +0 -2
- package/docs/configuration/seo.mdx +59 -1
- package/docs/configuration/theming.mdx +14 -9
- package/docs/content/meta.mdx +3 -17
- package/docs/content/navigation.mdx +41 -4
- package/docs/content/syntax.mdx +4 -8
- package/package.json +3 -1
- package/src/ai/agent-readability.ts +97 -0
- package/src/ai/ask-context.ts +131 -8
- package/src/ai/ask-data.ts +4 -1
- package/src/astro/generate.ts +40 -11
- package/src/astro/templates.ts +90 -10
- package/src/cli/commands/build.ts +41 -1
- package/src/cli/commands/dev.ts +31 -14
- package/src/cli/dev-lock.ts +94 -21
- package/src/components/content/GithubInfo.astro +11 -10
- package/src/components/content/TypeTable.astro +8 -3
- package/src/components/content/Update.astro +12 -2
- package/src/components/content/changelog-element.ts +62 -0
- package/src/components/islands/AskAI.astro +66 -2
- package/src/components/islands/ask-ai.tsx +289 -53
- package/src/components/layout/Header.astro +1 -1
- package/src/components/layout/NavTree.astro +1 -1
- package/src/components/layout/PageActions.astro +73 -30
- package/src/components/layout/RootLayout.astro +79 -10
- package/src/core/config-input.ts +933 -0
- package/src/core/config.ts +126 -3
- package/src/core/data.ts +4 -0
- package/src/core/graph.ts +7 -2
- package/src/core/i18n-ui.ts +5 -0
- package/src/core/nav-diagnostics.ts +7 -0
- package/src/core/navigation.ts +38 -12
- package/src/core/schema.ts +130 -22
- package/src/core/sources/filesystem.ts +5 -1
- package/src/core/sources/watch.ts +43 -12
- package/src/core/types.ts +9 -0
- package/src/deploy/adapter-output.ts +82 -0
- package/src/deploy/robots.ts +37 -4
- package/src/index.ts +1 -1
- package/src/markdown/index.ts +28 -30
- package/src/markdown/math.ts +3 -2
- package/src/openapi/scalar.ts +1 -1
- package/src/registry/eject.ts +21 -14
- package/src/search/documents.ts +9 -2
- package/src/theme/entry.ts +7 -3
- package/src/theme/palette.ts +21 -14
|
@@ -1,20 +1,51 @@
|
|
|
1
1
|
import type { WatchListener } from "node:fs";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Directory segments
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
4
|
+
* Directory segments that are never authored content: VCS, dependency trees,
|
|
5
|
+
* Blume's own generated project and build output, and framework/deploy caches.
|
|
6
|
+
* Both the content scan and the dev watcher skip these unconditionally, on top
|
|
7
|
+
* of the user's `content.exclude`.
|
|
8
|
+
*
|
|
9
|
+
* The scan needs them because a broadly-scoped `content.root` — `"."` or an app
|
|
10
|
+
* dir that also holds `node_modules`/`dist`, the common shape when migrating a
|
|
11
|
+
* docs app that lives at the repo or app root — would otherwise glob thousands
|
|
12
|
+
* of stray markdown files out of dependencies and build artifacts. `content.root`
|
|
13
|
+
* defaults to `docs/` where this rarely bites, but any wider root hits it.
|
|
14
|
+
*
|
|
15
|
+
* The watcher needs them because a recursive `fs.watch` rooted at the project
|
|
16
|
+
* also sees Blume's own `.blume/` output, which the dev server rewrites on every
|
|
17
|
+
* render (e.g. `.blume/.astro/data-store.json`). Left unfiltered, each such write
|
|
18
|
+
* re-triggers a rescan + runtime regeneration whose writes land back under
|
|
19
|
+
* `.blume/` and fire the watcher again: a self-sustaining loop that stalls page
|
|
20
|
+
* renders and floods the console (and, mid-render, corrupts Astro's dev module
|
|
21
|
+
* graph so `astro:server-app.js` fails to load). `fs.watch` has no ignore
|
|
15
22
|
* option, so we filter by the changed path in the callback.
|
|
16
23
|
*/
|
|
17
|
-
export const
|
|
24
|
+
export const BLUME_IGNORE_DIRS = [
|
|
25
|
+
".blume",
|
|
26
|
+
".cache",
|
|
27
|
+
".git",
|
|
28
|
+
".next",
|
|
29
|
+
".turbo",
|
|
30
|
+
".vercel",
|
|
31
|
+
"dist",
|
|
32
|
+
"node_modules",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Baseline scan-ignore globs applied to every filesystem source, unioned with
|
|
37
|
+
* the user's `content.exclude`. Kept in sync with the watcher via the shared
|
|
38
|
+
* {@link BLUME_IGNORE_DIRS} so `load()` and `watch()` never disagree on what is
|
|
39
|
+
* content. `**\/<dir>/**` matches the directory at the content root or nested.
|
|
40
|
+
*/
|
|
41
|
+
export const baselineScanIgnore = (): string[] =>
|
|
42
|
+
BLUME_IGNORE_DIRS.map((dir) => `**/${dir}/**`);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Alias retained for the dev watcher's call site and its tests; the watcher and
|
|
46
|
+
* the scan share the same never-content directory set.
|
|
47
|
+
*/
|
|
48
|
+
export const BLUME_WATCH_IGNORE_DIRS = BLUME_IGNORE_DIRS;
|
|
18
49
|
|
|
19
50
|
/** Extract single-segment ignore dirs (`foo`) from `foo/**`-style excludes. */
|
|
20
51
|
export const excludeDirSegments = (patterns: readonly string[]): string[] =>
|
package/src/core/types.ts
CHANGED
|
@@ -180,11 +180,20 @@ export interface NavSelector {
|
|
|
180
180
|
items: NavSelectorItem[];
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** A pinned link rendered above the sidebar sections (external or internal). */
|
|
184
|
+
export interface FeaturedLink {
|
|
185
|
+
label: string;
|
|
186
|
+
href: string;
|
|
187
|
+
icon?: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
183
190
|
/** The complete navigation model derived from the content graph. */
|
|
184
191
|
export interface Navigation {
|
|
185
192
|
tabs: NavTab[];
|
|
186
193
|
selectors: NavSelector[];
|
|
187
194
|
sidebar: NavNode[];
|
|
195
|
+
/** Pinned links shown above the sidebar sections, unscoped by tab. */
|
|
196
|
+
featured: FeaturedLink[];
|
|
188
197
|
/** Repo URL for the header link, or null when hidden (`navigation.repo`). */
|
|
189
198
|
repoUrl?: string | null;
|
|
190
199
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { dirname, join } from "pathe";
|
|
5
|
+
|
|
6
|
+
import type { ResolvedConfig } from "../core/schema.ts";
|
|
7
|
+
import type { ProjectContext } from "../core/types.ts";
|
|
8
|
+
|
|
9
|
+
type Adapter = NonNullable<ResolvedConfig["deployment"]["adapter"]>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Server adapters whose deploy bundle lands *outside* Astro's `outDir`, at a
|
|
13
|
+
* path relative to the Astro project root. Blume points the Astro root at the
|
|
14
|
+
* hidden `<root>/.blume` runtime, so these adapters write their bundle to
|
|
15
|
+
* `<root>/.blume/<path>` — where the deploy platform never looks. Each value is
|
|
16
|
+
* the sub-path to surface up to the real project root.
|
|
17
|
+
*
|
|
18
|
+
* `vercel` writes a Build Output API v3 tree at `.vercel/output`; only that
|
|
19
|
+
* subtree is moved, so a `vercel pull`-ed `.vercel/project.json` sitting at the
|
|
20
|
+
* project root survives the relocation. `netlify` owns its whole `.netlify`
|
|
21
|
+
* dir. `node` and `cloudflare` emit into `dist/` (already at the project root),
|
|
22
|
+
* so they are absent here and need no relocation.
|
|
23
|
+
*/
|
|
24
|
+
export const ADAPTER_OUTPUT_PATHS: Partial<Record<Adapter, string>> = {
|
|
25
|
+
netlify: ".netlify",
|
|
26
|
+
vercel: ".vercel/output",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Directory whose contents the deploy platform serves as static files. Build
|
|
31
|
+
* artifacts (robots.txt, sitemap.xml, llms.txt, …) must be written here to be
|
|
32
|
+
* served. For a Vercel server build that is the adapter's
|
|
33
|
+
* `.vercel/output/static`; every other build serves `dist/`.
|
|
34
|
+
*/
|
|
35
|
+
export const deployStaticDir = (
|
|
36
|
+
config: ResolvedConfig,
|
|
37
|
+
context: ProjectContext
|
|
38
|
+
): string => {
|
|
39
|
+
const { adapter, output } = config.deployment;
|
|
40
|
+
if (output === "server" && adapter === "vercel") {
|
|
41
|
+
return join(context.root, ".vercel", "output", "static");
|
|
42
|
+
}
|
|
43
|
+
return context.distDir ?? join(context.root, "dist");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Outcome of {@link surfaceAdapterOutput}, for logging and `.gitignore`. */
|
|
47
|
+
export type SurfaceResult =
|
|
48
|
+
| { moved: false }
|
|
49
|
+
| { from: string; ignore: string; moved: true; to: string };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Move a server adapter's deploy bundle out of the hidden `.blume` runtime and
|
|
53
|
+
* up to the project root, where the deploy platform (and `vercel deploy
|
|
54
|
+
* --prebuilt`) expects it. A no-op for static builds, for adapters that emit
|
|
55
|
+
* into `dist/`, and when the expected output is absent.
|
|
56
|
+
*/
|
|
57
|
+
export const surfaceAdapterOutput = async (
|
|
58
|
+
config: ResolvedConfig,
|
|
59
|
+
context: ProjectContext
|
|
60
|
+
): Promise<SurfaceResult> => {
|
|
61
|
+
const { adapter, output } = config.deployment;
|
|
62
|
+
if (output !== "server" || !adapter) {
|
|
63
|
+
return { moved: false };
|
|
64
|
+
}
|
|
65
|
+
const rel = ADAPTER_OUTPUT_PATHS[adapter];
|
|
66
|
+
if (!rel) {
|
|
67
|
+
return { moved: false };
|
|
68
|
+
}
|
|
69
|
+
const from = join(context.outDir, rel);
|
|
70
|
+
const to = join(context.root, rel);
|
|
71
|
+
if (!existsSync(from)) {
|
|
72
|
+
return { moved: false };
|
|
73
|
+
}
|
|
74
|
+
await mkdir(dirname(to), { recursive: true });
|
|
75
|
+
await rm(to, { force: true, recursive: true });
|
|
76
|
+
await cp(from, to, { recursive: true });
|
|
77
|
+
await rm(from, { force: true, recursive: true });
|
|
78
|
+
// The `.gitignore` entry is the surfaced top-level dir (`.vercel`/`.netlify`),
|
|
79
|
+
// never the moved sub-path — Vercel's own `.vercel/project.json` lives there
|
|
80
|
+
// too and must also be ignored.
|
|
81
|
+
return { from, ignore: `${rel.split("/")[0]}/`, moved: true, to };
|
|
82
|
+
};
|
package/src/deploy/robots.ts
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
2
|
+
import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
5
|
+
* Ordered mapping from config field to its `Content-Signal` token. The order
|
|
6
|
+
* fixes the emitted sequence (`search`, then `ai-input`, then `ai-train`).
|
|
7
|
+
*/
|
|
8
|
+
const SIGNAL_TOKENS: [keyof ContentSignalPolicy, string][] = [
|
|
9
|
+
["search", "search"],
|
|
10
|
+
["aiInput", "ai-input"],
|
|
11
|
+
["aiTrain", "ai-train"],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The `Content-Signal:` line declaring how crawlers may reuse the site, or null
|
|
16
|
+
* when the declaration is disabled (`contentSignals: false`). Otherwise every
|
|
17
|
+
* signal is emitted with its resolved yes/no value.
|
|
18
|
+
*/
|
|
19
|
+
const contentSignalLine = (signals: ContentSignals): string | null => {
|
|
20
|
+
if (!signals) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const tokens = SIGNAL_TOKENS.map(
|
|
24
|
+
([key, token]) => `${token}=${signals[key] ? "yes" : "no"}`
|
|
25
|
+
);
|
|
26
|
+
return `Content-Signal: ${tokens.join(", ")}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a robots.txt that allows all crawlers, declares any configured
|
|
31
|
+
* `Content-Signal` usage preferences, and points to the sitemap when one is
|
|
32
|
+
* available (a `site` is set and the sitemap is enabled). Returns null when
|
|
33
|
+
* robots generation is disabled.
|
|
7
34
|
*/
|
|
8
35
|
export const buildRobots = (project: BlumeProject): string | null => {
|
|
9
36
|
const { config } = project;
|
|
@@ -11,7 +38,13 @@ export const buildRobots = (project: BlumeProject): string | null => {
|
|
|
11
38
|
return null;
|
|
12
39
|
}
|
|
13
40
|
|
|
14
|
-
const lines = ["User-agent: *"
|
|
41
|
+
const lines = ["User-agent: *"];
|
|
42
|
+
const signal = contentSignalLine(config.seo.contentSignals);
|
|
43
|
+
if (signal) {
|
|
44
|
+
lines.push(signal);
|
|
45
|
+
}
|
|
46
|
+
lines.push("Allow: /");
|
|
47
|
+
|
|
15
48
|
const { site } = config.deployment;
|
|
16
49
|
if (site && config.seo.sitemap) {
|
|
17
50
|
lines.push("", `Sitemap: ${site.replace(/\/$/u, "")}/sitemap.xml`);
|
package/src/index.ts
CHANGED
|
@@ -22,8 +22,8 @@ export type {
|
|
|
22
22
|
FolderMetaFactory,
|
|
23
23
|
} from "./core/define-meta.ts";
|
|
24
24
|
export type { UIStrings } from "./core/i18n-ui.ts";
|
|
25
|
+
export type { BlumeConfig } from "./core/config-input.ts";
|
|
25
26
|
export type {
|
|
26
|
-
BlumeConfig,
|
|
27
27
|
FolderMeta,
|
|
28
28
|
HydrationMode,
|
|
29
29
|
ResolvedConfig,
|
package/src/markdown/index.ts
CHANGED
|
@@ -46,15 +46,16 @@ type HastPlugin = NonNullable<
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Hast plugins enabled by config. Inline `` `code`{:lang} `` highlighting is
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* always on: it only fires on an explicit trailing `{:lang}` marker, so plain
|
|
50
|
+
* inline code is untouched and there's nothing to opt out of. Self-linking
|
|
51
|
+
* heading anchors (`<h2>`–`<h6>` wrapped in an `<a>` to their own id) are on
|
|
52
|
+
* unless `markdown.headingAnchors` is `false`. Inline code runs first so the
|
|
53
|
+
* anchor wrap re-refs already-highlighted code.
|
|
52
54
|
*/
|
|
53
55
|
const blumeHastPlugins = (options: BlumeMarkdownOptions): HastPlugin[] => {
|
|
54
|
-
const plugins: HastPlugin[] = [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
56
|
+
const plugins: HastPlugin[] = [
|
|
57
|
+
inlineCodeHighlightPlugin() as unknown as HastPlugin,
|
|
58
|
+
];
|
|
58
59
|
if (options.headingAnchors !== false) {
|
|
59
60
|
plugins.push(headingAnchorPlugin() as unknown as HastPlugin);
|
|
60
61
|
}
|
|
@@ -204,8 +205,6 @@ export interface BlumeMarkdownOptions {
|
|
|
204
205
|
* On unless explicitly `false`.
|
|
205
206
|
*/
|
|
206
207
|
headingAnchors?: boolean;
|
|
207
|
-
/** Highlight inline `` `code`{:lang} `` snippets (`markdown.code.inline`). */
|
|
208
|
-
inline?: boolean;
|
|
209
208
|
}
|
|
210
209
|
|
|
211
210
|
/** Sätteri processor for plain `.md`, with Blume's curated feature set. */
|
|
@@ -215,38 +214,37 @@ export const blumeMarkdownProcessor = (options: BlumeMarkdownOptions = {}) =>
|
|
|
215
214
|
hastPlugins: blumeHastPlugins(options),
|
|
216
215
|
});
|
|
217
216
|
|
|
218
|
-
export
|
|
219
|
-
/** Enable KaTeX math parsing and rendering. */
|
|
220
|
-
math?: boolean;
|
|
221
|
-
}
|
|
217
|
+
export type BlumeMdxOptions = BlumeMarkdownOptions;
|
|
222
218
|
|
|
223
219
|
/**
|
|
224
220
|
* Sätteri MDX processor: Blume's feature set plus the MDAST plugins that target
|
|
225
221
|
* components — `package-install` → package-manager tabs, `:::note` →
|
|
226
|
-
* `<Callout>`,
|
|
227
|
-
*
|
|
228
|
-
* `.mdx` only (plain `.md` uses
|
|
229
|
-
*
|
|
222
|
+
* `<Callout>`, ` ```mermaid ` → a `<blume-mermaid>` element, and block math
|
|
223
|
+
* (`$$…$$`) → the `<Math>` component. Used as the `processor` for
|
|
224
|
+
* `@astrojs/mdx` so these apply to `.mdx` only (plain `.md` uses
|
|
225
|
+
* {@link blumeMarkdownProcessor}).
|
|
226
|
+
*
|
|
227
|
+
* Math is always on but block-only: `singleDollarTextMath: false` keeps a bare
|
|
228
|
+
* `$` (currency, shell, code) as literal text and only parses `$$…$$`. The
|
|
229
|
+
* generated runtime imports the `<Math>` component (and KaTeX's stylesheet) only
|
|
230
|
+
* when content actually uses `$$`, so a math-free site ships no KaTeX CSS.
|
|
230
231
|
*
|
|
231
232
|
* The plugins are modeled with minimal structural types; bridge them to
|
|
232
233
|
* Satteri's full `MdastPlugin` type at this single boundary.
|
|
233
234
|
*/
|
|
234
|
-
export const blumeMdxProcessor = (options: BlumeMdxOptions = {}) =>
|
|
235
|
-
|
|
236
|
-
packageInstallPlugin(),
|
|
237
|
-
directiveToCalloutPlugin(),
|
|
238
|
-
mermaidPlugin(),
|
|
239
|
-
];
|
|
240
|
-
if (options.math) {
|
|
241
|
-
plugins.push(mathPlugin());
|
|
242
|
-
}
|
|
243
|
-
return satteri({
|
|
235
|
+
export const blumeMdxProcessor = (options: BlumeMdxOptions = {}) =>
|
|
236
|
+
satteri({
|
|
244
237
|
features: {
|
|
245
238
|
...FEATURES,
|
|
246
239
|
directive: true,
|
|
247
|
-
|
|
240
|
+
// Block-only: `$$…$$` parses, a bare `$` stays literal text.
|
|
241
|
+
math: { singleDollarTextMath: false },
|
|
248
242
|
},
|
|
249
243
|
hastPlugins: blumeHastPlugins(options),
|
|
250
|
-
mdastPlugins:
|
|
244
|
+
mdastPlugins: [
|
|
245
|
+
packageInstallPlugin(),
|
|
246
|
+
directiveToCalloutPlugin(),
|
|
247
|
+
mermaidPlugin(),
|
|
248
|
+
mathPlugin(),
|
|
249
|
+
] as unknown as MdastPlugin[],
|
|
251
250
|
});
|
|
252
|
-
};
|
package/src/markdown/math.ts
CHANGED
|
@@ -8,8 +8,9 @@ interface MathNode extends MdastNode {
|
|
|
8
8
|
/**
|
|
9
9
|
* Satteri MDAST plugin that turns math nodes into Blume's `<Math>` component,
|
|
10
10
|
* which renders them with KaTeX at build time. Block math (`$$…$$`) becomes a
|
|
11
|
-
* block element
|
|
12
|
-
*
|
|
11
|
+
* block element. Blume runs the parser block-only (`singleDollarTextMath:
|
|
12
|
+
* false`), so a bare `$` stays literal and no `inlineMath` nodes are produced;
|
|
13
|
+
* the `inlineMath` visitor remains as a harmless safety net.
|
|
13
14
|
*/
|
|
14
15
|
export const mathPlugin = () => ({
|
|
15
16
|
inlineMath(node: MathNode, ctx: MdastVisitorContext) {
|
package/src/openapi/scalar.ts
CHANGED
|
@@ -61,7 +61,7 @@ const themeConfiguration = (
|
|
|
61
61
|
const accent = resolveAccent(config.theme);
|
|
62
62
|
const radius = resolveRadius(config.theme);
|
|
63
63
|
return {
|
|
64
|
-
customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent};--scalar-radius:${radius};}`,
|
|
64
|
+
customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent.light};--scalar-radius:${radius};}.dark-mode{--scalar-color-accent:${accent.dark};}`,
|
|
65
65
|
...darkModeConfig(config.theme.mode),
|
|
66
66
|
};
|
|
67
67
|
};
|
package/src/registry/eject.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
buildRuntimeData,
|
|
13
13
|
collectStaged,
|
|
14
14
|
detectNeedsReact,
|
|
15
|
+
detectUsesMath,
|
|
15
16
|
} from "../astro/generate.ts";
|
|
16
17
|
import { discoverIslands } from "../astro/islands.ts";
|
|
17
18
|
import { customOgRoutes, discoverPages, routeIsTaken } from "../astro/pages.ts";
|
|
@@ -102,19 +103,25 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
102
103
|
const exportPdf = config.export.pdf;
|
|
103
104
|
const exportEpub = config.export.epub;
|
|
104
105
|
|
|
105
|
-
const [
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
const [
|
|
107
|
+
pages,
|
|
108
|
+
needsReactRaw,
|
|
109
|
+
usesMath,
|
|
110
|
+
userTheme,
|
|
111
|
+
rawMarkdown,
|
|
112
|
+
islands,
|
|
113
|
+
examples,
|
|
114
|
+
] = await Promise.all([
|
|
115
|
+
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
116
|
+
detectNeedsReact(root),
|
|
117
|
+
detectUsesMath(root),
|
|
118
|
+
context.themeFile
|
|
119
|
+
? readFile(context.themeFile, "utf-8")
|
|
120
|
+
: Promise.resolve(""),
|
|
121
|
+
buildRawMarkdown(project),
|
|
122
|
+
discoverIslands(root),
|
|
123
|
+
discoverExamples(root, config.examples),
|
|
124
|
+
]);
|
|
118
125
|
// Island/example frameworks drive which Astro renderers the ejected config
|
|
119
126
|
// wires in; React also switches on for project `.tsx`/`.jsx` and Ask AI.
|
|
120
127
|
const frameworks = new Set<string>([
|
|
@@ -192,7 +199,7 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
192
199
|
askEnabled,
|
|
193
200
|
exportEpub,
|
|
194
201
|
exportPdf,
|
|
195
|
-
mathEnabled:
|
|
202
|
+
mathEnabled: usesMath,
|
|
196
203
|
needsReact,
|
|
197
204
|
}),
|
|
198
205
|
path: join(srcDir, "pages", "[...slug].astro"),
|
package/src/search/documents.ts
CHANGED
|
@@ -114,10 +114,15 @@ const buildCrumbIndex = (sidebar: NavNode[]): Map<string, Crumbs> => {
|
|
|
114
114
|
* Pass `includeWhenDisabled` to index pages on their content merits even when
|
|
115
115
|
* the search provider is `none` — used by the MCP server, which is a separate
|
|
116
116
|
* feature from on-page search.
|
|
117
|
+
*
|
|
118
|
+
* `content` selects the extraction: `"plain"` (default) strips Markdown to bare
|
|
119
|
+
* searchable text; `"markdown"` keeps the body's Markdown — code blocks, lists,
|
|
120
|
+
* headings — for Ask AI grounding, where fenced examples are often the answer
|
|
121
|
+
* and stripping them makes the model unable to cite content the docs do contain.
|
|
117
122
|
*/
|
|
118
123
|
export const buildSearchDocuments = async (
|
|
119
124
|
project: BlumeProject,
|
|
120
|
-
options?: { includeWhenDisabled?: boolean }
|
|
125
|
+
options?: { includeWhenDisabled?: boolean; content?: "markdown" | "plain" }
|
|
121
126
|
): Promise<SearchDocument[]> => {
|
|
122
127
|
const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
|
|
123
128
|
|
|
@@ -148,7 +153,9 @@ export const buildSearchDocuments = async (
|
|
|
148
153
|
indexable.map(async (route) => {
|
|
149
154
|
const page = pageById.get(route.id);
|
|
150
155
|
const raw = page ? await readEntryText(project, page) : "";
|
|
151
|
-
const
|
|
156
|
+
const source = raw ? matter(raw).content : "";
|
|
157
|
+
const body =
|
|
158
|
+
options?.content === "markdown" ? source.trim() : toPlainText(source);
|
|
152
159
|
const tags = page?.meta?.search?.tags;
|
|
153
160
|
const crumb = crumbs.get(route.path);
|
|
154
161
|
return {
|
package/src/theme/entry.ts
CHANGED
|
@@ -435,6 +435,10 @@ blume-diff {
|
|
|
435
435
|
font-size: 0.8125rem;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
.prose :where(td, th) :not(pre) > code {
|
|
439
|
+
white-space: nowrap;
|
|
440
|
+
}
|
|
441
|
+
|
|
438
442
|
blume-tabs pre,
|
|
439
443
|
.not-prose > div > pre {
|
|
440
444
|
background: var(--blume-code-background);
|
|
@@ -610,9 +614,9 @@ pre:has(.line.focused):hover .line:not(.focused) {
|
|
|
610
614
|
content: none;
|
|
611
615
|
}
|
|
612
616
|
|
|
613
|
-
/* Inline code highlighting
|
|
614
|
-
|
|
615
|
-
|
|
617
|
+
/* Inline code highlighting: Shiki colors the tokens of a \`code\`{:lang} snippet
|
|
618
|
+
via the same dual-theme CSS variables as fenced blocks, keeping the inline
|
|
619
|
+
pill background. Always on — it only fires on the trailing {:lang} marker. */
|
|
616
620
|
.prose code.blume-inline-code span {
|
|
617
621
|
color: var(--shiki-light);
|
|
618
622
|
}
|
package/src/theme/palette.ts
CHANGED
|
@@ -67,10 +67,12 @@ const themeRootCss = (
|
|
|
67
67
|
"--blume-action-foreground",
|
|
68
68
|
options.action ? "oklch(1 0 0)" : null
|
|
69
69
|
),
|
|
70
|
-
...cssToken("--blume-background", safeColorOrNull(theme.background)),
|
|
70
|
+
...cssToken("--blume-background", safeColorOrNull(theme.background?.light)),
|
|
71
71
|
...cssToken(
|
|
72
72
|
"--blume-background-image",
|
|
73
|
-
theme.backgroundImage
|
|
73
|
+
theme.backgroundImage?.light
|
|
74
|
+
? backgroundImageCss(theme.backgroundImage.light)
|
|
75
|
+
: null
|
|
74
76
|
),
|
|
75
77
|
` --blume-radius: ${options.radius};`,
|
|
76
78
|
]
|
|
@@ -96,11 +98,11 @@ const themeDarkCss = (
|
|
|
96
98
|
"--blume-action-foreground",
|
|
97
99
|
options.action ? "oklch(1 0 0)" : null
|
|
98
100
|
),
|
|
99
|
-
...cssToken("--blume-background", safeColorOrNull(theme.
|
|
101
|
+
...cssToken("--blume-background", safeColorOrNull(theme.background?.dark)),
|
|
100
102
|
...cssToken(
|
|
101
103
|
"--blume-background-image",
|
|
102
|
-
theme.
|
|
103
|
-
? backgroundImageCss(theme.
|
|
104
|
+
theme.backgroundImage?.dark
|
|
105
|
+
? backgroundImageCss(theme.backgroundImage.dark)
|
|
104
106
|
: null
|
|
105
107
|
),
|
|
106
108
|
].filter(Boolean);
|
|
@@ -111,12 +113,18 @@ ${tokens.join("\n")}
|
|
|
111
113
|
};
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
|
-
* Resolve the configured accent to
|
|
115
|
-
* preset; any other value is treated as a raw CSS color so
|
|
116
|
-
* arbitrary colors without a config change.
|
|
116
|
+
* Resolve the configured accent to per-mode CSS colors. A named accent
|
|
117
|
+
* resolves to its preset; any other value is treated as a raw CSS color so
|
|
118
|
+
* users can pass arbitrary colors without a config change. A string accent
|
|
119
|
+
* has already been normalized by the config schema to the same color for
|
|
120
|
+
* both modes.
|
|
117
121
|
*/
|
|
118
|
-
export const resolveAccent = (
|
|
119
|
-
|
|
122
|
+
export const resolveAccent = (
|
|
123
|
+
theme: ResolvedConfig["theme"]
|
|
124
|
+
): { dark: string; light: string } => ({
|
|
125
|
+
dark: presetOrColor(theme.accent.dark),
|
|
126
|
+
light: presetOrColor(theme.accent.light),
|
|
127
|
+
});
|
|
120
128
|
|
|
121
129
|
/** Resolve the configured radius preset to a CSS length. */
|
|
122
130
|
export const resolveRadius = (theme: ResolvedConfig["theme"]): string =>
|
|
@@ -128,17 +136,16 @@ export const resolveRadius = (theme: ResolvedConfig["theme"]): string =>
|
|
|
128
136
|
* arbitrary colors without a config change.
|
|
129
137
|
*/
|
|
130
138
|
export const buildThemeCss = (theme: ResolvedConfig["theme"]): string => {
|
|
131
|
-
const accent =
|
|
132
|
-
const accentDark = theme.accentDark ? presetOrColor(theme.accentDark) : null;
|
|
139
|
+
const accent = resolveAccent(theme);
|
|
133
140
|
const action = theme.action ? presetOrColor(theme.action) : null;
|
|
134
141
|
const radius = RADII[theme.radius];
|
|
135
142
|
const root = themeRootCss(theme, {
|
|
136
|
-
accent,
|
|
143
|
+
accent: accent.light,
|
|
137
144
|
action,
|
|
138
145
|
radius,
|
|
139
146
|
});
|
|
140
147
|
const dark = themeDarkCss(theme, {
|
|
141
|
-
accent:
|
|
148
|
+
accent: accent.dark,
|
|
142
149
|
action,
|
|
143
150
|
});
|
|
144
151
|
|