blume 1.4.2 → 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 +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -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 +116 -15
- package/dist/types/core/sources/types.d.ts +11 -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 +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- 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/sources.mdx +1 -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 +13 -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 +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- 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 +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- 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/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- 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/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- 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/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- 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 +8 -5
- package/src/components/layout/search/types.ts +45 -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/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -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 +74 -4
- 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/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- 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 +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- 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 +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- 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 +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
package/src/deploy/sitemap.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../astro/pages.ts";
|
|
9
9
|
import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
10
10
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
11
|
+
import { siteRoot } from "../core/site-url.ts";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Astro's reserved error routes. A user-authored override (`pages/404.astro`,
|
|
@@ -29,22 +30,81 @@ const lastmodTag = (value: string | undefined): string => {
|
|
|
29
30
|
: `<lastmod>${date.toISOString().slice(0, 10)}</lastmod>`;
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
/** One emitted sitemap artifact: its dist-root filename and XML body. */
|
|
34
|
+
export interface SitemapFile {
|
|
35
|
+
name: string;
|
|
36
|
+
xml: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The sitemaps.org cap on `<url>` entries in a single file. Beyond it,
|
|
41
|
+
* `sitemap.xml` becomes a sitemap index pointing at numbered chunk files —
|
|
42
|
+
* search engines reject an oversized urlset outright.
|
|
43
|
+
*/
|
|
44
|
+
const URLS_PER_FILE = 50_000;
|
|
45
|
+
|
|
46
|
+
const renderUrlset = (
|
|
47
|
+
urls: string[]
|
|
48
|
+
): string => `<?xml version="1.0" encoding="UTF-8"?>
|
|
49
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
50
|
+
${urls.join("\n")}
|
|
51
|
+
</urlset>
|
|
52
|
+
`;
|
|
53
|
+
|
|
32
54
|
/**
|
|
33
|
-
* Build
|
|
55
|
+
* Build the sitemap files from the route manifest plus the routes the manifest
|
|
34
56
|
* can't see: custom `.astro` pages (most importantly a custom landing `/`) and
|
|
35
57
|
* the generated `/changelog` index. Returns null when the sitemap is disabled
|
|
36
58
|
* or no `site` is configured (absolute URLs are required for a valid sitemap).
|
|
37
59
|
* Drafts, hidden, and `noindex` pages are excluded.
|
|
60
|
+
*
|
|
61
|
+
* Sites within the per-file URL cap get the single classic `sitemap.xml`;
|
|
62
|
+
* larger sites get `sitemap.xml` as a sitemap index over numbered
|
|
63
|
+
* `sitemap-N.xml` chunks, all served from the same directory robots.txt
|
|
64
|
+
* already points at.
|
|
38
65
|
*/
|
|
39
|
-
export const
|
|
66
|
+
export const buildSitemapFiles = (
|
|
67
|
+
project: BlumeProject
|
|
68
|
+
): SitemapFile[] | null => {
|
|
40
69
|
const { site } = project.config.deployment;
|
|
41
70
|
if (!(site && project.config.seo.sitemap)) {
|
|
42
71
|
return null;
|
|
43
72
|
}
|
|
44
73
|
|
|
45
|
-
const base = site
|
|
74
|
+
const base = siteRoot(site);
|
|
46
75
|
// Routes carry `basePath`; a `deployment.base` subdirectory is layered on top.
|
|
47
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
|
+
|
|
48
108
|
const seen = new Set<string>();
|
|
49
109
|
const urls: string[] = [];
|
|
50
110
|
const pushUrl = (route: string, lastModified?: string): void => {
|
|
@@ -64,7 +124,8 @@ export const buildSitemap = (project: BlumeProject): string | null => {
|
|
|
64
124
|
page.meta.draft ||
|
|
65
125
|
page.meta.sidebar.hidden ||
|
|
66
126
|
page.meta.seo.noindex ||
|
|
67
|
-
ERROR_ROUTES.has(page.route)
|
|
127
|
+
ERROR_ROUTES.has(page.route) ||
|
|
128
|
+
archivedExcluded(page)
|
|
68
129
|
) {
|
|
69
130
|
continue;
|
|
70
131
|
}
|
|
@@ -87,9 +148,29 @@ export const buildSitemap = (project: BlumeProject): string | null => {
|
|
|
87
148
|
}
|
|
88
149
|
urls.sort();
|
|
89
150
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
151
|
+
if (urls.length <= URLS_PER_FILE) {
|
|
152
|
+
return [{ name: "sitemap.xml", xml: renderUrlset(urls) }];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const chunks: SitemapFile[] = [];
|
|
156
|
+
const references: string[] = [];
|
|
157
|
+
for (let start = 0; start < urls.length; start += URLS_PER_FILE) {
|
|
158
|
+
const name = `sitemap-${chunks.length + 1}.xml`;
|
|
159
|
+
chunks.push({
|
|
160
|
+
name,
|
|
161
|
+
xml: renderUrlset(urls.slice(start, start + URLS_PER_FILE)),
|
|
162
|
+
});
|
|
163
|
+
// Chunks sit next to sitemap.xml, so their URLs layer the same deployment
|
|
164
|
+
// base robots.txt uses for the index.
|
|
165
|
+
const loc = escapeXml(
|
|
166
|
+
encodeURI(`${base}${withBasePath(deployBase, `/${name}`)}`)
|
|
167
|
+
);
|
|
168
|
+
references.push(` <sitemap><loc>${loc}</loc></sitemap>`);
|
|
169
|
+
}
|
|
170
|
+
const index = `<?xml version="1.0" encoding="UTF-8"?>
|
|
171
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
172
|
+
${references.join("\n")}
|
|
173
|
+
</sitemapindex>
|
|
94
174
|
`;
|
|
175
|
+
return [{ name: "sitemap.xml", xml: index }, ...chunks];
|
|
95
176
|
};
|
|
@@ -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/agents.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
|
|
3
|
+
import spawn from "cross-spawn";
|
|
4
4
|
import { join } from "pathe";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
@@ -48,7 +48,6 @@ export interface HeadlessResult {
|
|
|
48
48
|
|
|
49
49
|
export interface HeadlessOptions {
|
|
50
50
|
cwd: string;
|
|
51
|
-
platform?: NodeJS.Platform;
|
|
52
51
|
prompt: string;
|
|
53
52
|
timeoutMs: number;
|
|
54
53
|
}
|
|
@@ -58,6 +57,15 @@ export interface HeadlessOptions {
|
|
|
58
57
|
* cmd.exe newline quoting alike), stdout and stderr captured, SIGTERM at the
|
|
59
58
|
* deadline with a SIGKILL follow-up. Resolves with the captured result;
|
|
60
59
|
* rejects only when the executable cannot be spawned at all (ENOENT).
|
|
60
|
+
*
|
|
61
|
+
* Spawned through cross-spawn, which runs npm's `.cmd` shims on Windows with
|
|
62
|
+
* its own cmd.exe escaping — under the old `shell: true` launch, cmd.exe was
|
|
63
|
+
* free to mangle the JSON carried by codex's `-c` arguments, and a missing
|
|
64
|
+
* executable surfaced as exit code 9009 instead of an ENOENT rejection.
|
|
65
|
+
* execa was evaluated and rejected here: it waits for the stdio streams to
|
|
66
|
+
* close, and the `exit`-beats-`close` resolution below exists precisely
|
|
67
|
+
* because a killed agent's own children (an MCP server, a shell) hold the
|
|
68
|
+
* pipes open past the SIGTERM.
|
|
61
69
|
*/
|
|
62
70
|
export const runAgentHeadless = (
|
|
63
71
|
bin: string,
|
|
@@ -66,23 +74,18 @@ export const runAgentHeadless = (
|
|
|
66
74
|
): Promise<HeadlessResult> =>
|
|
67
75
|
// oxlint-disable-next-line promise/avoid-new -- adapt spawn's event callbacks
|
|
68
76
|
new Promise((resolve, reject) => {
|
|
69
|
-
const platform = options.platform ?? process.platform;
|
|
70
|
-
// npm installs agent CLIs as `.cmd` shims on Windows, which Node refuses
|
|
71
|
-
// to spawn without a shell. Arguments are plain flags and absolute paths,
|
|
72
|
-
// so shell interpolation has nothing to mangle.
|
|
73
77
|
const child = spawn(bin, args, {
|
|
74
78
|
cwd: options.cwd,
|
|
75
|
-
shell: platform === "win32",
|
|
76
79
|
stdio: ["pipe", "pipe", "pipe"],
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
let stdout = "";
|
|
80
83
|
let stderr = "";
|
|
81
84
|
let timedOut = false;
|
|
82
|
-
child.stdout
|
|
85
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
83
86
|
stdout += chunk.toString("utf-8");
|
|
84
87
|
});
|
|
85
|
-
child.stderr
|
|
88
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
86
89
|
stderr += chunk.toString("utf-8");
|
|
87
90
|
});
|
|
88
91
|
|
|
@@ -112,7 +115,7 @@ export const runAgentHeadless = (
|
|
|
112
115
|
}
|
|
113
116
|
});
|
|
114
117
|
|
|
115
|
-
child.stdin
|
|
118
|
+
child.stdin?.end(options.prompt);
|
|
116
119
|
});
|
|
117
120
|
|
|
118
121
|
/** The spawn signature `runEval` accepts, injectable for tests. */
|
package/src/eval/report.ts
CHANGED
|
@@ -6,40 +6,27 @@ import type { ColorFunction } from "consola/utils";
|
|
|
6
6
|
import { join, relative } from "pathe";
|
|
7
7
|
|
|
8
8
|
import { AGENTS } from "../audit/agent.ts";
|
|
9
|
+
import { duration, money, seconds } from "../cli/report-format.ts";
|
|
9
10
|
import { countBySeverity } from "../core/diagnostics.ts";
|
|
10
11
|
import type { EvalResult, QuestionResult, QuestionStatus } from "./run.ts";
|
|
11
12
|
|
|
12
|
-
const GLYPH
|
|
13
|
+
const GLYPH = {
|
|
13
14
|
error: "!",
|
|
14
15
|
fail: "✖",
|
|
15
16
|
pass: "✔",
|
|
16
17
|
skip: "⊘",
|
|
17
|
-
}
|
|
18
|
+
} satisfies Record<QuestionStatus, string>;
|
|
18
19
|
|
|
19
|
-
const STATUS_COLOR
|
|
20
|
+
const STATUS_COLOR = {
|
|
20
21
|
error: colors.yellow,
|
|
21
22
|
fail: colors.red,
|
|
22
23
|
pass: colors.green,
|
|
23
24
|
skip: colors.dim,
|
|
24
|
-
}
|
|
25
|
+
} satisfies Record<QuestionStatus, ColorFunction>;
|
|
25
26
|
|
|
26
27
|
/** Longest id gets the room; everything shorter aligns to it. */
|
|
27
28
|
const ID_PAD = 28;
|
|
28
29
|
|
|
29
|
-
const seconds = (ms: number): string => `${(ms / 1000).toFixed(1)}s`;
|
|
30
|
-
|
|
31
|
-
const money = (cost: number | undefined): string =>
|
|
32
|
-
cost === undefined ? "" : `$${cost.toFixed(2)}`;
|
|
33
|
-
|
|
34
|
-
const duration = (ms: number): string => {
|
|
35
|
-
if (ms < 60_000) {
|
|
36
|
-
return seconds(ms);
|
|
37
|
-
}
|
|
38
|
-
const minutes = Math.floor(ms / 60_000);
|
|
39
|
-
const rest = Math.round((ms % 60_000) / 1000);
|
|
40
|
-
return `${minutes}m ${rest}s`;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
30
|
/** One question's progress/report line: glyph, id, status, score, time, cost. */
|
|
44
31
|
export const questionLine = (result: QuestionResult): string => {
|
|
45
32
|
const color = STATUS_COLOR[result.status];
|
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;
|