blume 2.1.3 → 2.2.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/AGENTS.md +1 -1
- package/CHANGELOG.md +134 -0
- package/README.md +4 -4
- package/dist/cli/{chunk-y4msrwm9.js → chunk-080mfch6.js} +15 -15
- package/dist/cli/{chunk-gfc5fqvf.js → chunk-1ch3ct3r.js} +18 -18
- package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-1ch3ct3r.js.map} +1 -1
- package/dist/cli/{chunk-32khwcb6.js → chunk-2ssh60qh.js} +42 -69
- package/dist/cli/{chunk-32khwcb6.js.map → chunk-2ssh60qh.js.map} +9 -10
- package/dist/cli/{chunk-9qzgg6mj.js → chunk-34wap3ed.js} +3 -3
- package/dist/cli/{chunk-k3gr1nm9.js → chunk-40vk8kgf.js} +13 -16
- package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-40vk8kgf.js.map} +3 -3
- package/dist/cli/chunk-487b95qy.js +329 -0
- package/dist/cli/chunk-487b95qy.js.map +13 -0
- package/dist/cli/chunk-4zxh25hc.js +157 -0
- package/dist/cli/chunk-4zxh25hc.js.map +10 -0
- package/dist/cli/chunk-69rjf786.js +146 -0
- package/dist/cli/chunk-69rjf786.js.map +11 -0
- package/dist/cli/{chunk-dqhg54mr.js → chunk-9h8mh2tk.js} +3 -3
- package/dist/cli/{chunk-dqhg54mr.js.map → chunk-9h8mh2tk.js.map} +2 -2
- package/dist/cli/{chunk-bqxaqn5p.js → chunk-9jqmq5hv.js} +8 -8
- package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-9jqmq5hv.js.map} +1 -1
- package/dist/cli/{chunk-dv2z0rzf.js → chunk-aafhwx74.js} +38 -13
- package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-aafhwx74.js.map} +5 -4
- package/dist/cli/{chunk-desvm72z.js → chunk-b680sv30.js} +22 -26
- package/dist/cli/chunk-b680sv30.js.map +13 -0
- package/dist/cli/{chunk-0w6hs479.js → chunk-beb0z983.js} +30 -18
- package/dist/cli/chunk-beb0z983.js.map +10 -0
- package/dist/cli/{chunk-1f882900.js → chunk-c5bjxq8v.js} +2 -2
- package/dist/cli/chunk-e1n41w9m.js +155 -0
- package/dist/cli/chunk-e1n41w9m.js.map +11 -0
- package/dist/cli/{chunk-sc93apfs.js → chunk-etckvbrh.js} +17 -17
- package/dist/cli/{chunk-sc93apfs.js.map → chunk-etckvbrh.js.map} +2 -2
- package/dist/cli/{chunk-bxwhtvw8.js → chunk-f260jdrn.js} +5 -5
- package/dist/cli/{chunk-74wfhv79.js → chunk-fnxgtjeq.js} +1 -1
- package/dist/cli/{chunk-ms9z66nz.js → chunk-gwy55cx3.js} +2 -2
- package/dist/cli/{chunk-ms9z66nz.js.map → chunk-gwy55cx3.js.map} +2 -2
- package/dist/cli/{chunk-dhwtqnnd.js → chunk-gxhcf2hs.js} +2 -2
- package/dist/cli/{chunk-tfycddbt.js → chunk-h0qwdbem.js} +2 -2
- package/dist/cli/{chunk-nbyvgnnm.js → chunk-hktq4j5r.js} +109 -49
- package/dist/cli/chunk-hktq4j5r.js.map +36 -0
- package/dist/cli/{chunk-twy7psdz.js → chunk-hn3g2b89.js} +14 -10
- package/dist/cli/chunk-hn3g2b89.js.map +10 -0
- package/dist/cli/{chunk-j1x92mx9.js → chunk-j4nfgn2j.js} +1 -1
- package/dist/cli/{chunk-dpy8qdjf.js → chunk-k45sq16q.js} +74 -183
- package/dist/cli/chunk-k45sq16q.js.map +18 -0
- package/dist/cli/{chunk-5cmts6dv.js → chunk-kmswjxj9.js} +7 -7
- package/dist/cli/{chunk-5cmts6dv.js.map → chunk-kmswjxj9.js.map} +1 -1
- package/dist/cli/{chunk-36bxs9c7.js → chunk-nqna8dzp.js} +27 -23
- package/dist/cli/chunk-nqna8dzp.js.map +10 -0
- package/dist/cli/{chunk-a57yh0h9.js → chunk-p834rkcn.js} +7 -7
- package/dist/cli/{chunk-a57yh0h9.js.map → chunk-p834rkcn.js.map} +1 -1
- package/dist/cli/{chunk-60hz265r.js → chunk-ppa6mg4d.js} +24 -11
- package/dist/cli/chunk-ppa6mg4d.js.map +10 -0
- package/dist/cli/{chunk-s0bdnc7v.js → chunk-q1wwrxzw.js} +5 -4
- package/dist/cli/chunk-q1wwrxzw.js.map +10 -0
- package/dist/cli/{chunk-ave1wcbr.js → chunk-qcyxkj44.js} +1 -1
- package/dist/cli/{chunk-124fvmzq.js → chunk-r5nm9w0c.js} +11183 -8806
- package/dist/cli/{chunk-124fvmzq.js.map → chunk-r5nm9w0c.js.map} +82 -66
- package/dist/cli/{chunk-ckrcm08h.js → chunk-rtvgmg2j.js} +48 -62
- package/dist/cli/{chunk-ckrcm08h.js.map → chunk-rtvgmg2j.js.map} +4 -5
- package/dist/cli/{chunk-297ajdcv.js → chunk-t2h8pwy1.js} +6 -6
- package/dist/cli/{chunk-297ajdcv.js.map → chunk-t2h8pwy1.js.map} +3 -3
- package/dist/cli/{chunk-gxtg6ed2.js → chunk-x60e15st.js} +21 -21
- package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-x60e15st.js.map} +3 -3
- package/dist/cli/{chunk-58bt3hag.js → chunk-x7bh6d9j.js} +4 -4
- package/dist/cli/{chunk-hvcxng5f.js → chunk-ydqzkhjn.js} +6 -6
- package/dist/cli/{chunk-kdpzw38a.js → chunk-yg8ckc3g.js} +95 -7
- package/dist/cli/chunk-yg8ckc3g.js.map +11 -0
- package/dist/cli/{chunk-0pmb4tr4.js → chunk-zxd5jrgx.js} +1205 -222
- package/dist/cli/chunk-zxd5jrgx.js.map +44 -0
- package/dist/cli/index.js +78 -42
- package/dist/cli/index.js.map +7 -6
- package/dist/types/ai/changelog-components.d.ts +18 -0
- package/dist/types/ai/changelog-markdown.d.ts +12 -0
- package/dist/types/ai/openapi-components.d.ts +7 -7
- package/dist/types/ai/operation-details.d.ts +39 -0
- package/dist/types/ai/relative-links.d.ts +3 -1
- package/dist/types/ai/serializers.d.ts +6 -4
- package/dist/types/analytics/head.d.ts +4 -2
- package/dist/types/analytics/script.d.ts +8 -2
- package/dist/types/components/content/youtube.d.ts +20 -0
- package/dist/types/components/layout/nav-utils.d.ts +8 -0
- package/dist/types/components/openapi/example-panels.d.ts +33 -0
- package/dist/types/components/openapi/helpers.d.ts +53 -11
- package/dist/types/components/openapi/snippets.d.ts +7 -1
- package/dist/types/core/alert-diagnostics.d.ts +10 -0
- package/dist/types/core/builtin-tags.d.ts +16 -0
- package/dist/types/core/changelog-list.d.ts +48 -0
- package/dist/types/core/closest-match.d.ts +6 -0
- package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
- package/dist/types/core/code-fences.d.ts +8 -1
- package/dist/types/core/config-input.d.ts +38 -6
- package/dist/types/core/content-assets.d.ts +16 -18
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/diagnostics.d.ts +14 -0
- package/dist/types/core/directive-diagnostics.d.ts +22 -8
- package/dist/types/core/former-routes.d.ts +14 -0
- package/dist/types/core/github.d.ts +5 -4
- package/dist/types/core/heading-markers.d.ts +65 -2
- package/dist/types/core/hidden-pages.d.ts +3 -0
- package/dist/types/core/includes.d.ts +23 -0
- package/dist/types/core/last-modified.d.ts +18 -6
- package/dist/types/core/links.d.ts +42 -2
- package/dist/types/core/load-module.d.ts +19 -4
- package/dist/types/core/manifest.d.ts +14 -1
- package/dist/types/core/markdown-mirrors.d.ts +22 -0
- package/dist/types/core/mcp-clients.d.ts +47 -0
- package/dist/types/core/mdx-elements.d.ts +32 -0
- package/dist/types/core/mdx-syntax.d.ts +100 -0
- package/dist/types/core/meta-diagnostics.d.ts +7 -0
- package/dist/types/core/meta.d.ts +32 -5
- package/dist/types/core/nav-diagnostics.d.ts +16 -2
- package/dist/types/core/navigation.d.ts +13 -0
- package/dist/types/core/ordering-prefix.d.ts +6 -0
- package/dist/types/core/pinned-ids.d.ts +23 -0
- package/dist/types/core/project-graph.d.ts +9 -1
- package/dist/types/core/redirect-patterns.d.ts +18 -0
- package/dist/types/core/relative-files.d.ts +31 -0
- package/dist/types/core/schema.d.ts +322 -11
- package/dist/types/core/sources/normalize.d.ts +101 -9
- package/dist/types/core/sources/types.d.ts +16 -0
- package/dist/types/core/static-files.d.ts +10 -0
- package/dist/types/core/static-host.d.ts +42 -0
- package/dist/types/core/svg-images.d.ts +26 -0
- package/dist/types/core/syntax-diagnostics.d.ts +14 -0
- package/dist/types/core/types.d.ts +42 -1
- package/dist/types/deploy/generated-files.d.ts +41 -0
- package/dist/types/deploy/redirects.d.ts +21 -7
- package/dist/types/markdown/code-title.d.ts +9 -5
- package/dist/types/markdown/directives.d.ts +8 -0
- package/dist/types/markdown/fence-language.d.ts +51 -0
- package/dist/types/markdown/fence-meta.d.ts +5 -0
- package/dist/types/markdown/github-alerts.d.ts +35 -0
- package/dist/types/openapi/asyncapi.d.ts +6 -5
- package/dist/types/openapi/checks.d.ts +10 -5
- package/dist/types/openapi/model.d.ts +55 -8
- package/dist/types/openapi/parse.d.ts +4 -1
- package/dist/types/ratelimit/index.d.ts +2 -0
- package/dist/types/ratelimit/memory.d.ts +1 -1
- package/dist/types/ratelimit/schema.d.ts +40 -0
- package/dist/types/ratelimit/unkey.d.ts +48 -0
- package/dist/types/sources/filesystem.d.ts +14 -4
- package/dist/types/sources/mdx-remote.d.ts +6 -3
- package/dist/types/sources/registry.d.ts +3 -3
- package/dist/types/theme/color.d.ts +33 -0
- package/dist/types/theme/palette.d.ts +122 -0
- package/docs/01-quickstart.mdx +1 -1
- package/docs/02-deployment.mdx +24 -8
- package/docs/04-migrating.mdx +16 -4
- package/docs/advanced/changelog.mdx +27 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +1 -1
- package/docs/cli/doctor.mdx +2 -2
- package/docs/cli/evals.mdx +1 -1
- package/docs/cli/index.mdx +4 -2
- package/docs/cli/validate.mdx +10 -7
- package/docs/configuration/analytics.mdx +2 -0
- package/docs/configuration/customization.mdx +3 -1
- package/docs/configuration/index.mdx +7 -6
- package/docs/configuration/rate-limiting.mdx +20 -1
- package/docs/configuration/theming.mdx +5 -1
- package/docs/content/components.mdx +26 -6
- package/docs/content/frontmatter.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/includes.mdx +6 -2
- package/docs/content/index.mdx +11 -1
- package/docs/content/islands.mdx +2 -0
- package/docs/content/meta.mdx +9 -3
- package/docs/content/navigation.mdx +14 -6
- package/docs/content/sources/obsidian.mdx +1 -1
- package/docs/content/sources/remote-mdx.mdx +3 -1
- package/docs/content/syntax.mdx +76 -8
- package/docs/content/variables.mdx +3 -1
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/json-api.mdx +1 -1
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/discoverability/markdown.mdx +2 -2
- package/docs/discoverability/mcp.mdx +40 -8
- package/docs/discoverability/open-graph.mdx +1 -1
- package/docs/discoverability/sitemap-and-robots.mdx +1 -1
- package/docs/references/graphql.mdx +2 -0
- package/docs/references/openapi.mdx +45 -7
- package/package.json +4 -4
- package/skills/blume/SKILL.md +2 -2
- package/skills/blume-migrate/SKILL.md +51 -29
- package/skills/blume-migrate/references/docsify.md +262 -0
- package/skills/blume-migrate/references/docus.md +298 -0
- package/skills/blume-migrate/references/docusaurus.md +5 -5
- package/skills/blume-migrate/references/fern.md +327 -0
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/gitbook.md +267 -0
- package/skills/blume-migrate/references/github-wiki.md +259 -0
- package/skills/blume-migrate/references/jekyll.md +253 -0
- package/skills/blume-migrate/references/mdbook.md +256 -0
- package/skills/blume-migrate/references/mintlify.md +5 -5
- package/skills/blume-migrate/references/mkdocs.md +236 -0
- package/skills/blume-migrate/references/monorepo.md +20 -8
- package/skills/blume-migrate/references/nextra.md +5 -5
- package/skills/blume-migrate/references/readme.md +257 -0
- package/skills/blume-migrate/references/redocly.md +198 -0
- package/skills/blume-migrate/references/starlight.md +3 -3
- package/skills/blume-migrate/references/vitepress.md +204 -0
- package/skills/blume-migrate/references/vuepress.md +271 -0
- package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
- package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
- package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
- package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
- package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
- package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
- package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
- package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
- package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
- package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
- package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
- package/src/ai/changelog-components.ts +24 -0
- package/src/ai/changelog-markdown.ts +45 -41
- package/src/ai/component-markdown.ts +22 -9
- package/src/ai/llms.ts +36 -5
- package/src/ai/markdown.ts +2 -2
- package/src/ai/openapi-components.ts +25 -9
- package/src/ai/operation-details.ts +208 -0
- package/src/ai/relative-links.ts +48 -8
- package/src/ai/serializers.ts +7 -6
- package/src/ai/site-skill.ts +6 -2
- package/src/analytics/head.ts +11 -5
- package/src/analytics/script.ts +13 -3
- package/src/astro/generate.ts +47 -11
- package/src/astro/index.ts +1 -0
- package/src/astro/integration.ts +27 -1
- package/src/astro/mdx-source-errors.ts +46 -0
- package/src/astro/templates.ts +93 -189
- package/src/audit/checks/redirects.ts +5 -1
- package/src/audit/redirects.ts +36 -19
- package/src/audit/report.ts +22 -9
- package/src/audit/run.ts +12 -28
- package/src/audit/terms.ts +23 -0
- package/src/audit/url.ts +2 -2
- package/src/cli/build-failure.ts +256 -7
- package/src/cli/command-meta.ts +1 -1
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.ts +12 -3
- package/src/cli/commands/check.ts +13 -2
- package/src/cli/commands/dev.ts +1 -1
- package/src/cli/commands/doctor.ts +4 -2
- package/src/cli/commands/init.ts +3 -3
- package/src/cli/commands/preview.ts +17 -1
- package/src/cli/commands/validate.ts +22 -10
- package/src/cli/prepare.ts +1 -1
- package/src/cli/preview-pages.ts +161 -0
- package/src/cli/required-secrets.ts +0 -1
- package/src/cli/unknown-flags.ts +1 -42
- package/src/components/code-copy.ts +51 -0
- package/src/components/code-language.ts +75 -0
- package/src/components/content/AccordionItem.astro +16 -1
- package/src/components/content/Callout.astro +11 -1
- package/src/components/content/Changelog.astro +189 -0
- package/src/components/content/CodeBlock.astro +8 -11
- package/src/components/content/Tabs.astro +16 -2
- package/src/components/content/YouTube.astro +6 -4
- package/src/components/content/code-block.ts +33 -0
- package/src/components/content/content-strings.ts +5 -1
- package/src/components/content/inline-markdown.ts +35 -8
- package/src/components/content/youtube.ts +49 -7
- package/src/components/layout/Analytics.astro +5 -1
- package/src/components/layout/NavTree.astro +8 -3
- package/src/components/layout/PageActions.astro +81 -23
- package/src/components/layout/RootLayout.astro +35 -32
- package/src/components/layout/SiteFooter.astro +31 -8
- package/src/components/layout/nav-utils.ts +14 -0
- package/src/components/layout/search/pagefind.ts +2 -1
- package/src/components/openapi/Operation.astro +4 -0
- package/src/components/openapi/PanelTabs.astro +9 -3
- package/src/components/openapi/RequestBody.astro +42 -8
- package/src/components/openapi/RequestPanel.astro +34 -53
- package/src/components/openapi/example-panels.ts +110 -0
- package/src/components/openapi/helpers.ts +195 -20
- package/src/components/openapi/operation-model.ts +63 -26
- package/src/components/openapi/request.ts +13 -0
- package/src/components/openapi/snippets.ts +14 -2
- package/src/core/alert-diagnostics.ts +89 -0
- package/src/core/builtin-tags.ts +76 -0
- package/src/core/changelog-list.ts +87 -0
- package/src/core/closest-match.ts +41 -0
- package/src/core/code-fence-diagnostics.ts +267 -0
- package/src/core/code-fences.ts +43 -7
- package/src/core/component-diagnostics.ts +65 -12
- package/src/core/config-input.ts +39 -5
- package/src/core/config.ts +46 -1
- package/src/core/content-assets.ts +64 -144
- package/src/core/data.ts +10 -3
- package/src/core/diagnostics.ts +36 -3
- package/src/core/directive-diagnostics.ts +265 -47
- package/src/core/former-routes.ts +46 -0
- package/src/core/git-ignored.ts +53 -0
- package/src/core/github.ts +5 -4
- package/src/core/heading-markers.ts +156 -2
- package/src/core/hidden-pages.ts +64 -0
- package/src/core/includes.ts +181 -41
- package/src/core/last-modified.ts +47 -10
- package/src/core/links.ts +438 -83
- package/src/core/load-module.ts +98 -9
- package/src/core/manifest.ts +12 -9
- package/src/core/markdown-mirrors.ts +52 -0
- package/src/core/mcp-clients.ts +87 -0
- package/src/core/mdx-elements.ts +153 -0
- package/src/core/mdx-syntax.ts +377 -0
- package/src/core/meta-diagnostics.ts +134 -0
- package/src/core/meta.ts +154 -55
- package/src/core/nav-diagnostics.ts +41 -4
- package/src/core/navigation.ts +70 -10
- package/src/core/ordering-prefix.ts +22 -2
- package/src/core/pinned-ids.ts +141 -0
- package/src/core/project-graph.ts +221 -29
- package/src/core/redirect-patterns.ts +41 -0
- package/src/core/relative-files.ts +145 -0
- package/src/core/schema.ts +110 -6
- package/src/core/sources/mdx-remote.ts +75 -14
- package/src/core/sources/normalize.ts +885 -179
- package/src/core/sources/notion.ts +5 -4
- package/src/core/sources/obsidian.ts +2 -5
- package/src/core/sources/types.ts +17 -0
- package/src/core/static-files.ts +40 -0
- package/src/core/static-host.ts +125 -0
- package/src/core/svg-images.ts +98 -0
- package/src/core/syntax-diagnostics.ts +491 -0
- package/src/core/types.ts +43 -1
- package/src/deploy/artifacts.ts +2 -28
- package/src/deploy/generated-files.ts +128 -0
- package/src/deploy/platforms/vercel.ts +25 -1
- package/src/deploy/redirects.ts +43 -16
- package/src/deploy/sitemap.ts +2 -1
- package/src/markdown/base-links.ts +87 -6
- package/src/markdown/code-title.ts +29 -13
- package/src/markdown/content-assets.ts +210 -0
- package/src/markdown/directives.ts +69 -5
- package/src/markdown/fence-language.ts +106 -0
- package/src/markdown/fence-meta.ts +42 -0
- package/src/markdown/github-alerts.ts +172 -0
- package/src/markdown/heading-anchors.ts +265 -56
- package/src/markdown/include.ts +30 -3
- package/src/markdown/index.ts +145 -40
- package/src/markdown/prompt-text.ts +95 -0
- package/src/markdown/relative-links.ts +37 -2
- package/src/markdown/route-snapshot.ts +22 -12
- package/src/markdown/svg-fallback.ts +51 -0
- package/src/markdown/title-heading.ts +134 -0
- package/src/migrate/migrate.ts +127 -2
- package/src/og/card.ts +14 -4
- package/src/openapi/asyncapi.ts +14 -6
- package/src/openapi/checks.ts +160 -15
- package/src/openapi/model.ts +172 -33
- package/src/openapi/parse.ts +13 -4
- package/src/openapi/render-mdx.ts +55 -9
- package/src/openapi/source.ts +87 -6
- package/src/ratelimit/index.ts +2 -0
- package/src/ratelimit/memory.ts +1 -1
- package/src/ratelimit/runtime.ts +81 -0
- package/src/ratelimit/schema.ts +2 -0
- package/src/ratelimit/unkey.ts +61 -0
- package/src/registry/eject.ts +0 -1
- package/src/registry/registry.ts +1 -1
- package/src/search/documents.ts +10 -2
- package/src/sources/filesystem.ts +28 -3
- package/src/sources/mdx-remote.ts +7 -15
- package/src/theme/color.ts +14 -0
- package/src/theme/entry.ts +22 -1
- package/src/theme/palette.ts +74 -1
- package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
- package/dist/cli/chunk-0w6hs479.js.map +0 -10
- package/dist/cli/chunk-150jk7km.js +0 -52
- package/dist/cli/chunk-150jk7km.js.map +0 -10
- package/dist/cli/chunk-36bxs9c7.js.map +0 -10
- package/dist/cli/chunk-53jqw8z0.js +0 -89
- package/dist/cli/chunk-53jqw8z0.js.map +0 -10
- package/dist/cli/chunk-60hz265r.js.map +0 -10
- package/dist/cli/chunk-desvm72z.js.map +0 -14
- package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
- package/dist/cli/chunk-es8h0v3j.js +0 -441
- package/dist/cli/chunk-es8h0v3j.js.map +0 -11
- package/dist/cli/chunk-kdpzw38a.js.map +0 -11
- package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
- package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
- package/dist/cli/chunk-twy7psdz.js.map +0 -10
- package/dist/cli/chunk-ydp3065p.js +0 -64
- package/dist/cli/chunk-ydp3065p.js.map +0 -10
- /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-080mfch6.js.map} +0 -0
- /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-34wap3ed.js.map} +0 -0
- /package/dist/cli/{chunk-1f882900.js.map → chunk-c5bjxq8v.js.map} +0 -0
- /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-f260jdrn.js.map} +0 -0
- /package/dist/cli/{chunk-74wfhv79.js.map → chunk-fnxgtjeq.js.map} +0 -0
- /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-gxhcf2hs.js.map} +0 -0
- /package/dist/cli/{chunk-tfycddbt.js.map → chunk-h0qwdbem.js.map} +0 -0
- /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-j4nfgn2j.js.map} +0 -0
- /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-qcyxkj44.js.map} +0 -0
- /package/dist/cli/{chunk-58bt3hag.js.map → chunk-x7bh6d9j.js.map} +0 -0
- /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-ydqzkhjn.js.map} +0 -0
package/src/core/builtin-tags.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const BUILTIN_MDX_TAGS = new Set<string>([
|
|
|
14
14
|
"Callout",
|
|
15
15
|
"Card",
|
|
16
16
|
"CardGroup",
|
|
17
|
+
"Changelog",
|
|
17
18
|
"CodeBlock",
|
|
18
19
|
"CodeGroup",
|
|
19
20
|
"Color",
|
|
@@ -48,3 +49,78 @@ export const BUILTIN_MDX_TAGS = new Set<string>([
|
|
|
48
49
|
"Visibility",
|
|
49
50
|
"YouTube",
|
|
50
51
|
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The props each built-in that shows its children takes, keyed by MDX tag. A
|
|
55
|
+
* childless use with any other prop renders without the content its author
|
|
56
|
+
* meant it to carry (VitePress writes `<Badge type="tip" text="beta" />`, and
|
|
57
|
+
* Blume's Badge shows only its children), which the unknown-prop diagnostic
|
|
58
|
+
* reports. Each list is the component's own `Props` (or what it reads from
|
|
59
|
+
* `Astro.props`); a test checks them against the `.astro` files.
|
|
60
|
+
*/
|
|
61
|
+
export const BUILTIN_CHILD_PROPS: ReadonlyMap<string, readonly string[]> =
|
|
62
|
+
new Map([
|
|
63
|
+
["Accordion", []],
|
|
64
|
+
["AccordionItem", ["defaultOpen", "description", "icon", "id", "title"]],
|
|
65
|
+
[
|
|
66
|
+
"Badge",
|
|
67
|
+
[
|
|
68
|
+
"class",
|
|
69
|
+
"className",
|
|
70
|
+
"color",
|
|
71
|
+
"disabled",
|
|
72
|
+
"icon",
|
|
73
|
+
"shape",
|
|
74
|
+
"size",
|
|
75
|
+
"stroke",
|
|
76
|
+
"tooltip",
|
|
77
|
+
"variant",
|
|
78
|
+
],
|
|
79
|
+
],
|
|
80
|
+
["Callout", ["color", "icon", "title", "type"]],
|
|
81
|
+
[
|
|
82
|
+
"Card",
|
|
83
|
+
[
|
|
84
|
+
"arrow",
|
|
85
|
+
"color",
|
|
86
|
+
"cta",
|
|
87
|
+
"horizontal",
|
|
88
|
+
"href",
|
|
89
|
+
"icon",
|
|
90
|
+
"img",
|
|
91
|
+
"title",
|
|
92
|
+
"type",
|
|
93
|
+
],
|
|
94
|
+
],
|
|
95
|
+
["CardGroup", ["cols"]],
|
|
96
|
+
["CodeBlock", ["code", "icons", "lang", "title"]],
|
|
97
|
+
["CodeGroup", ["dropdown"]],
|
|
98
|
+
["Column", []],
|
|
99
|
+
["Columns", ["cols"]],
|
|
100
|
+
["Expandable", ["defaultOpen", "title"]],
|
|
101
|
+
["Frame", ["caption", "hint"]],
|
|
102
|
+
["Panel", ["title"]],
|
|
103
|
+
["Prompt", ["actions", "description"]],
|
|
104
|
+
["RequestExample", ["dropdown"]],
|
|
105
|
+
["ResponseExample", ["dropdown"]],
|
|
106
|
+
["Step", ["icon", "title"]],
|
|
107
|
+
["Steps", ["titleSize"]],
|
|
108
|
+
["Tab", ["class", "icon", "id", "style", "title"]],
|
|
109
|
+
[
|
|
110
|
+
"Tabs",
|
|
111
|
+
[
|
|
112
|
+
"borderBottom",
|
|
113
|
+
"defaultTabIndex",
|
|
114
|
+
"dropdown",
|
|
115
|
+
"hash",
|
|
116
|
+
"inline",
|
|
117
|
+
"param",
|
|
118
|
+
"sync",
|
|
119
|
+
"syncKey",
|
|
120
|
+
],
|
|
121
|
+
],
|
|
122
|
+
["Tile", ["description", "href", "title"]],
|
|
123
|
+
["Tooltip", ["cta", "headline", "href", "tip"]],
|
|
124
|
+
["View", ["class", "icon", "title"]],
|
|
125
|
+
["Visibility", ["for"]],
|
|
126
|
+
]);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy the changelog index lists by, shared by the surfaces that build
|
|
3
|
+
* it: the `<Changelog />` component (reading Astro collection entries) and
|
|
4
|
+
* `ai/changelog-markdown.ts` (reading the build-time page graph for the
|
|
5
|
+
* `/changelog.md` mirror and MCP `get_page`).
|
|
6
|
+
*
|
|
7
|
+
* The data sources genuinely differ — a collection entry on the Astro side, a
|
|
8
|
+
* `PageRecord` on the Node side — so what lives here is the *decisions*, not
|
|
9
|
+
* the access: which date a row carries, what order rows come in, and how they
|
|
10
|
+
* fall into year groups. Keeping them in one place is what stops the rendered
|
|
11
|
+
* page and the Markdown an agent reads from drifting apart.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A changelog entry's date, from `date` or the `changelog.date` it may live
|
|
16
|
+
* under instead. Front matter hands either a `Date` (an unquoted YAML date) or
|
|
17
|
+
* a string, and an unparseable value is treated as no date at all rather than
|
|
18
|
+
* failing the build — a malformed date costs the row its stamp and its year
|
|
19
|
+
* group, not the page.
|
|
20
|
+
*/
|
|
21
|
+
export const parseChangelogDate = (
|
|
22
|
+
value: Date | string | null | undefined
|
|
23
|
+
): Date | null => {
|
|
24
|
+
if (value === null || value === undefined) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
28
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Newest first. An undated entry sorts as the epoch, so undated rows collect
|
|
33
|
+
* at the end of the list (and, through {@link groupChangelogYears}, into a
|
|
34
|
+
* final group with no year label).
|
|
35
|
+
*/
|
|
36
|
+
export const compareChangelogDates = (a: Date | null, b: Date | null): number =>
|
|
37
|
+
(b?.getTime() ?? 0) - (a?.getTime() ?? 0);
|
|
38
|
+
|
|
39
|
+
/** One year's run of rows. `year` is `null` for the undated group. */
|
|
40
|
+
export interface ChangelogYearGroup<T> {
|
|
41
|
+
items: T[];
|
|
42
|
+
year: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Consecutive rows sharing a year, in list order, so the index reads as a year
|
|
47
|
+
* rail beside its rows. Runs are consecutive rather than keyed, which matters
|
|
48
|
+
* only for a list that isn't sorted by date: such a list keeps its order and
|
|
49
|
+
* repeats the year heading instead of silently regrouping around it.
|
|
50
|
+
*
|
|
51
|
+
* `yearOf` returns the label the surface wants — a locale-formatted year on the
|
|
52
|
+
* rendered page, a plain number in Markdown — or `null` for an undated row.
|
|
53
|
+
*/
|
|
54
|
+
export const groupChangelogYears = <T>(
|
|
55
|
+
items: T[],
|
|
56
|
+
yearOf: (item: T) => string | null
|
|
57
|
+
): ChangelogYearGroup<T>[] => {
|
|
58
|
+
const groups: ChangelogYearGroup<T>[] = [];
|
|
59
|
+
for (const item of items) {
|
|
60
|
+
const year = yearOf(item);
|
|
61
|
+
const last = groups.at(-1);
|
|
62
|
+
if (last && last.year === year) {
|
|
63
|
+
last.items.push(item);
|
|
64
|
+
} else {
|
|
65
|
+
groups.push({ items: [item], year });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return groups;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Make each row's anchor unique. Repeated labels slug to the same id (two
|
|
73
|
+
* entries with neither a title nor a version both fall back to "update"), so
|
|
74
|
+
* the later ones take a `-2`, `-3`, … suffix and the first keeps the plain
|
|
75
|
+
* slug — every row ends up with its own hash anchor.
|
|
76
|
+
*/
|
|
77
|
+
export const uniqueAnchorIds = (ids: string[]): string[] => {
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
return ids.map((id) => {
|
|
80
|
+
let unique = id;
|
|
81
|
+
for (let n = 2; seen.has(unique); n += 1) {
|
|
82
|
+
unique = `${id}-${n}`;
|
|
83
|
+
}
|
|
84
|
+
seen.add(unique);
|
|
85
|
+
return unique;
|
|
86
|
+
});
|
|
87
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Edit distance between two short strings (insertions, deletions, swaps of one letter). */
|
|
2
|
+
const distance = (from: string, to: string): number => {
|
|
3
|
+
let previous = Array.from({ length: to.length + 1 }, (_, index) => index);
|
|
4
|
+
for (const [row, fromChar] of [...from].entries()) {
|
|
5
|
+
const current = [row + 1];
|
|
6
|
+
for (const [column, toChar] of [...to].entries()) {
|
|
7
|
+
current.push(
|
|
8
|
+
Math.min(
|
|
9
|
+
(previous[column + 1] ?? 0) + 1,
|
|
10
|
+
(current[column] ?? 0) + 1,
|
|
11
|
+
(previous[column] ?? 0) + (fromChar === toChar ? 0 : 1)
|
|
12
|
+
)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
previous = current;
|
|
16
|
+
}
|
|
17
|
+
return previous[to.length] ?? 0;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The candidate closest to `name`, when it's close enough to be the likely
|
|
22
|
+
* intent: at most two edits away, and fewer edits than half the candidate's
|
|
23
|
+
* length, so a short typo doesn't "suggest" an unrelated short word.
|
|
24
|
+
*/
|
|
25
|
+
export const closestMatch = (
|
|
26
|
+
name: string,
|
|
27
|
+
candidates: readonly string[]
|
|
28
|
+
): string | undefined => {
|
|
29
|
+
let best: { candidate: string; edits: number } | undefined;
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
const edits = distance(name, candidate);
|
|
32
|
+
if (
|
|
33
|
+
edits <= 2 &&
|
|
34
|
+
edits * 2 < candidate.length &&
|
|
35
|
+
(best === undefined || edits < best.edits)
|
|
36
|
+
) {
|
|
37
|
+
best = { candidate, edits };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return best?.candidate;
|
|
41
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { Nodes } from "mdast";
|
|
2
|
+
import { markdownToMdast, mdxToMdast } from "satteri";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MARKDOWN_BODY_FEATURES,
|
|
6
|
+
MDX_BODY_FEATURES,
|
|
7
|
+
} from "../markdown/features.ts";
|
|
8
|
+
import { isKnownLanguage, normalizeFence } from "../markdown/fence-language.ts";
|
|
9
|
+
import {
|
|
10
|
+
metaTokens,
|
|
11
|
+
RUST_LANGUAGES,
|
|
12
|
+
RUST_META_KEYWORDS,
|
|
13
|
+
} from "../markdown/fence-meta.ts";
|
|
14
|
+
import { strippedLineOffset } from "./sources/normalize.ts";
|
|
15
|
+
import type { SourceEntry } from "./sources/types.ts";
|
|
16
|
+
import type { Diagnostic } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
type Code = Extract<Nodes, { type: "code" }>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fence languages that render without a Shiki grammar on purpose: `math`,
|
|
22
|
+
* which Astro never highlights, and `package-install`, which `.mdx` turns
|
|
23
|
+
* into install tabs (and the docs describe as a plain block in `.md`).
|
|
24
|
+
*/
|
|
25
|
+
const BLUME_LANGUAGES: ReadonlySet<string> = new Set([
|
|
26
|
+
"math",
|
|
27
|
+
"package-install",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** A fence problem at a line of the parsed text, before it's placed in a file. */
|
|
31
|
+
interface Finding {
|
|
32
|
+
code: string;
|
|
33
|
+
line: number;
|
|
34
|
+
message: string;
|
|
35
|
+
suggestion: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A fence option another docs tool reads and Blume doesn't. */
|
|
39
|
+
interface ForeignOption {
|
|
40
|
+
/** Matches the option's token in the fence's info string. */
|
|
41
|
+
pattern: RegExp;
|
|
42
|
+
/** The fence languages the option belongs to; any language when unset. */
|
|
43
|
+
languages?: ReadonlySet<string>;
|
|
44
|
+
/** What comes of the option, since Blume ignores it. */
|
|
45
|
+
effect: string;
|
|
46
|
+
/** The Blume spelling, given the option's quoted value. */
|
|
47
|
+
instead: (value: string) => string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `hl_lines="2 4-5"` → `{2,4-5}`: MkDocs separates lines with spaces. */
|
|
51
|
+
const lineRange = (value: string): string =>
|
|
52
|
+
`{${value
|
|
53
|
+
.trim()
|
|
54
|
+
.split(/[\s,]+/u)
|
|
55
|
+
.join(",")}}`;
|
|
56
|
+
|
|
57
|
+
const FOREIGN_OPTIONS: readonly ForeignOption[] = [
|
|
58
|
+
{
|
|
59
|
+
effect: "the block highlights no lines",
|
|
60
|
+
instead: (value) =>
|
|
61
|
+
`Put the lines in braces after the language instead: \`${lineRange(value)}\`.`,
|
|
62
|
+
pattern: /^hl_lines=["'](?<value>[^"']*)["']$/u,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
effect: "the block shows no line numbers",
|
|
66
|
+
instead: (value) =>
|
|
67
|
+
`Write \`lineNumbers\` after the language instead.${
|
|
68
|
+
value.trim() === "1" ? "" : " Blume numbers every block from 1."
|
|
69
|
+
}`,
|
|
70
|
+
pattern: /^linenums=["'](?<value>[^"']*)["']$/u,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
effect: "the block shows no line numbers",
|
|
74
|
+
instead: () => "Write `lineNumbers` after the language instead.",
|
|
75
|
+
pattern: /^showLineNumbers(?:\{\d*\})?$/u,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
// Mintlify's. Like `showLineNumbers` and `wordWrap`, it never joins the
|
|
79
|
+
// title (see `markdown/fence-meta.ts`).
|
|
80
|
+
effect: "the block shows no line numbers",
|
|
81
|
+
instead: () =>
|
|
82
|
+
'Write `lineNumbers` after the language instead. To keep the word in the block\'s title, set the title with `title="…"`.',
|
|
83
|
+
pattern: /^lines$/u,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
effect: "the block doesn't wrap its long lines",
|
|
87
|
+
instead: () => "Write `wrap` after the language instead.",
|
|
88
|
+
pattern: /^wordWrap$/u,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
effect: "the block has no title",
|
|
92
|
+
instead: (value) => `Write \`title="${value}"\` instead.`,
|
|
93
|
+
pattern: /^filename=["'](?<value>[^"']*)["']$/u,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
// rustdoc's and mdBook's, on a Rust fence. They never join the title.
|
|
97
|
+
effect:
|
|
98
|
+
"it does nothing: Blume shows a code block as static text, and never compiles, tests, or runs it",
|
|
99
|
+
instead: () => "Remove it.",
|
|
100
|
+
languages: RUST_LANGUAGES,
|
|
101
|
+
pattern: new RegExp(`^(?:${[...RUST_META_KEYWORDS].join("|")})$`, "u"),
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
/** The option `token` is in a fence whose language is `lang`, if any. */
|
|
106
|
+
const foreignOption = (
|
|
107
|
+
token: string,
|
|
108
|
+
lang: string
|
|
109
|
+
): { option: ForeignOption; value: string } | null => {
|
|
110
|
+
for (const option of FOREIGN_OPTIONS) {
|
|
111
|
+
const match = option.pattern.exec(token);
|
|
112
|
+
if (match && (option.languages?.has(lang) ?? true)) {
|
|
113
|
+
return { option, value: match.groups?.value ?? "" };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Whether a fence's language renders as the author meant: highlighted, or plain on purpose. */
|
|
120
|
+
const rendersLanguage = (lang: string): boolean =>
|
|
121
|
+
isKnownLanguage(lang) || BLUME_LANGUAGES.has(lang);
|
|
122
|
+
|
|
123
|
+
/** A fence's language and option tokens, as the renderer reads them. */
|
|
124
|
+
const fenceParts = (
|
|
125
|
+
lang: string | null | undefined,
|
|
126
|
+
meta: string | null | undefined
|
|
127
|
+
) => {
|
|
128
|
+
const fence = normalizeFence(lang, meta) ?? {
|
|
129
|
+
lang: lang ?? "",
|
|
130
|
+
meta: meta ?? null,
|
|
131
|
+
};
|
|
132
|
+
return { lang: fence.lang, meta: fence.meta, tokens: metaTokens(fence.meta) };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const unknownLanguage = (node: Code, line: number): Finding[] => {
|
|
136
|
+
const { lang } = fenceParts(node.lang, node.meta);
|
|
137
|
+
if (!lang || rendersLanguage(lang)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
code: "BLUME_UNKNOWN_CODE_LANGUAGE",
|
|
143
|
+
line,
|
|
144
|
+
message: `\`${lang}\` isn't a language Blume can highlight, so the code block renders as plain text.`,
|
|
145
|
+
suggestion:
|
|
146
|
+
'Use a Shiki language id or alias (https://shiki.style/languages), or `text` for plain text. Options like a title go after the language, as in `text title="notes.txt"`.',
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const foreignOptions = (node: Code, line: number): Finding[] => {
|
|
152
|
+
const { lang, tokens } = fenceParts(node.lang, node.meta);
|
|
153
|
+
// A fence with options and no language reads its first option as the
|
|
154
|
+
// language, so that token counts too.
|
|
155
|
+
return [node.lang ?? "", ...tokens].flatMap((token) => {
|
|
156
|
+
const found = foreignOption(token, lang);
|
|
157
|
+
if (!found) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
code: "BLUME_CODE_FENCE_OPTION",
|
|
163
|
+
line,
|
|
164
|
+
message: `\`${token}\` isn't a Blume code block option, so ${found.option.effect}.`,
|
|
165
|
+
suggestion: found.option.instead(found.value),
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// A fence opener's info string: the text after a run of three or more
|
|
172
|
+
// backticks or tildes. Matched anywhere in a line, so it also sees fences in
|
|
173
|
+
// list items and quotes (and fence-like inline code, which the parse below
|
|
174
|
+
// sorts out).
|
|
175
|
+
const FENCE_INFO = /(?:`{3,}|~{3,})[\t ]*(?<info>[^`\n]*)/gu;
|
|
176
|
+
|
|
177
|
+
/** Whether a fence's info string holds anything these checks report. */
|
|
178
|
+
const suspectInfo = (info: string): boolean => {
|
|
179
|
+
const [lang, ...rest] = info.trim().split(/\s+/u);
|
|
180
|
+
if (!lang) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const parts = fenceParts(lang, rest.join(" ") || null);
|
|
184
|
+
return (
|
|
185
|
+
!rendersLanguage(parts.lang) ||
|
|
186
|
+
[lang, ...parts.tokens].some(
|
|
187
|
+
(token) => foreignOption(token, parts.lang) !== null
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const parse = (text: string, format: "md" | "mdx"): Nodes | null => {
|
|
193
|
+
try {
|
|
194
|
+
return format === "mdx"
|
|
195
|
+
? mdxToMdast(text, { features: MDX_BODY_FEATURES })
|
|
196
|
+
: markdownToMdast(text, { features: MARKDOWN_BODY_FEATURES });
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The fence problems in a page body, in source order. The body is parsed as
|
|
204
|
+
* the renderer reads it, so a fence shown inside another code block never
|
|
205
|
+
* counts. A page is parsed only when an info string looks like one of these
|
|
206
|
+
* problems, so pages without one cost a scan. A body MDX can't parse fails to
|
|
207
|
+
* render anyway, with its own error.
|
|
208
|
+
*/
|
|
209
|
+
const fenceFindings = (text: string, format: "md" | "mdx"): Finding[] => {
|
|
210
|
+
const suspect = [...text.matchAll(FENCE_INFO)].some((match) =>
|
|
211
|
+
suspectInfo(match.groups?.info ?? "")
|
|
212
|
+
);
|
|
213
|
+
const tree = suspect ? parse(text, format) : null;
|
|
214
|
+
if (!tree) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const found: Finding[] = [];
|
|
218
|
+
const walk = (node: Nodes): void => {
|
|
219
|
+
if (node.type === "code") {
|
|
220
|
+
const line = node.position?.start.line ?? 1;
|
|
221
|
+
found.push(...unknownLanguage(node, line), ...foreignOptions(node, line));
|
|
222
|
+
}
|
|
223
|
+
if ("children" in node) {
|
|
224
|
+
for (const child of node.children) {
|
|
225
|
+
walk(child);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
walk(tree);
|
|
230
|
+
return found;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Warn about code fences that don't render as their author meant:
|
|
235
|
+
*
|
|
236
|
+
* - `BLUME_UNKNOWN_CODE_LANGUAGE`: a language Shiki doesn't know, so the block
|
|
237
|
+
* renders as plain text. Shiki itself only logs a console line naming the
|
|
238
|
+
* language, with no page or line.
|
|
239
|
+
* - `BLUME_CODE_FENCE_OPTION`: an option another docs tool reads, like MkDocs'
|
|
240
|
+
* `hl_lines="2 3"`, Docusaurus' `showLineNumbers`, Mintlify's `lines`, or
|
|
241
|
+
* rustdoc's `no_run`, which Blume ignores. None of them becomes part of the
|
|
242
|
+
* block's title. The suggestion gives the Blume spelling, or says to remove
|
|
243
|
+
* an option Blume has no use for.
|
|
244
|
+
*
|
|
245
|
+
* Lines point into the file the author wrote, a partial's own file for a
|
|
246
|
+
* fence an `<include>` brought in.
|
|
247
|
+
*/
|
|
248
|
+
export const codeFenceDiagnostics = (
|
|
249
|
+
entry: SourceEntry,
|
|
250
|
+
sourceName: string
|
|
251
|
+
): Diagnostic[] => {
|
|
252
|
+
const page = entry.sourcePath ?? `${sourceName}:${entry.ref}`;
|
|
253
|
+
const offset =
|
|
254
|
+
entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
|
|
255
|
+
return fenceFindings(
|
|
256
|
+
entry.expanded?.text ?? entry.body.text,
|
|
257
|
+
entry.body.format
|
|
258
|
+
).map(({ line, ...finding }) => {
|
|
259
|
+
const origin = entry.expanded?.origins[line - 1];
|
|
260
|
+
return {
|
|
261
|
+
...finding,
|
|
262
|
+
file: origin?.file ?? page,
|
|
263
|
+
line: origin?.line ?? line + offset,
|
|
264
|
+
severity: "warning",
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
};
|
package/src/core/code-fences.ts
CHANGED
|
@@ -12,37 +12,73 @@
|
|
|
12
12
|
*/
|
|
13
13
|
const CODE_FENCE = /^(?<run>`{3,}|~{3,})/u;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// One block-quote marker. A fence inside a quote (`> ```ts`) is written after
|
|
16
|
+
// one marker per level of nesting, and so is every line of its code.
|
|
17
|
+
const QUOTE_MARKER = /^[ \t]*>/u;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The open fence's delimiter char, its run length, and the block-quote depth
|
|
21
|
+
* it opened at, or null outside one.
|
|
22
|
+
*/
|
|
23
|
+
export type FenceState = {
|
|
24
|
+
delimiter: "`" | "~";
|
|
25
|
+
length: number;
|
|
26
|
+
quotes: number;
|
|
27
|
+
} | null;
|
|
28
|
+
|
|
29
|
+
/** Strip up to `max` leading block-quote markers from `line`. */
|
|
30
|
+
const stripQuotes = (line: string, max: number) => {
|
|
31
|
+
let rest = line;
|
|
32
|
+
let depth = 0;
|
|
33
|
+
while (depth < max) {
|
|
34
|
+
const marker = QUOTE_MARKER.exec(rest);
|
|
35
|
+
if (!marker) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
rest = rest.slice(marker[0].length);
|
|
39
|
+
depth += 1;
|
|
40
|
+
}
|
|
41
|
+
return { depth, rest };
|
|
42
|
+
};
|
|
17
43
|
|
|
18
44
|
/**
|
|
19
45
|
* Advance the fenced-code state for one line: an opening fence records its
|
|
20
46
|
* delimiter and run length, only a bare run of the same character at least as
|
|
21
47
|
* long closes it (CommonMark), and any other line leaves the state untouched.
|
|
48
|
+
* A fence inside block quotes is read past the quote markers it opened under,
|
|
49
|
+
* and a line without them ends the quote, and the fence with it: code takes
|
|
50
|
+
* no lazy continuation lines (CommonMark).
|
|
22
51
|
*/
|
|
23
52
|
export const nextFenceState = (line: string, fence: FenceState): FenceState => {
|
|
24
|
-
const
|
|
53
|
+
const { depth, rest } = stripQuotes(
|
|
54
|
+
line,
|
|
55
|
+
fence === null ? Number.POSITIVE_INFINITY : fence.quotes
|
|
56
|
+
);
|
|
57
|
+
if (fence !== null && depth < fence.quotes) {
|
|
58
|
+
return nextFenceState(line, null);
|
|
59
|
+
}
|
|
60
|
+
const trimmed = rest.trimStart();
|
|
25
61
|
const run = trimmed.match(CODE_FENCE)?.groups?.run;
|
|
26
62
|
if (run === undefined) {
|
|
27
63
|
return fence;
|
|
28
64
|
}
|
|
29
|
-
const
|
|
65
|
+
const info = trimmed.slice(run.length);
|
|
30
66
|
const delimiter = run.startsWith("`") ? ("`" as const) : ("~" as const);
|
|
31
67
|
if (fence === null) {
|
|
32
68
|
// A backtick fence's info string cannot itself contain a backtick
|
|
33
69
|
// (CommonMark) — a line-leading ```inline``` span is a paragraph, and
|
|
34
70
|
// opening a phantom fence on it would swallow every heading and link
|
|
35
71
|
// after it. Tilde fences carry no such rule.
|
|
36
|
-
if (delimiter === "`" &&
|
|
72
|
+
if (delimiter === "`" && info.includes("`")) {
|
|
37
73
|
return fence;
|
|
38
74
|
}
|
|
39
|
-
return { delimiter, length: run.length };
|
|
75
|
+
return { delimiter, length: run.length, quotes: depth };
|
|
40
76
|
}
|
|
41
77
|
// A closing fence may be followed only by whitespace: a same-length
|
|
42
78
|
// ```js line inside an open ``` block is content, not a closer.
|
|
43
79
|
return fence.delimiter === delimiter &&
|
|
44
80
|
run.length >= fence.length &&
|
|
45
|
-
|
|
81
|
+
info.trim() === ""
|
|
46
82
|
? null
|
|
47
83
|
: fence;
|
|
48
84
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { relative } from "pathe";
|
|
2
2
|
|
|
3
|
-
import { BUILTIN_MDX_TAGS } from "./builtin-tags.ts";
|
|
3
|
+
import { BUILTIN_CHILD_PROPS, BUILTIN_MDX_TAGS } from "./builtin-tags.ts";
|
|
4
4
|
import type { Diagnostic, PageRecord } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
/** `CardGroup` → `card-group`, matching registry item names. */
|
|
@@ -9,36 +9,89 @@ const toKebab = (tag: string): string =>
|
|
|
9
9
|
.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>")
|
|
10
10
|
.toLowerCase();
|
|
11
11
|
|
|
12
|
+
const LIST = new Intl.ListFormat("en", { type: "conjunction" });
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Warn when an `.mdx` page uses a `<Component>` tag that resolves to nothing —
|
|
14
16
|
* a built-in, an island, or a `components.ts` override — so a typo surfaces as a
|
|
15
17
|
* friendly diagnostic (with a `blume add` hint where one exists) instead of a raw
|
|
16
18
|
* MDX "X is not defined" build error. `extraTags` are the project's own known
|
|
17
19
|
* components (islands + overrides); `registryNames` gates the install hint.
|
|
20
|
+
* One warning per tag names every page that uses it, anchored to the first.
|
|
18
21
|
*/
|
|
19
22
|
export const validateUsedComponents = (
|
|
20
23
|
pages: PageRecord[],
|
|
21
24
|
extraTags: Set<string>,
|
|
22
25
|
registryNames: Set<string>
|
|
26
|
+
): Diagnostic[] => {
|
|
27
|
+
const users = new Map<string, PageRecord[]>();
|
|
28
|
+
for (const page of pages) {
|
|
29
|
+
for (const tag of page.componentsUsed ?? []) {
|
|
30
|
+
if (!(BUILTIN_MDX_TAGS.has(tag) || extraTags.has(tag))) {
|
|
31
|
+
users.set(tag, [...(users.get(tag) ?? []), page]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...users].map(([tag, using]) => {
|
|
36
|
+
const name = toKebab(tag);
|
|
37
|
+
const suggestion = registryNames.has(name)
|
|
38
|
+
? `Run \`blume add ${name}\` to install it, or register <${tag}> in components.ts (mdx).`
|
|
39
|
+
: `Register <${tag}> in components.ts (mdx), or add an islands/${tag}.tsx component.`;
|
|
40
|
+
const routes = using.map((page) => page.route);
|
|
41
|
+
const [first] = using;
|
|
42
|
+
return {
|
|
43
|
+
code: "BLUME_UNKNOWN_COMPONENT",
|
|
44
|
+
file: first?.sourcePath ?? first?.id,
|
|
45
|
+
message:
|
|
46
|
+
routes.length === 1
|
|
47
|
+
? `<${tag}> is used in ${LIST.format(routes)} but isn't a known component.`
|
|
48
|
+
: `<${tag}> is used on ${routes.length} pages but isn't a known component: ${LIST.format(routes)}.`,
|
|
49
|
+
severity: "warning",
|
|
50
|
+
suggestion,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const EITHER = new Intl.ListFormat("en", { type: "disjunction" });
|
|
56
|
+
const quoted = (names: readonly string[]): string[] =>
|
|
57
|
+
names.map((name) => `\`${name}\``);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Warn about each childless built-in in an `.mdx` page given props it doesn't
|
|
61
|
+
* take (`<Badge type="tip" text="beta" />`, VitePress's Badge). Blume's
|
|
62
|
+
* components ignore unknown props, so the element renders without the
|
|
63
|
+
* content those props carried. `extraTags` are the project's own components
|
|
64
|
+
* (islands + overrides): one that replaces a built-in takes its own props,
|
|
65
|
+
* so its uses aren't checked. A file shared by several locales is reported
|
|
66
|
+
* once.
|
|
67
|
+
*/
|
|
68
|
+
export const unknownPropDiagnostics = (
|
|
69
|
+
pages: readonly PageRecord[],
|
|
70
|
+
extraTags: ReadonlySet<string>
|
|
23
71
|
): Diagnostic[] => {
|
|
24
72
|
const diagnostics: Diagnostic[] = [];
|
|
25
73
|
const seen = new Set<string>();
|
|
26
74
|
for (const page of pages) {
|
|
27
|
-
for (const
|
|
28
|
-
|
|
75
|
+
for (const use of page.unknownProps ?? []) {
|
|
76
|
+
const file = use.file ?? page.sourcePath ?? page.id;
|
|
77
|
+
const key = `${file}:${use.line}:${use.column}`;
|
|
78
|
+
if (extraTags.has(use.tag) || seen.has(key)) {
|
|
29
79
|
continue;
|
|
30
80
|
}
|
|
31
|
-
seen.add(
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
seen.add(key);
|
|
82
|
+
const accepted = BUILTIN_CHILD_PROPS.get(use.tag) ?? [];
|
|
83
|
+
const takes =
|
|
84
|
+
accepted.length > 0
|
|
85
|
+
? `, and use the props ${use.tag} takes: ${LIST.format(quoted(accepted))}`
|
|
86
|
+
: `; ${use.tag} takes no props`;
|
|
36
87
|
diagnostics.push({
|
|
37
|
-
code: "
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
code: "BLUME_UNKNOWN_PROP",
|
|
89
|
+
column: use.column,
|
|
90
|
+
file,
|
|
91
|
+
line: use.line,
|
|
92
|
+
message: `<${use.tag}> in ${page.route} has no children, and ${use.tag} doesn't take ${EITHER.format(quoted(use.props))}, so it renders without what ${use.props.length === 1 ? "that prop" : "those props"} meant to show.`,
|
|
40
93
|
severity: "warning",
|
|
41
|
-
suggestion
|
|
94
|
+
suggestion: `Put the content between the tags (<${use.tag}>…</${use.tag}>)${takes}.`,
|
|
42
95
|
});
|
|
43
96
|
}
|
|
44
97
|
}
|