blume 1.4.3 → 1.5.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/CHANGELOG.md +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +1784 -633
- package/dist/cli/index.js.map +111 -106
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +80 -28
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +19 -3
- 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/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/configuration/theming.mdx +4 -2
- 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 +2 -1
- 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 +64 -34
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +181 -40
- 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/Fonts.astro +23 -3
- 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/PageLayout.astro +72 -3
- package/src/components/layout/ReferenceLayout.astro +2 -1
- package/src/components/layout/RootLayout.astro +20 -1
- 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 +89 -28
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +19 -3
- 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/last-modified.ts +49 -0
- 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 +26 -3
- package/src/core/schema.ts +214 -68
- 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 +45 -18
- 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 +33 -12
- 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/entry.ts +24 -2
- package/src/theme/fonts.ts +83 -7
- 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,13 @@ export interface VercelRoute {
|
|
|
32
36
|
has?: { key?: string; type: string; value?: string }[];
|
|
33
37
|
headers?: Record<string, string>;
|
|
34
38
|
src?: string;
|
|
35
|
-
|
|
39
|
+
status?: number;
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
/** Whether a parsed route field is a real string (the config is raw JSON). */
|
|
43
|
+
const isString = (value: string | undefined): value is string =>
|
|
44
|
+
typeof value === "string";
|
|
45
|
+
|
|
38
46
|
const ACCEPT_MARKDOWN_CONDITION: VercelRoute["has"] = [
|
|
39
47
|
{ key: "accept", type: "header", value: ACCEPT_MARKDOWN_HEADER_VALUE },
|
|
40
48
|
];
|
|
@@ -156,6 +164,21 @@ export const buildNegotiationRoutes = (
|
|
|
156
164
|
/** The `src` of the injected homepage `Link` header route. */
|
|
157
165
|
const HOME_SRC = "^/$";
|
|
158
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Permanent redirect from any trailing-slash URL to its slashless twin, so
|
|
169
|
+
* `/docs/` and `/docs` don't serve as duplicate URLs (canonicals, sitemap, and
|
|
170
|
+
* hreflang all use the slashless form; the root `/` is untouched — `.+`
|
|
171
|
+
* requires a non-empty path). Spliced into the main phase before `handle:
|
|
172
|
+
* "filesystem"`, after the Markdown rewrites, so an agent's `Accept:
|
|
173
|
+
* text/markdown` request on a slashed URL still rewrites without the extra
|
|
174
|
+
* hop. Vercel carries the query string over to the `Location` target itself.
|
|
175
|
+
*/
|
|
176
|
+
export const TRAILING_SLASH_REDIRECT: VercelRoute = {
|
|
177
|
+
headers: { Location: "/$1" },
|
|
178
|
+
src: "^/(.+)/$",
|
|
179
|
+
status: 308,
|
|
180
|
+
};
|
|
181
|
+
|
|
159
182
|
/**
|
|
160
183
|
* Whether a route is one this module previously injected, so re-injection
|
|
161
184
|
* replaces rather than duplicates. Rewrites are identified by their `accept`
|
|
@@ -171,12 +194,14 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
|
171
194
|
) === true ||
|
|
172
195
|
(route.continue === true &&
|
|
173
196
|
route.headers?.vary === "Accept" &&
|
|
174
|
-
|
|
197
|
+
isString(route.src) &&
|
|
175
198
|
Object.keys(route).length === 3) ||
|
|
176
199
|
(route.continue === true &&
|
|
177
|
-
|
|
200
|
+
isString(route.headers?.link) &&
|
|
178
201
|
route.src === HOME_SRC &&
|
|
179
|
-
Object.keys(route).length === 3)
|
|
202
|
+
Object.keys(route).length === 3) ||
|
|
203
|
+
(route.status === TRAILING_SLASH_REDIRECT.status &&
|
|
204
|
+
route.src === TRAILING_SLASH_REDIRECT.src);
|
|
180
205
|
|
|
181
206
|
/**
|
|
182
207
|
* Splice the negotiation routes into a Build Output `config.json`, plus — when
|
|
@@ -186,10 +211,12 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
|
186
211
|
* rides on the prerendered homepage response. `contentTypeOverrides` maps static-dir
|
|
187
212
|
* relative paths to media types via the Build Output `overrides` field — the
|
|
188
213
|
* platform's mechanism for extensionless static files (e.g. the Web Bot Auth
|
|
189
|
-
* signature directory).
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
214
|
+
* signature directory). The trailing-slash 308 redirect is always spliced in
|
|
215
|
+
* alongside, so slashed duplicates of every page collapse onto the canonical
|
|
216
|
+
* slashless URL. Returns the updated JSON text (tab-indented, like the
|
|
217
|
+
* adapter's own output), or `null` when there is nowhere safe to splice: an
|
|
218
|
+
* unparsable config, no `routes` array, or no `handle: "filesystem"` marker
|
|
219
|
+
* to anchor the splice.
|
|
193
220
|
*/
|
|
194
221
|
export const injectNegotiationRoutes = (
|
|
195
222
|
configText: string,
|
|
@@ -199,13 +226,6 @@ export const injectNegotiationRoutes = (
|
|
|
199
226
|
homeTokens?: number
|
|
200
227
|
): string | null => {
|
|
201
228
|
const overrideEntries = Object.entries(contentTypeOverrides ?? {});
|
|
202
|
-
if (
|
|
203
|
-
routePaths.length === 0 &&
|
|
204
|
-
!homeLinkHeader &&
|
|
205
|
-
overrideEntries.length === 0
|
|
206
|
-
) {
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
229
|
let config: {
|
|
210
230
|
overrides?: Record<string, { contentType?: string; path?: string }>;
|
|
211
231
|
routes?: VercelRoute[];
|
|
@@ -243,8 +263,15 @@ export const injectNegotiationRoutes = (
|
|
|
243
263
|
}
|
|
244
264
|
// Headers first: `continue` routes accumulate, so a request the rewrite
|
|
245
265
|
// route then terminates (Markdown negotiation on the homepage) still carries
|
|
246
|
-
// the Link header.
|
|
247
|
-
|
|
266
|
+
// the Link header. The trailing-slash redirect goes last so a slashed URL's
|
|
267
|
+
// Markdown negotiation still rewrites directly instead of bouncing.
|
|
268
|
+
routes.splice(
|
|
269
|
+
filesystemIndex,
|
|
270
|
+
0,
|
|
271
|
+
...headerRoutes,
|
|
272
|
+
...rewriteRoutes,
|
|
273
|
+
TRAILING_SLASH_REDIRECT
|
|
274
|
+
);
|
|
248
275
|
config.routes = routes;
|
|
249
276
|
return `${JSON.stringify(config, null, "\t")}\n`;
|
|
250
277
|
};
|
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;
|