blume 1.4.3 → 1.5.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 +17 -0
- package/dist/cli/index.js +1621 -576
- package/dist/cli/index.js.map +109 -104
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +79 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +54 -29
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +152 -33
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/RootLayout.astro +18 -0
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +88 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +15 -3
- package/src/core/schema.ts +213 -67
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +30 -11
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
package/src/deploy/sitemap.ts
CHANGED
|
@@ -74,6 +74,37 @@ export const buildSitemapFiles = (
|
|
|
74
74
|
const base = siteRoot(site);
|
|
75
75
|
// Routes carry `basePath`; a `deployment.base` subdirectory is layered on top.
|
|
76
76
|
const deployBase = normalizeBasePath(project.config.deployment.base);
|
|
77
|
+
|
|
78
|
+
// Archived-version pages leave the sitemap when the version is noindexed,
|
|
79
|
+
// or when their canonical points at a still-existing latest equivalent —
|
|
80
|
+
// listing a URL whose canonical says "index the other page" invites the
|
|
81
|
+
// noindexed-page-in-sitemap incoherence Docusaurus is known for. A page
|
|
82
|
+
// that exists only in an archived version stays listed (self-canonical).
|
|
83
|
+
const { versions } = project.config;
|
|
84
|
+
const archivedById = new Map(
|
|
85
|
+
(versions?.archived ?? []).map((version) => [version.id, version])
|
|
86
|
+
);
|
|
87
|
+
const currentKeys = versions
|
|
88
|
+
? new Set(
|
|
89
|
+
project.graph.pages.flatMap((page) =>
|
|
90
|
+
page.version === "" ? [`${page.versionKey}\u0000${page.locale}`] : []
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
: null;
|
|
94
|
+
const archivedExcluded = (page: (typeof project.graph.pages)[number]) => {
|
|
95
|
+
if (!page.version) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
// A non-empty version always names a configured archived entry — that is
|
|
99
|
+
// the only way detection assigns one.
|
|
100
|
+
const archived = archivedById.get(page.version);
|
|
101
|
+
return Boolean(
|
|
102
|
+
archived?.noindex ||
|
|
103
|
+
(archived?.canonical === "latest" &&
|
|
104
|
+
currentKeys?.has(`${page.versionKey}\u0000${page.locale}`))
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
77
108
|
const seen = new Set<string>();
|
|
78
109
|
const urls: string[] = [];
|
|
79
110
|
const pushUrl = (route: string, lastModified?: string): void => {
|
|
@@ -93,7 +124,8 @@ export const buildSitemapFiles = (
|
|
|
93
124
|
page.meta.draft ||
|
|
94
125
|
page.meta.sidebar.hidden ||
|
|
95
126
|
page.meta.seo.noindex ||
|
|
96
|
-
ERROR_ROUTES.has(page.route)
|
|
127
|
+
ERROR_ROUTES.has(page.route) ||
|
|
128
|
+
archivedExcluded(page)
|
|
97
129
|
) {
|
|
98
130
|
continue;
|
|
99
131
|
}
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
export const ACCEPT_MARKDOWN_HEADER_VALUE =
|
|
25
25
|
"(.*,)?\\s*text/(x-)?markdown(\\s*[;,].*)?$";
|
|
26
26
|
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* A Build Output API route — the subset these helpers read and write. Parsed
|
|
29
|
+
* routes keep whatever other fields they carry at runtime; only these are
|
|
30
|
+
* typed.
|
|
31
|
+
*/
|
|
28
32
|
export interface VercelRoute {
|
|
29
33
|
continue?: boolean;
|
|
30
34
|
dest?: string;
|
|
@@ -32,9 +36,12 @@ export interface VercelRoute {
|
|
|
32
36
|
has?: { key?: string; type: string; value?: string }[];
|
|
33
37
|
headers?: Record<string, string>;
|
|
34
38
|
src?: string;
|
|
35
|
-
[key: string]: unknown;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/** Whether a parsed route field is a real string (the config is raw JSON). */
|
|
42
|
+
const isString = (value: string | undefined): value is string =>
|
|
43
|
+
typeof value === "string";
|
|
44
|
+
|
|
38
45
|
const ACCEPT_MARKDOWN_CONDITION: VercelRoute["has"] = [
|
|
39
46
|
{ key: "accept", type: "header", value: ACCEPT_MARKDOWN_HEADER_VALUE },
|
|
40
47
|
];
|
|
@@ -171,10 +178,10 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
|
171
178
|
) === true ||
|
|
172
179
|
(route.continue === true &&
|
|
173
180
|
route.headers?.vary === "Accept" &&
|
|
174
|
-
|
|
181
|
+
isString(route.src) &&
|
|
175
182
|
Object.keys(route).length === 3) ||
|
|
176
183
|
(route.continue === true &&
|
|
177
|
-
|
|
184
|
+
isString(route.headers?.link) &&
|
|
178
185
|
route.src === HOME_SRC &&
|
|
179
186
|
Object.keys(route).length === 3);
|
|
180
187
|
|
package/src/eval/report.ts
CHANGED
|
@@ -10,19 +10,19 @@ import { duration, money, seconds } from "../cli/report-format.ts";
|
|
|
10
10
|
import { countBySeverity } from "../core/diagnostics.ts";
|
|
11
11
|
import type { EvalResult, QuestionResult, QuestionStatus } from "./run.ts";
|
|
12
12
|
|
|
13
|
-
const GLYPH
|
|
13
|
+
const GLYPH = {
|
|
14
14
|
error: "!",
|
|
15
15
|
fail: "✖",
|
|
16
16
|
pass: "✔",
|
|
17
17
|
skip: "⊘",
|
|
18
|
-
}
|
|
18
|
+
} satisfies Record<QuestionStatus, string>;
|
|
19
19
|
|
|
20
|
-
const STATUS_COLOR
|
|
20
|
+
const STATUS_COLOR = {
|
|
21
21
|
error: colors.yellow,
|
|
22
22
|
fail: colors.red,
|
|
23
23
|
pass: colors.green,
|
|
24
24
|
skip: colors.dim,
|
|
25
|
-
}
|
|
25
|
+
} satisfies Record<QuestionStatus, ColorFunction>;
|
|
26
26
|
|
|
27
27
|
/** Longest id gets the room; everything shorter aligns to it. */
|
|
28
28
|
const ID_PAD = 28;
|
package/src/eval/run.ts
CHANGED
|
@@ -263,12 +263,12 @@ export const runEval = async (options: EvalRunOptions): Promise<EvalResult> => {
|
|
|
263
263
|
});
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
const counts
|
|
266
|
+
const counts = {
|
|
267
267
|
error: 0,
|
|
268
268
|
fail: 0,
|
|
269
269
|
pass: 0,
|
|
270
270
|
skip: 0,
|
|
271
|
-
}
|
|
271
|
+
} satisfies Record<QuestionStatus, number>;
|
|
272
272
|
for (const result of results) {
|
|
273
273
|
counts[result.status] += 1;
|
|
274
274
|
}
|
package/src/eval/schema.ts
CHANGED
|
@@ -17,7 +17,7 @@ const questionSchema = z.strictObject({
|
|
|
17
17
|
routes: z
|
|
18
18
|
.union([z.string(), z.array(z.string())])
|
|
19
19
|
.default([])
|
|
20
|
-
.transform((value) => (
|
|
20
|
+
.transform((value) => (Array.isArray(value) ? value : [value])),
|
|
21
21
|
severity: z.enum(["error", "warning"]).default("error"),
|
|
22
22
|
skip: z.boolean().default(false),
|
|
23
23
|
});
|
|
@@ -11,7 +11,7 @@ interface UrlNode extends MdastNode {
|
|
|
11
11
|
* `setProperty`, not by mutating the node object.
|
|
12
12
|
*/
|
|
13
13
|
interface MdastUrlContext {
|
|
14
|
-
setProperty: (node:
|
|
14
|
+
setProperty: (node: MdastNode, key: "url", value: string) => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -26,6 +26,10 @@ const ASSET_PATH = /\.[a-z0-9]+$/iu;
|
|
|
26
26
|
/** Strip any `#fragment`/`?query` so only the path is extension-tested. */
|
|
27
27
|
const pathOf = (url: string): string => url.replace(/[#?].*$/u, "");
|
|
28
28
|
|
|
29
|
+
/** Only a string URL can be rebased; MDAST allows null or absent urls. */
|
|
30
|
+
const isUrl = (url: string | null | undefined): url is string =>
|
|
31
|
+
typeof url === "string";
|
|
32
|
+
|
|
29
33
|
/**
|
|
30
34
|
* Satteri MDAST plugin that prepends the served-URL base — `deployment.base`
|
|
31
35
|
* layered over the site-wide `basePath` — to root-relative internal page links
|
|
@@ -38,11 +42,7 @@ const pathOf = (url: string): string => url.replace(/[#?].*$/u, "");
|
|
|
38
42
|
export const baseLinksPlugin = (deployBase: string, basePath: string) => {
|
|
39
43
|
const rebase = (node: UrlNode, ctx: MdastUrlContext): void => {
|
|
40
44
|
const { url } = node;
|
|
41
|
-
if (
|
|
42
|
-
typeof url === "string" &&
|
|
43
|
-
isInternalPath(url) &&
|
|
44
|
-
!ASSET_PATH.test(pathOf(url))
|
|
45
|
-
) {
|
|
45
|
+
if (isUrl(url) && isInternalPath(url) && !ASSET_PATH.test(pathOf(url))) {
|
|
46
46
|
const next = withComposedBasePath(deployBase, basePath, url);
|
|
47
47
|
if (next !== url) {
|
|
48
48
|
ctx.setProperty(node, "url", next);
|
|
@@ -21,7 +21,11 @@ const CALLOUT_TYPES = new Set([
|
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
23
|
/** Friendly aliases for the canonical Callout types. */
|
|
24
|
-
|
|
24
|
+
interface CalloutAliases {
|
|
25
|
+
[alias: string]: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ALIASES: CalloutAliases = {
|
|
25
29
|
caution: "warning",
|
|
26
30
|
error: "danger",
|
|
27
31
|
important: "note",
|
|
@@ -54,6 +58,8 @@ export const directiveToCalloutPlugin = () => ({
|
|
|
54
58
|
let title = node.attributes?.title ?? undefined;
|
|
55
59
|
|
|
56
60
|
// A leading `:::name[Label]` parses to a paragraph flagged `directiveLabel`.
|
|
61
|
+
// SAFETY: Satteri stamps `directiveLabel` on that paragraph's `data`; any
|
|
62
|
+
// other node reads undefined and fails the check.
|
|
57
63
|
const labelIndex = children.findIndex(
|
|
58
64
|
(child) =>
|
|
59
65
|
child.type === "paragraph" &&
|
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
import { satteriCollectHastText } from "@astrojs/markdown-satteri";
|
|
20
20
|
import GithubSlugger from "github-slugger";
|
|
21
21
|
|
|
22
|
+
/** A hast property value: an attribute primitive or a token list. */
|
|
23
|
+
type HastPropertyValue = string | number | boolean | (string | number)[];
|
|
24
|
+
|
|
22
25
|
/** A minimal hast node (avoids a hast type dependency). */
|
|
23
26
|
interface HastNode {
|
|
24
27
|
children?: HastNode[];
|
|
25
28
|
name?: string;
|
|
26
|
-
properties?: Record<string,
|
|
29
|
+
properties?: Record<string, HastPropertyValue>;
|
|
27
30
|
tagName?: string;
|
|
28
31
|
type: string;
|
|
29
32
|
value?: string;
|
|
@@ -31,8 +34,10 @@ interface HastNode {
|
|
|
31
34
|
|
|
32
35
|
/** The slice of Satteri's hast visitor context this plugin reads. */
|
|
33
36
|
interface HastContext {
|
|
34
|
-
data?: {
|
|
35
|
-
|
|
37
|
+
data?: {
|
|
38
|
+
astro?: { frontmatter?: Parameters<typeof satteriCollectHastText>[1] };
|
|
39
|
+
};
|
|
40
|
+
setProperty: (node: HastNode, key: string, value: HastPropertyValue) => void;
|
|
36
41
|
textContent: (node: HastNode) => string;
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -63,7 +68,7 @@ const containsAnchor = (node: HastNode): boolean => {
|
|
|
63
68
|
// page, but slug disambiguation must reset per document; the render-scoped
|
|
64
69
|
// `astro` data object is a stable, unique key for one render (entries are
|
|
65
70
|
// dropped once the render is collected, so this never leaks).
|
|
66
|
-
const FALLBACK_SCOPE
|
|
71
|
+
const FALLBACK_SCOPE = {};
|
|
67
72
|
const sluggers = new WeakMap<object, GithubSlugger>();
|
|
68
73
|
|
|
69
74
|
const sluggerFor = (ctx: HastContext): GithubSlugger => {
|
|
@@ -77,6 +82,10 @@ const sluggerFor = (ctx: HastContext): GithubSlugger => {
|
|
|
77
82
|
return slugger;
|
|
78
83
|
};
|
|
79
84
|
|
|
85
|
+
/** Whether a heading already carries a usable string `id`. */
|
|
86
|
+
const isStringId = (value: HastPropertyValue | undefined): value is string =>
|
|
87
|
+
typeof value === "string";
|
|
88
|
+
|
|
80
89
|
/** The slug for a heading, mirroring Satteri's `heading-ids` exactly. */
|
|
81
90
|
const slugFor = (
|
|
82
91
|
node: HastNode,
|
|
@@ -86,6 +95,8 @@ const slugFor = (
|
|
|
86
95
|
const rawText = ctx.textContent(node);
|
|
87
96
|
// `frontmatter`-interpolated MDX headings (`## {frontmatter.title}`) need the
|
|
88
97
|
// resolved value; the helper is the same one `heading-ids` defers to.
|
|
98
|
+
// SAFETY: HastNode is a structural subset of the hast element shape the
|
|
99
|
+
// helper walks (children/type/value), so the visited node always fits.
|
|
89
100
|
const text = rawText.includes("frontmatter")
|
|
90
101
|
? satteriCollectHastText(
|
|
91
102
|
node as Parameters<typeof satteriCollectHastText>[0],
|
|
@@ -93,7 +104,7 @@ const slugFor = (
|
|
|
93
104
|
)
|
|
94
105
|
: rawText;
|
|
95
106
|
const existingId = node.properties?.id;
|
|
96
|
-
return
|
|
107
|
+
return isStringId(existingId) ? existingId : slugger.slug(text);
|
|
97
108
|
};
|
|
98
109
|
|
|
99
110
|
/** Build the plugin. Wraps `<h2>`–`<h6>` in self-linking anchors. */
|
|
@@ -108,7 +119,7 @@ export const headingAnchorPlugin = (): HeadingAnchorPlugin => ({
|
|
|
108
119
|
if (!wrap) {
|
|
109
120
|
// Unwrapped headings (h1, an empty slug, or one that already links) still
|
|
110
121
|
// need the id so `heading-ids` adopts it instead of re-slugging.
|
|
111
|
-
if (
|
|
122
|
+
if (!isStringId(node.properties?.id)) {
|
|
112
123
|
ctx.setProperty(node, "id", slug);
|
|
113
124
|
}
|
|
114
125
|
return;
|
package/src/markdown/index.ts
CHANGED
|
@@ -52,6 +52,28 @@ type HastPlugin = NonNullable<
|
|
|
52
52
|
NonNullable<Parameters<typeof satteri>[0]>["hastPlugins"]
|
|
53
53
|
>[number];
|
|
54
54
|
|
|
55
|
+
/*
|
|
56
|
+
* Blume's plugins model only the node/context slices they touch (no hast or
|
|
57
|
+
* Satteri type dependency); these bridges are the single boundary where those
|
|
58
|
+
* minimal structural shapes meet the host pipeline's full plugin types.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
// SAFETY: Satteri drives plugins through the same visitor protocol Blume's
|
|
62
|
+
// minimal structural shapes model; they narrow the node/context types the
|
|
63
|
+
// visitor hooks receive, never widen them.
|
|
64
|
+
const asHastPlugin = (plugin: { name: string }): HastPlugin =>
|
|
65
|
+
plugin as HastPlugin;
|
|
66
|
+
|
|
67
|
+
// SAFETY: the same visitor-protocol bridge as `asHastPlugin`, for the mdast
|
|
68
|
+
// phase's plugins.
|
|
69
|
+
const asMdastPlugin = (plugin: { name: string }): MdastPlugin =>
|
|
70
|
+
plugin as MdastPlugin;
|
|
71
|
+
|
|
72
|
+
// SAFETY: Shiki calls only the hooks a transformer declares, and Blume's
|
|
73
|
+
// transformers type their hook parameters as the hast slices they touch.
|
|
74
|
+
const asShikiTransformer = (transformer: { name: string }): ShikiTransformer =>
|
|
75
|
+
transformer as ShikiTransformer;
|
|
76
|
+
|
|
55
77
|
/**
|
|
56
78
|
* Hast plugins enabled by config. Inline `` `code`{:lang} `` highlighting is
|
|
57
79
|
* always on: it only fires on an explicit trailing `{:lang}` marker, so plain
|
|
@@ -62,11 +84,11 @@ type HastPlugin = NonNullable<
|
|
|
62
84
|
*/
|
|
63
85
|
const blumeHastPlugins = (options: BlumeMarkdownOptions): HastPlugin[] => {
|
|
64
86
|
const plugins: HastPlugin[] = [
|
|
65
|
-
inlineCodeHighlightPlugin(options.codeThemes)
|
|
66
|
-
tableWrapPlugin()
|
|
87
|
+
asHastPlugin(inlineCodeHighlightPlugin(options.codeThemes)),
|
|
88
|
+
asHastPlugin(tableWrapPlugin()),
|
|
67
89
|
];
|
|
68
90
|
if (options.headingAnchors !== false) {
|
|
69
|
-
plugins.push(headingAnchorPlugin()
|
|
91
|
+
plugins.push(asHastPlugin(headingAnchorPlugin()));
|
|
70
92
|
}
|
|
71
93
|
return plugins;
|
|
72
94
|
};
|
|
@@ -101,29 +123,55 @@ export const blumeShikiTransformers = (
|
|
|
101
123
|
transformerMetaHighlight(),
|
|
102
124
|
];
|
|
103
125
|
if (options.icons !== false) {
|
|
104
|
-
transformers.push(languageIconTransformer()
|
|
126
|
+
transformers.push(asShikiTransformer(languageIconTransformer()));
|
|
105
127
|
}
|
|
106
128
|
// The fence-meta reader (title / line numbers) always runs last.
|
|
107
|
-
transformers.push(codeTitleTransformer()
|
|
129
|
+
transformers.push(asShikiTransformer(codeTitleTransformer()));
|
|
108
130
|
return transformers;
|
|
109
131
|
};
|
|
110
132
|
|
|
133
|
+
/** The value space of a hast element property. */
|
|
134
|
+
type HastPropertyValue =
|
|
135
|
+
| string
|
|
136
|
+
| number
|
|
137
|
+
| boolean
|
|
138
|
+
| (string | number)[]
|
|
139
|
+
| null
|
|
140
|
+
| undefined;
|
|
141
|
+
|
|
142
|
+
/** The `<pre>` slice the class/language transformers touch. */
|
|
143
|
+
interface PreElement {
|
|
144
|
+
properties: { class?: HastPropertyValue; dataLanguage?: HastPropertyValue };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const isStringProperty = (value: HastPropertyValue): value is string =>
|
|
148
|
+
typeof value === "string";
|
|
149
|
+
|
|
150
|
+
/** A Blume-local Shiki transformer: a name plus the `pre` hook it declares. */
|
|
151
|
+
interface PreTransformer {
|
|
152
|
+
name: string;
|
|
153
|
+
pre: (node: PreElement) => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
111
156
|
/**
|
|
112
157
|
* Tag the highlighted `<pre>` with `astro-code` (plus any extra classes) so the
|
|
113
158
|
* theme's code-block styles apply — `codeToHtml`'s bare output is `pre.shiki`,
|
|
114
159
|
* which the theme doesn't style.
|
|
115
160
|
*/
|
|
116
|
-
const astroCodeClassTransformer = (extra?: string): ShikiTransformer =>
|
|
117
|
-
|
|
161
|
+
const astroCodeClassTransformer = (extra?: string): ShikiTransformer => {
|
|
162
|
+
const transformer: PreTransformer = {
|
|
118
163
|
name: "blume:astro-code-class",
|
|
119
|
-
pre(node
|
|
120
|
-
const existing =
|
|
121
|
-
|
|
164
|
+
pre(node) {
|
|
165
|
+
const existing = isStringProperty(node.properties.class)
|
|
166
|
+
? node.properties.class
|
|
167
|
+
: "";
|
|
122
168
|
node.properties.class = `astro-code ${extra ?? ""} ${existing}`
|
|
123
169
|
.replaceAll(/\s+/gu, " ")
|
|
124
170
|
.trim();
|
|
125
171
|
},
|
|
126
|
-
}
|
|
172
|
+
};
|
|
173
|
+
return asShikiTransformer(transformer);
|
|
174
|
+
};
|
|
127
175
|
|
|
128
176
|
/**
|
|
129
177
|
* Tag the `<pre>` with `data-language` — raw `codeToHtml` omits it (unlike
|
|
@@ -132,13 +180,15 @@ const astroCodeClassTransformer = (extra?: string): ShikiTransformer =>
|
|
|
132
180
|
* fence would, while header-less panes (e.g. the Component source view) stay
|
|
133
181
|
* untouched.
|
|
134
182
|
*/
|
|
135
|
-
const languageAttrTransformer = (lang: string): ShikiTransformer =>
|
|
136
|
-
|
|
183
|
+
const languageAttrTransformer = (lang: string): ShikiTransformer => {
|
|
184
|
+
const transformer: PreTransformer = {
|
|
137
185
|
name: "blume:data-language",
|
|
138
|
-
pre(node
|
|
186
|
+
pre(node) {
|
|
139
187
|
node.properties.dataLanguage ??= lang;
|
|
140
188
|
},
|
|
141
|
-
}
|
|
189
|
+
};
|
|
190
|
+
return asShikiTransformer(transformer);
|
|
191
|
+
};
|
|
142
192
|
|
|
143
193
|
export interface HighlightCodeOptions extends BlumeShikiOptions {
|
|
144
194
|
/** Extra `<pre>` class names, e.g. `blume-source` for a height-capped pane. */
|
|
@@ -235,10 +285,9 @@ const blumeSharedMdastPlugins = (
|
|
|
235
285
|
): MdastPlugin[] =>
|
|
236
286
|
options.basePath || options.deployBase
|
|
237
287
|
? [
|
|
238
|
-
|
|
239
|
-
options.deployBase ?? "",
|
|
240
|
-
|
|
241
|
-
) as unknown as MdastPlugin,
|
|
288
|
+
asMdastPlugin(
|
|
289
|
+
baseLinksPlugin(options.deployBase ?? "", options.basePath ?? "")
|
|
290
|
+
),
|
|
242
291
|
]
|
|
243
292
|
: [];
|
|
244
293
|
|
|
@@ -278,10 +327,10 @@ export const blumeMdxProcessor = (options: BlumeMdxOptions = {}) =>
|
|
|
278
327
|
},
|
|
279
328
|
hastPlugins: blumeHastPlugins(options),
|
|
280
329
|
mdastPlugins: [
|
|
281
|
-
packageInstallPlugin(),
|
|
282
|
-
directiveToCalloutPlugin(),
|
|
283
|
-
mermaidPlugin(),
|
|
284
|
-
mathPlugin(),
|
|
330
|
+
asMdastPlugin(packageInstallPlugin()),
|
|
331
|
+
asMdastPlugin(directiveToCalloutPlugin()),
|
|
332
|
+
asMdastPlugin(mermaidPlugin()),
|
|
333
|
+
asMdastPlugin(mathPlugin()),
|
|
285
334
|
...blumeSharedMdastPlugins(options),
|
|
286
|
-
]
|
|
335
|
+
],
|
|
287
336
|
});
|
|
@@ -15,10 +15,19 @@
|
|
|
15
15
|
import { DEFAULT_CODE_THEMES } from "./themes.ts";
|
|
16
16
|
import type { CodeThemes } from "./themes.ts";
|
|
17
17
|
|
|
18
|
+
/** The value space of a hast element property (mirrors hast's `Properties`). */
|
|
19
|
+
type HastPropertyValue =
|
|
20
|
+
| string
|
|
21
|
+
| number
|
|
22
|
+
| boolean
|
|
23
|
+
| (string | number)[]
|
|
24
|
+
| null
|
|
25
|
+
| undefined;
|
|
26
|
+
|
|
18
27
|
/** A minimal hast node (avoids a hast type dependency). */
|
|
19
28
|
interface HastNode {
|
|
20
29
|
children?: HastNode[];
|
|
21
|
-
properties?: Record<string,
|
|
30
|
+
properties?: Record<string, HastPropertyValue>;
|
|
22
31
|
tagName?: string;
|
|
23
32
|
type: string;
|
|
24
33
|
value?: string;
|
|
@@ -71,7 +80,10 @@ type InlineHighlighter = (
|
|
|
71
80
|
// `import()` caches the module, so this dedupes Shiki across calls on its own.
|
|
72
81
|
const loadHighlighter = async (): Promise<InlineHighlighter> => {
|
|
73
82
|
const mod = await import("shiki");
|
|
74
|
-
|
|
83
|
+
// SAFETY: Shiki accepts arbitrary lang/theme strings at runtime (an unknown
|
|
84
|
+
// one rejects the promise, which the caller catches); only its bundled types
|
|
85
|
+
// constrain them to known ids, so the loose signature narrows nothing real.
|
|
86
|
+
return mod.codeToHast as InlineHighlighter;
|
|
75
87
|
};
|
|
76
88
|
|
|
77
89
|
/** Build the plugin. Highlights inline `` `code{:lang}` `` snippets. */
|
|
@@ -55,7 +55,11 @@ interface SimpleIcon {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Fence language (and common aliases) → icon. Unmapped languages get none. */
|
|
58
|
-
|
|
58
|
+
interface LanguageIcons {
|
|
59
|
+
[language: string]: SimpleIcon;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const LANGUAGE_ICONS: LanguageIcons = {
|
|
59
63
|
astro: siAstro,
|
|
60
64
|
bash: siGnubash,
|
|
61
65
|
c: siC,
|
|
@@ -119,7 +123,7 @@ const LANGUAGE_ICONS: Record<string, SimpleIcon> = {
|
|
|
119
123
|
/** A minimal hast node (avoids a hast type dependency). */
|
|
120
124
|
interface HastNode {
|
|
121
125
|
children?: HastNode[];
|
|
122
|
-
properties?: Record<string,
|
|
126
|
+
properties?: Record<string, boolean | number | string | string[] | undefined>;
|
|
123
127
|
tagName?: string;
|
|
124
128
|
type: string;
|
|
125
129
|
value?: string;
|
package/src/markdown/mdast.ts
CHANGED
|
@@ -5,15 +5,29 @@
|
|
|
5
5
|
* Satteri's real `MdastPlugin` type at a single boundary in `index.ts`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A property value on an MDAST node: primitives, nested nodes, and lists of
|
|
10
|
+
* either. Covers everything Blume's plugins read or build (positions, data
|
|
11
|
+
* flags, attribute lists) without admitting functions or class instances.
|
|
12
|
+
*/
|
|
13
|
+
export type MdastValue =
|
|
14
|
+
| string
|
|
15
|
+
| number
|
|
16
|
+
| boolean
|
|
17
|
+
| null
|
|
18
|
+
| undefined
|
|
19
|
+
| MdastValue[]
|
|
20
|
+
| { [key: string]: MdastValue };
|
|
21
|
+
|
|
8
22
|
/** The visitor context Blume's plugins use to mutate the tree. */
|
|
9
23
|
export interface MdastVisitorContext {
|
|
10
|
-
replaceNode: (node:
|
|
24
|
+
replaceNode: (node: MdastNode, replacement: MdastNode) => void;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
/** Any MDAST node, keyed loosely since we build a small subset by hand. */
|
|
14
28
|
export interface MdastNode {
|
|
15
29
|
type: string;
|
|
16
|
-
[key: string]:
|
|
30
|
+
[key: string]: MdastValue;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
/** Build an MDX JSX attribute. A `null` value renders as a boolean attribute. */
|
|
@@ -29,14 +43,14 @@ type JsxAttribute = ReturnType<typeof jsxAttribute>;
|
|
|
29
43
|
export const jsxFlowElement = (
|
|
30
44
|
name: string,
|
|
31
45
|
attributes: JsxAttribute[],
|
|
32
|
-
children:
|
|
46
|
+
children: MdastValue[]
|
|
33
47
|
) => ({ attributes, children, name, type: "mdxJsxFlowElement" });
|
|
34
48
|
|
|
35
49
|
/** Build an inline MDX JSX element (phrasing context). */
|
|
36
50
|
export const jsxTextElement = (
|
|
37
51
|
name: string,
|
|
38
52
|
attributes: JsxAttribute[],
|
|
39
|
-
children:
|
|
53
|
+
children: MdastValue[] = []
|
|
40
54
|
) => ({ attributes, children, name, type: "mdxJsxTextElement" });
|
|
41
55
|
|
|
42
56
|
/** Build a fenced code block node. */
|
|
@@ -14,12 +14,12 @@ export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
|
14
14
|
* (yarnpkg/berry#821), so global installs on the yarn tab honestly render
|
|
15
15
|
* npm's form, matching `ni`'s table.
|
|
16
16
|
*/
|
|
17
|
-
const AGENT_FOR
|
|
17
|
+
const AGENT_FOR = {
|
|
18
18
|
bun: "bun",
|
|
19
19
|
npm: "npm",
|
|
20
20
|
pnpm: "pnpm",
|
|
21
21
|
yarn: "yarn@berry",
|
|
22
|
-
}
|
|
22
|
+
} satisfies Record<PackageManager, Agent>;
|
|
23
23
|
|
|
24
24
|
/** Words that mark the input as an explicit command rather than a bare list. */
|
|
25
25
|
const MANAGER_PREFIXES = new Set(["bun", "bunx", "npm", "npx", "pnpm", "yarn"]);
|
|
@@ -137,14 +137,14 @@ const parseIntent = (input: string): Intent => {
|
|
|
137
137
|
};
|
|
138
138
|
|
|
139
139
|
/** The package-manager-detector command for each non-global operation. */
|
|
140
|
-
const COMMAND_FOR
|
|
140
|
+
const COMMAND_FOR = {
|
|
141
141
|
add: "add",
|
|
142
142
|
ci: "frozen",
|
|
143
143
|
exec: "execute",
|
|
144
144
|
install: "install",
|
|
145
145
|
remove: "uninstall",
|
|
146
146
|
run: "run",
|
|
147
|
-
}
|
|
147
|
+
} satisfies Record<Exclude<Operation, "create">, Command>;
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Render one manager's command for the given intent, via
|
|
@@ -188,9 +188,7 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
|
188
188
|
* manager. Accepts a bare package list (`react`) or a full command
|
|
189
189
|
* (`npm i -D typescript`, `npx astro add react`).
|
|
190
190
|
*/
|
|
191
|
-
export const toPackageCommands = (
|
|
192
|
-
input: string
|
|
193
|
-
): Record<PackageManager, string> => {
|
|
191
|
+
export const toPackageCommands = (input: string) => {
|
|
194
192
|
const intent = parseIntent(input);
|
|
195
193
|
const normalize = (command: string): string =>
|
|
196
194
|
command.replaceAll(WHITESPACE_RUN, " ").trim();
|
|
@@ -199,5 +197,5 @@ export const toPackageCommands = (
|
|
|
199
197
|
npm: normalize(buildCommand("npm", intent)),
|
|
200
198
|
pnpm: normalize(buildCommand("pnpm", intent)),
|
|
201
199
|
yarn: normalize(buildCommand("yarn", intent)),
|
|
202
|
-
}
|
|
200
|
+
} satisfies Record<PackageManager, string>;
|
|
203
201
|
};
|
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* containing any non-text content (an image, an icon) counts as non-empty.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
/** The value shapes hast allows on an element's `properties`. */
|
|
19
|
+
type HastPropertyValue = string | number | boolean | (string | number)[];
|
|
20
|
+
|
|
18
21
|
/** A minimal hast node (avoids a hast type dependency). */
|
|
19
22
|
interface HastNode {
|
|
20
23
|
children?: HastNode[];
|
|
21
|
-
properties?: Record<string,
|
|
24
|
+
properties?: Record<string, HastPropertyValue>;
|
|
22
25
|
tagName?: string;
|
|
23
26
|
type: string;
|
|
24
27
|
value?: string;
|
package/src/markdown/twoslash.ts
CHANGED
|
@@ -38,6 +38,8 @@ const require = createRequire(import.meta.url);
|
|
|
38
38
|
* never pays the TypeScript parse cost.
|
|
39
39
|
*/
|
|
40
40
|
export const blumeTwoslashTransformer = (): ShikiTransformer => {
|
|
41
|
+
// SAFETY: this resolves Blume's own pinned `typescript` dependency, whose
|
|
42
|
+
// CJS entry exports exactly the API namespace `typeof TS` describes.
|
|
41
43
|
const tsModule = require("typescript") as typeof TS;
|
|
42
44
|
const twoslasher = createTwoslasher({
|
|
43
45
|
// Match the stock transformer's default: fence snippets are authored
|