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/meta.ts
CHANGED
|
@@ -90,17 +90,16 @@ const withinInclude = (
|
|
|
90
90
|
);
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
* meta module name, shared `meta.$.*` included, by default)
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* translates through this too, so it covers exactly the ones the scan reads.
|
|
93
|
+
* Every folder-meta file under a filesystem source's root: `patterns` (every
|
|
94
|
+
* meta module name, shared `meta.$.*` included, by default), minus the
|
|
95
|
+
* source's `exclude` globs, dependencies, and build output — whether or not
|
|
96
|
+
* its `include` globs reach the file's folder.
|
|
98
97
|
*/
|
|
99
|
-
|
|
98
|
+
const globFolderMetaFiles = (
|
|
100
99
|
source: FolderMetaSource,
|
|
101
|
-
patterns: readonly string[]
|
|
102
|
-
): Promise<string[]> =>
|
|
103
|
-
|
|
100
|
+
patterns: readonly string[]
|
|
101
|
+
): Promise<string[]> =>
|
|
102
|
+
glob([...patterns], {
|
|
104
103
|
absolute: true,
|
|
105
104
|
cwd: source.root,
|
|
106
105
|
// Never descend into dependencies or build output — relevant when the
|
|
@@ -116,9 +115,24 @@ export const findFolderMetaFiles = async (
|
|
|
116
115
|
],
|
|
117
116
|
onlyFiles: true,
|
|
118
117
|
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
|
|
119
|
+
/** Whether the source's `include` globs reach a meta file's folder. */
|
|
120
|
+
const isReadMetaFile = (source: FolderMetaSource, file: string): boolean =>
|
|
121
|
+
withinInclude(relative(source.root, dirname(file)), source.include);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The folder-meta files a filesystem source contributes: `patterns` (every
|
|
125
|
+
* meta module name, shared `meta.$.*` included, by default) under the source's
|
|
126
|
+
* root, minus its `exclude` globs, dependencies, and build output, and only in
|
|
127
|
+
* folders its `include` globs reach. `blume translate` finds the meta files it
|
|
128
|
+
* translates through this too, so it covers exactly the ones the scan reads.
|
|
129
|
+
*/
|
|
130
|
+
export const findFolderMetaFiles = async (
|
|
131
|
+
source: FolderMetaSource,
|
|
132
|
+
patterns: readonly string[] = META_FILES
|
|
133
|
+
): Promise<string[]> => {
|
|
134
|
+
const found = await globFolderMetaFiles(source, patterns);
|
|
135
|
+
return found.filter((file) => isReadMetaFile(source, file));
|
|
122
136
|
};
|
|
123
137
|
|
|
124
138
|
/**
|
|
@@ -135,6 +149,29 @@ const metaKeyFor = (prefix: string | undefined, dir: string): string => {
|
|
|
135
149
|
return dir ? `${clean}/${dir}` : clean;
|
|
136
150
|
};
|
|
137
151
|
|
|
152
|
+
/** Folder meta by key: per-locale `meta.*` files, and shared `meta.$.*` ones. */
|
|
153
|
+
export interface FolderMetaMaps {
|
|
154
|
+
meta: Map<string, FolderMeta>;
|
|
155
|
+
shared: Map<string, FolderMeta>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A folder-meta file Blume found but doesn't read, and the key it would have. */
|
|
159
|
+
export interface UnreadFolderMeta {
|
|
160
|
+
/** Its folder, relative to the source root. */
|
|
161
|
+
dir: string;
|
|
162
|
+
file: string;
|
|
163
|
+
key: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** What {@link discoverFolderMeta} found across the sources. */
|
|
167
|
+
export interface DiscoveredFolderMeta extends FolderMetaMaps {
|
|
168
|
+
diagnostics: Diagnostic[];
|
|
169
|
+
/** The file each meta in `meta` and `shared` was read from. */
|
|
170
|
+
files: Map<FolderMeta, string>;
|
|
171
|
+
/** Meta files under a source root that no source's `include` globs reach. */
|
|
172
|
+
unread: UnreadFolderMeta[];
|
|
173
|
+
}
|
|
174
|
+
|
|
138
175
|
/**
|
|
139
176
|
* Discover `meta.{ts,js,mjs}` files across the given filesystem sources. Keys are
|
|
140
177
|
* the source's route prefix joined with the directory relative to that source's
|
|
@@ -162,11 +199,7 @@ export const discoverFolderMeta = async (
|
|
|
162
199
|
localeDirs?: readonly string[];
|
|
163
200
|
versionDirs?: readonly string[];
|
|
164
201
|
} = {}
|
|
165
|
-
): Promise<{
|
|
166
|
-
meta: Map<string, FolderMeta>;
|
|
167
|
-
shared: Map<string, FolderMeta>;
|
|
168
|
-
diagnostics: Diagnostic[];
|
|
169
|
-
}> => {
|
|
202
|
+
): Promise<DiscoveredFolderMeta> => {
|
|
170
203
|
const list: FolderMetaSource[] = Array.isArray(sources)
|
|
171
204
|
? sources
|
|
172
205
|
: [{ root: sources }];
|
|
@@ -181,34 +214,67 @@ export const discoverFolderMeta = async (
|
|
|
181
214
|
const load = createModuleLoader();
|
|
182
215
|
const meta = new Map<string, FolderMeta>();
|
|
183
216
|
const shared = new Map<string, FolderMeta>();
|
|
217
|
+
const files = new Map<FolderMeta, string>();
|
|
218
|
+
const unread: UnreadFolderMeta[] = [];
|
|
184
219
|
const diagnostics: Diagnostic[] = [];
|
|
185
220
|
|
|
221
|
+
// A version snapshot dir is outermost, with a locale dir one level deeper;
|
|
222
|
+
// both are hoisted in front of the (prefixed) group path, in that order —
|
|
223
|
+
// the lookup key reads `version/locale/prefix/dir`.
|
|
224
|
+
const keyOf = (source: FolderMetaSource, file: string): string => {
|
|
225
|
+
const dir = relative(source.root, dirname(file));
|
|
226
|
+
const [head, ...tail] = dir.split("/");
|
|
227
|
+
const version = head && versionDirs.has(head) ? head : "";
|
|
228
|
+
const afterVersion = version ? tail : [head ?? "", ...tail];
|
|
229
|
+
const [localeHead, ...localeTail] = afterVersion;
|
|
230
|
+
const locale = localeHead
|
|
231
|
+
? (localeDirs.get(localeHead.toLowerCase()) ?? "")
|
|
232
|
+
: "";
|
|
233
|
+
const rest = (locale ? localeTail : afterVersion).filter(Boolean).join("/");
|
|
234
|
+
return [version, locale, metaKeyFor(source.prefix, rest)]
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.join("/");
|
|
237
|
+
};
|
|
238
|
+
|
|
186
239
|
// Scan every source under its own root so a source rooted outside
|
|
187
240
|
// `content.root` still contributes its folder meta.
|
|
188
241
|
const perSource = await Promise.all(
|
|
189
242
|
list.map(async (source) => {
|
|
190
|
-
const
|
|
243
|
+
const found = await globFolderMetaFiles(source, META_FILES);
|
|
244
|
+
// Files outside every `include` glob are never imported; they're kept
|
|
245
|
+
// by key so a scan that finds pages in their group can say so.
|
|
246
|
+
for (const file of found) {
|
|
247
|
+
if (!isReadMetaFile(source, file)) {
|
|
248
|
+
unread.push({
|
|
249
|
+
dir: relative(source.root, dirname(file)),
|
|
250
|
+
file,
|
|
251
|
+
key: keyOf(source, file),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
191
255
|
const loaded = await Promise.all(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
256
|
+
found
|
|
257
|
+
.filter((file) => isReadMetaFile(source, file))
|
|
258
|
+
.map(
|
|
259
|
+
async (
|
|
260
|
+
file
|
|
261
|
+
): Promise<
|
|
262
|
+
| { ok: true; file: string; value: unknown }
|
|
263
|
+
| { ok: false; file: string; error: Error }
|
|
264
|
+
> => {
|
|
265
|
+
try {
|
|
266
|
+
return {
|
|
267
|
+
file,
|
|
268
|
+
ok: true,
|
|
269
|
+
value: await resolveMeta(await load(file)),
|
|
270
|
+
};
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// SAFETY: jiti surfaces load/evaluate failures as Error
|
|
273
|
+
// instances, and only `message` is read downstream.
|
|
274
|
+
return { error: error as Error, file, ok: false };
|
|
275
|
+
}
|
|
209
276
|
}
|
|
210
|
-
|
|
211
|
-
)
|
|
277
|
+
)
|
|
212
278
|
);
|
|
213
279
|
return { loaded, source };
|
|
214
280
|
})
|
|
@@ -216,23 +282,7 @@ export const discoverFolderMeta = async (
|
|
|
216
282
|
|
|
217
283
|
for (const { loaded, source } of perSource) {
|
|
218
284
|
for (const entry of loaded) {
|
|
219
|
-
const
|
|
220
|
-
// A version snapshot dir is outermost, with a locale dir one level
|
|
221
|
-
// deeper; both are hoisted in front of the (prefixed) group path, in
|
|
222
|
-
// that order — the lookup key reads `version/locale/prefix/dir`.
|
|
223
|
-
const [head, ...tail] = dir.split("/");
|
|
224
|
-
const version = head && versionDirs.has(head) ? head : "";
|
|
225
|
-
const afterVersion = version ? tail : [head ?? "", ...tail];
|
|
226
|
-
const [localeHead, ...localeTail] = afterVersion;
|
|
227
|
-
const locale = localeHead
|
|
228
|
-
? (localeDirs.get(localeHead.toLowerCase()) ?? "")
|
|
229
|
-
: "";
|
|
230
|
-
const rest = (locale ? localeTail : afterVersion)
|
|
231
|
-
.filter(Boolean)
|
|
232
|
-
.join("/");
|
|
233
|
-
const key = [version, locale, metaKeyFor(source.prefix, rest)]
|
|
234
|
-
.filter(Boolean)
|
|
235
|
-
.join("/");
|
|
285
|
+
const key = keyOf(source, entry.file);
|
|
236
286
|
|
|
237
287
|
if (!entry.ok) {
|
|
238
288
|
diagnostics.push({
|
|
@@ -250,6 +300,7 @@ export const discoverFolderMeta = async (
|
|
|
250
300
|
? shared
|
|
251
301
|
: meta;
|
|
252
302
|
target.set(key, result.data);
|
|
303
|
+
files.set(result.data, entry.file);
|
|
253
304
|
} else {
|
|
254
305
|
diagnostics.push(
|
|
255
306
|
...diagnosticsFromZod(result.error, {
|
|
@@ -261,5 +312,53 @@ export const discoverFolderMeta = async (
|
|
|
261
312
|
}
|
|
262
313
|
}
|
|
263
314
|
|
|
264
|
-
|
|
315
|
+
// Sources sharing a root partition it with `include` globs, so a file one
|
|
316
|
+
// source skips may be another's: only a file no source reads is unread.
|
|
317
|
+
const read = new Set(
|
|
318
|
+
perSource.flatMap(({ loaded }) => loaded.map((entry) => entry.file))
|
|
319
|
+
);
|
|
320
|
+
return {
|
|
321
|
+
diagnostics,
|
|
322
|
+
files,
|
|
323
|
+
meta,
|
|
324
|
+
shared,
|
|
325
|
+
unread: unread.filter((entry) => !read.has(entry.file)),
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Lay the folder meta content sources derive (the OpenAPI source's tag
|
|
331
|
+
* titles and order) beneath the user's `meta.ts` and `meta.$.ts` files, field
|
|
332
|
+
* by field: a user file's own keys win, and the generated ones fill the rest,
|
|
333
|
+
* so a `meta.ts` that only renames a tag group keeps its generated `order`.
|
|
334
|
+
* Generated meta is locale-agnostic, keyed like `meta.$.*`, so it also sits
|
|
335
|
+
* beneath a locale's own `meta.ts` (`fr/reference/pets` over
|
|
336
|
+
* `reference/pets`). It doesn't reach into an archived version's snapshot,
|
|
337
|
+
* whose groups read only meta keyed under the version.
|
|
338
|
+
*/
|
|
339
|
+
export const withGeneratedFolderMeta = (
|
|
340
|
+
user: FolderMetaMaps,
|
|
341
|
+
generated: Map<string, FolderMeta>,
|
|
342
|
+
localeDirs: readonly string[] = []
|
|
343
|
+
): FolderMetaMaps => {
|
|
344
|
+
const locales = new Set(localeDirs);
|
|
345
|
+
const beneath = (key: string, meta: FolderMeta): FolderMeta => {
|
|
346
|
+
const [head, ...rest] = key.split("/");
|
|
347
|
+
const generatedMeta = generated.get(
|
|
348
|
+
head !== undefined && locales.has(head) ? rest.join("/") : key
|
|
349
|
+
);
|
|
350
|
+
return generatedMeta ? { ...generatedMeta, ...meta } : meta;
|
|
351
|
+
};
|
|
352
|
+
return {
|
|
353
|
+
meta: new Map(
|
|
354
|
+
[...user.meta].map(([key, meta]) => [key, beneath(key, meta)])
|
|
355
|
+
),
|
|
356
|
+
shared: new Map([
|
|
357
|
+
...generated,
|
|
358
|
+
...[...user.shared].map(([key, meta]): [string, FolderMeta] => [
|
|
359
|
+
key,
|
|
360
|
+
{ ...generated.get(key), ...meta },
|
|
361
|
+
]),
|
|
362
|
+
]),
|
|
363
|
+
};
|
|
265
364
|
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isAssetIcon } from "../theme/icon-kind.ts";
|
|
2
2
|
import { hasIcon } from "../theme/icons.ts";
|
|
3
3
|
import { isInternalPath } from "./base-path.ts";
|
|
4
|
+
import { resolveLocalizable } from "./localizable.ts";
|
|
5
|
+
import { isBuiltInMcpClient } from "./mcp-clients.ts";
|
|
6
|
+
import type { McpClientEntry } from "./mcp-clients.ts";
|
|
4
7
|
import { isPageRef, normalizeRef } from "./navigation.ts";
|
|
5
8
|
import type { SidebarItemConfig } from "./schema.ts";
|
|
6
9
|
import type { Diagnostic, NavNode, Navigation, PageRecord } from "./types.ts";
|
|
@@ -100,6 +103,28 @@ export const validateSearchPopularIcons = (
|
|
|
100
103
|
return unknownIconDiagnostics(icons, ICON_FORMAT_HINT);
|
|
101
104
|
};
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Warn about unknown icons on custom "Connect to MCP" clients
|
|
108
|
+
* (`agents.mcp.clients`), which live outside the navigation model too. A
|
|
109
|
+
* per-locale label is named in the default locale.
|
|
110
|
+
*/
|
|
111
|
+
export const validateMcpClientIcons = (
|
|
112
|
+
clients: readonly McpClientEntry[],
|
|
113
|
+
defaultLocale?: string
|
|
114
|
+
): Diagnostic[] => {
|
|
115
|
+
const icons = clients.flatMap((client) =>
|
|
116
|
+
!isBuiltInMcpClient(client) && client.icon
|
|
117
|
+
? [
|
|
118
|
+
{
|
|
119
|
+
icon: client.icon,
|
|
120
|
+
where: `MCP client "${resolveLocalizable(client.label, defaultLocale, defaultLocale)}"`,
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
: []
|
|
124
|
+
);
|
|
125
|
+
return unknownIconDiagnostics(icons, ICON_FORMAT_HINT);
|
|
126
|
+
};
|
|
127
|
+
|
|
103
128
|
/** Whether an internal path resolves to a page or a section that has pages. */
|
|
104
129
|
const resolvesToPages = (routes: Set<string>, path: string): boolean =>
|
|
105
130
|
routes.has(path) || [...routes].some((route) => route.startsWith(`${path}/`));
|
|
@@ -108,14 +133,25 @@ const resolvesToPages = (routes: Set<string>, path: string): boolean =>
|
|
|
108
133
|
* Warn when a config-linked tab/selector target has no matching page. `routes`
|
|
109
134
|
* must be the full set of servable routes — content, custom `.astro` pages, and
|
|
110
135
|
* generated routes — so this runs where all three are known (`generateRuntime`),
|
|
111
|
-
* not in the content-only graph build.
|
|
136
|
+
* not in the content-only graph build. `servesFile` answers for the files the
|
|
137
|
+
* site serves beside its pages — `public/` and the generated ones (see
|
|
138
|
+
* `staticFileResolver`) — so a featured link to `/spec.pdf` or `/llms.txt`
|
|
139
|
+
* resolves too.
|
|
140
|
+
*
|
|
141
|
+
* A tab is checked where it links: its `href` — the author's, or the
|
|
142
|
+
* section's first page that navigation resolved for a `path` with no page of
|
|
143
|
+
* its own — and its `path` only when it links there.
|
|
112
144
|
*/
|
|
113
145
|
export const validateNavTargets = (
|
|
114
146
|
navigation: Navigation,
|
|
115
|
-
routes: Set<string
|
|
147
|
+
routes: Set<string>,
|
|
148
|
+
servesFile: (path: string) => boolean = () => false
|
|
116
149
|
): Diagnostic[] => {
|
|
117
150
|
const targets: { label: string; path: string }[] = [
|
|
118
|
-
...navigation.tabs.map((tab) => ({
|
|
151
|
+
...navigation.tabs.map((tab) => ({
|
|
152
|
+
label: tab.label,
|
|
153
|
+
path: tab.href ?? tab.path,
|
|
154
|
+
})),
|
|
119
155
|
...navigation.selectors.flatMap((selector) =>
|
|
120
156
|
selector.items.map((item) => ({ label: item.label, path: item.path }))
|
|
121
157
|
),
|
|
@@ -141,7 +177,8 @@ export const validateNavTargets = (
|
|
|
141
177
|
}
|
|
142
178
|
// A query or fragment (`/guides?tab=cli`) rides on the route; only the
|
|
143
179
|
// path names a page.
|
|
144
|
-
|
|
180
|
+
const bare = path.split(/[?#]/u)[0] ?? path;
|
|
181
|
+
if (!(resolvesToPages(routes, bare) || servesFile(bare))) {
|
|
145
182
|
seen.add(path);
|
|
146
183
|
diagnostics.push({
|
|
147
184
|
code: "BLUME_NAV_MISSING_PAGE",
|
package/src/core/navigation.ts
CHANGED
|
@@ -161,6 +161,40 @@ const stemOf = (filename: string): string =>
|
|
|
161
161
|
const navStem = (navPath: string): string =>
|
|
162
162
|
stemOf(navPath.split("/").at(-1) ?? navPath);
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* A page's key in its folder meta `pages` list: its file name without the
|
|
166
|
+
* extension, ordering prefix, or parentheses (`01-quickstart.mdx` is
|
|
167
|
+
* `quickstart`).
|
|
168
|
+
*/
|
|
169
|
+
export const pageKey = (filename: string): string =>
|
|
170
|
+
segmentKey(stemOf(filename));
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The keys a folder meta `pages` list can name, by group path: each folder's
|
|
174
|
+
* pages and subfolders, keyed as `buildFileSystemSidebar` keys its nodes (the
|
|
175
|
+
* name without its extension, ordering prefix, or parentheses). Read off nav
|
|
176
|
+
* paths, so it covers pages the sidebar leaves out, like hidden ones.
|
|
177
|
+
*/
|
|
178
|
+
export const folderChildKeys = (
|
|
179
|
+
navPaths: Iterable<string>
|
|
180
|
+
): Map<string, Set<string>> => {
|
|
181
|
+
const keys = new Map<string, Set<string>>();
|
|
182
|
+
const add = (group: string, key: string): void => {
|
|
183
|
+
const children = keys.get(group) ?? new Set<string>();
|
|
184
|
+
children.add(key);
|
|
185
|
+
keys.set(group, children);
|
|
186
|
+
};
|
|
187
|
+
for (const navPath of navPaths) {
|
|
188
|
+
const dirs = navPath.split("/");
|
|
189
|
+
const filename = dirs.pop() ?? navPath;
|
|
190
|
+
for (const [depth, dir] of dirs.entries()) {
|
|
191
|
+
add(dirs.slice(0, depth).join("/"), segmentKey(dir));
|
|
192
|
+
}
|
|
193
|
+
add(dirs.join("/"), pageKey(filename));
|
|
194
|
+
}
|
|
195
|
+
return keys;
|
|
196
|
+
};
|
|
197
|
+
|
|
164
198
|
interface MutablePage {
|
|
165
199
|
kind: "page";
|
|
166
200
|
key: string;
|
|
@@ -397,16 +431,21 @@ const applyFolderMeta = (
|
|
|
397
431
|
|
|
398
432
|
/**
|
|
399
433
|
* Warn when an index page's own frontmatter title diverges from its folder's
|
|
400
|
-
* explicit `meta.title`. The sidebar label and the page's own
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
434
|
+
* explicit `meta.title`. The sidebar label and the page's own title (its
|
|
435
|
+
* heading, and its `<title>` unless `seo.title` replaces that) are resolved
|
|
436
|
+
* from two independent sources — under i18n, a translator can update the
|
|
437
|
+
* folder's `meta.ts` and forget the index page's own frontmatter (or vice
|
|
438
|
+
* versa), and a correct-looking sidebar hides the mismatch.
|
|
404
439
|
*
|
|
405
440
|
* Only fires when the index page's own sidebar row is hidden
|
|
406
441
|
* (`sidebar.hidden`), so the linked group header is the only sidebar label the
|
|
407
442
|
* page has. A visible index row shows the page's own label beneath the header,
|
|
408
443
|
* so the sidebar already carries both titles and a divergence there is a
|
|
409
|
-
* deliberate pairing ("CLI" over "Overview"), not drift.
|
|
444
|
+
* deliberate pairing ("CLI" over "Overview"), not drift. A hidden index keeps a
|
|
445
|
+
* different title on purpose the same way any page does: its `sidebar.label`
|
|
446
|
+
* names the folder title, so the label its own row would carry agrees with the
|
|
447
|
+
* header. The suggestion names only ways out that clear the warning:
|
|
448
|
+
* `validate --strict` fails on any warning, so leaving it is not one.
|
|
410
449
|
*
|
|
411
450
|
* Only fires when the page has an explicit frontmatter `title` of its own:
|
|
412
451
|
* when it's absent, `page.title` is derived from the first heading or the
|
|
@@ -440,15 +479,19 @@ const indexTitleMismatchDiagnostic = (
|
|
|
440
479
|
const meta =
|
|
441
480
|
folderMeta.get(metaKey(folderPath, metaPrefix)) ??
|
|
442
481
|
sharedMeta.get(metaKey(folderPath, sharedMetaPrefix));
|
|
443
|
-
if (
|
|
482
|
+
if (
|
|
483
|
+
!meta?.title ||
|
|
484
|
+
meta.title === page.title ||
|
|
485
|
+
meta.title === page.meta.sidebar.label
|
|
486
|
+
) {
|
|
444
487
|
return undefined;
|
|
445
488
|
}
|
|
446
489
|
return {
|
|
447
490
|
code: "BLUME_NAV_INDEX_TITLE_MISMATCH",
|
|
448
491
|
file: page.sourcePath ?? page.id,
|
|
449
|
-
message: `Index page "${page.navPath}" has title "${page.title}", but its folder's meta.title is "${meta.title}"
|
|
492
|
+
message: `Index page "${page.navPath}" has title "${page.title}", but its folder's meta.title is "${meta.title}". The page hides its own sidebar row, so "${meta.title}" is the only sidebar label it has.`,
|
|
450
493
|
severity: "warning",
|
|
451
|
-
suggestion: `
|
|
494
|
+
suggestion: `Change the page's title or the folder's meta.ts title so they match, set the page's sidebar.label to "${meta.title}" to keep its title on purpose, or remove sidebar.hidden so the sidebar also lists the page under its own label.`,
|
|
452
495
|
};
|
|
453
496
|
};
|
|
454
497
|
|
|
@@ -622,10 +665,16 @@ const hoistPages = (
|
|
|
622
665
|
* group's *children*, not the tree root — so hoisting only the root leaves a
|
|
623
666
|
* tab section's loose pages interleaved with its groups. Hoist the top level of
|
|
624
667
|
* every group that owns a tab (matched on its URL path), mirroring the root.
|
|
668
|
+
*
|
|
669
|
+
* The section is the outermost group at the tab's path, the one render-time
|
|
670
|
+
* scoping shows (`sectionChildren`). A `(group)` folder inside it adds no
|
|
671
|
+
* route segment, so it shares that path, but it is a group in the section, not
|
|
672
|
+
* the section itself: below a matched group its path no longer matches, and
|
|
673
|
+
* the (group)'s own level hoists only by the `hoistPages` rules.
|
|
625
674
|
*/
|
|
626
675
|
const hoistTabSections = (
|
|
627
676
|
nodes: MutableNode[],
|
|
628
|
-
tabPaths:
|
|
677
|
+
tabPaths: ReadonlySet<string>,
|
|
629
678
|
display: SidebarDisplay
|
|
630
679
|
): void => {
|
|
631
680
|
for (const node of nodes) {
|
|
@@ -634,6 +683,10 @@ const hoistTabSections = (
|
|
|
634
683
|
}
|
|
635
684
|
if (node.routePath !== undefined && tabPaths.has(node.routePath)) {
|
|
636
685
|
hoistPages(node.children, display, true);
|
|
686
|
+
const inner = new Set(tabPaths);
|
|
687
|
+
inner.delete(node.routePath);
|
|
688
|
+
hoistTabSections(node.children, inner, display);
|
|
689
|
+
continue;
|
|
637
690
|
}
|
|
638
691
|
hoistTabSections(node.children, tabPaths, display);
|
|
639
692
|
}
|
|
@@ -1049,6 +1102,11 @@ const resolveTabHref = (
|
|
|
1049
1102
|
return path;
|
|
1050
1103
|
}
|
|
1051
1104
|
const rest = pathUnder(path, roots.tabs);
|
|
1105
|
+
// An archived tree's root tab links back to the current docs: its section
|
|
1106
|
+
// is the whole current tree, no page of which this tree holds.
|
|
1107
|
+
if (rest === "/" && roots.tree !== roots.tabs) {
|
|
1108
|
+
return path;
|
|
1109
|
+
}
|
|
1052
1110
|
const section =
|
|
1053
1111
|
rest === undefined || rest === "/" ? path : joinRoute(roots.tree, rest);
|
|
1054
1112
|
let first: string | undefined;
|
|
@@ -1058,10 +1116,12 @@ const resolveTabHref = (
|
|
|
1058
1116
|
if (route === section) {
|
|
1059
1117
|
return true;
|
|
1060
1118
|
}
|
|
1119
|
+
// `isUnderPath`, not a `${section}/` prefix: a root tab's section is
|
|
1120
|
+
// `/`, which every route sits under but none starts `//`.
|
|
1061
1121
|
if (
|
|
1062
1122
|
first === undefined &&
|
|
1063
1123
|
route !== undefined &&
|
|
1064
|
-
route
|
|
1124
|
+
isUnderPath(route, section)
|
|
1065
1125
|
) {
|
|
1066
1126
|
first = route;
|
|
1067
1127
|
}
|
|
@@ -11,11 +11,21 @@ const ORDERING_PREFIX = /^(?<order>\d+)[-_.]/u;
|
|
|
11
11
|
* (`2024-01-05`, `2024-01-05-first-post`), whose year would sort the sidebar
|
|
12
12
|
* and leave only `01-05…` of the route.
|
|
13
13
|
*/
|
|
14
|
-
const
|
|
14
|
+
const VERSION_OR_ISO_DATE = /^(?:\d+\.\d|\d{4}-\d{2}-\d{2}(?:[-_.]|$))/u;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The other dates a name can start with, kept whole the same way: day or
|
|
18
|
+
* month first with a four-digit year last (`12-05-2022`, `1-5-2022-launch`),
|
|
19
|
+
* and a year and month (`2024-01`, `2024-01-recap`). Blume once read their
|
|
20
|
+
* first number as an ordering prefix, so these names routed as `05-2022` and
|
|
21
|
+
* `01`; {@link formerRouteName} still knows those routes, so they redirect.
|
|
22
|
+
*/
|
|
23
|
+
const DATE_NAME =
|
|
24
|
+
/^(?:(?:0?[1-9]|[12]\d|3[01])-(?:0?[1-9]|[12]\d|3[01])-\d{4}|\d{4}-(?:0[1-9]|1[0-2]))(?:[-_.]|$)/u;
|
|
15
25
|
|
|
16
26
|
/** The digits of `name`'s ordering prefix (`01` of `01-intro`), if it has one. */
|
|
17
27
|
export const orderingPrefix = (name: string): string | undefined =>
|
|
18
|
-
|
|
28
|
+
VERSION_OR_ISO_DATE.test(name) || DATE_NAME.test(name)
|
|
19
29
|
? undefined
|
|
20
30
|
: name.match(ORDERING_PREFIX)?.groups?.order;
|
|
21
31
|
|
|
@@ -25,3 +35,13 @@ export const stripOrderingPrefix = (name: string): string => {
|
|
|
25
35
|
// The prefix is the digits plus their one-character separator.
|
|
26
36
|
return order === undefined ? name : name.slice(order.length + 1);
|
|
27
37
|
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The name an earlier Blume routed `name` as, when it differs from today's:
|
|
41
|
+
* a {@link DATE_NAME} lost its first number as an ordering prefix
|
|
42
|
+
* (`12-05-2022` routed as `05-2022`, `2024-01` as `01`).
|
|
43
|
+
*/
|
|
44
|
+
export const formerRouteName = (name: string): string | undefined =>
|
|
45
|
+
DATE_NAME.test(name) && !VERSION_OR_ISO_DATE.test(name)
|
|
46
|
+
? name.slice(name.indexOf("-") + 1)
|
|
47
|
+
: undefined;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pinned heading id (`## Heading [#a--b]`, `{#a--b}`) is used verbatim, but
|
|
3
|
+
* the parse that reads it applies smart punctuation (Astro's `smartypants`,
|
|
4
|
+
* on by default) to the whole heading first: `--` became `–`, `...` became
|
|
5
|
+
* `…`, and quotes curled, so the id rendered as `a–b` while every link
|
|
6
|
+
* written to it says `#a--b`. Smart punctuation leaves a backslash-escaped
|
|
7
|
+
* character alone, so before a source is parsed, the punctuation in each
|
|
8
|
+
* heading's trailing markers is escaped where the author wrote it, and the
|
|
9
|
+
* parse reads the id as typed. An id with a typed `–` keeps it.
|
|
10
|
+
*
|
|
11
|
+
* The page render, each `<include>` splice, and the scan-time heading reader
|
|
12
|
+
* all escape their source this way, so the rendered id and the anchor index
|
|
13
|
+
* `blume validate` checks against agree.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Heading, Nodes } from "mdast";
|
|
17
|
+
import { markdownToMdast, mdxToMdast } from "satteri";
|
|
18
|
+
import type { SourceFormat } from "satteri";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
MARKDOWN_BODY_FEATURES,
|
|
22
|
+
MDX_BODY_FEATURES,
|
|
23
|
+
} from "../markdown/features.ts";
|
|
24
|
+
import { parseHeadingMarkers } from "./heading-markers.ts";
|
|
25
|
+
|
|
26
|
+
/** What smart punctuation rewrites: dashes, an ellipsis, and quotes. */
|
|
27
|
+
const SMART_INPUT = /--|\.\.\.|['"]/u;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A run that opens a pinned id, up to whitespace or a closing bracket. The
|
|
31
|
+
* runs never overlap, so finding them all stays linear.
|
|
32
|
+
*/
|
|
33
|
+
const PIN_RUN = /[[{]#[^\s\]}]*/gu;
|
|
34
|
+
|
|
35
|
+
/** A backslash escape (kept as is), or a character smart punctuation reads. */
|
|
36
|
+
const PUNCTUATION = /\\[\s\S]|[-.'"]/gu;
|
|
37
|
+
|
|
38
|
+
const escapePunctuation = (text: string): string =>
|
|
39
|
+
text.replaceAll(PUNCTUATION, (match) =>
|
|
40
|
+
match.length === 1 ? `\\${match}` : match
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** Whether any pinned id in the source holds something smart punctuation rewrites. */
|
|
44
|
+
const hasPunctuatedPin = (source: string): boolean => {
|
|
45
|
+
for (const [run] of source.matchAll(PIN_RUN)) {
|
|
46
|
+
if (SMART_INPUT.test(run)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** One replacement in the source: `text` in place of `from`…`end`. */
|
|
54
|
+
interface Edit {
|
|
55
|
+
end: number;
|
|
56
|
+
from: number;
|
|
57
|
+
text: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The escape a heading's trailing markers need, if any. Markers only count at
|
|
62
|
+
* the end of a heading's last text node (see `markdown/heading-anchors.ts`),
|
|
63
|
+
* and a heading that is nothing but markers keeps them as literal text, so
|
|
64
|
+
* neither is touched here either.
|
|
65
|
+
*/
|
|
66
|
+
const pinEdit = (heading: Heading, source: string): Edit | undefined => {
|
|
67
|
+
const last = heading.children.at(-1);
|
|
68
|
+
const start = last?.position?.start.offset;
|
|
69
|
+
const end = last?.position?.end.offset;
|
|
70
|
+
if (last?.type !== "text" || start === undefined || end === undefined) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const parsed = parseHeadingMarkers(last.value);
|
|
74
|
+
if (
|
|
75
|
+
parsed.id === undefined ||
|
|
76
|
+
(parsed.text === "" && heading.children.length === 1)
|
|
77
|
+
) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
// The same markers, read where the author wrote them.
|
|
81
|
+
const written = parseHeadingMarkers(source.slice(start, end));
|
|
82
|
+
const from = start + written.text.length;
|
|
83
|
+
const markers = source.slice(from, end);
|
|
84
|
+
if (written.id === undefined || !SMART_INPUT.test(markers)) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return { end, from, text: escapePunctuation(markers) };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Collect the edits for every heading under `node`, in source order. */
|
|
91
|
+
const collectEdits = (node: Nodes, source: string, edits: Edit[]): void => {
|
|
92
|
+
if (node.type === "heading") {
|
|
93
|
+
const edit = pinEdit(node, source);
|
|
94
|
+
if (edit) {
|
|
95
|
+
edits.push(edit);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if ("children" in node) {
|
|
100
|
+
for (const child of node.children) {
|
|
101
|
+
collectEdits(child, source, edits);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const parse = (source: string, format: SourceFormat): Nodes | undefined => {
|
|
107
|
+
try {
|
|
108
|
+
return format === "mdx"
|
|
109
|
+
? mdxToMdast(source, { features: MDX_BODY_FEATURES })
|
|
110
|
+
: markdownToMdast(source, { features: MARKDOWN_BODY_FEATURES });
|
|
111
|
+
} catch {
|
|
112
|
+
// A source MDX can't parse fails its own render, with its own error.
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `source` with the smart-punctuation characters in each heading's pinned id
|
|
119
|
+
* backslash-escaped, so a parse with smart punctuation reads the id as
|
|
120
|
+
* written. `source` must be the whole text the parse will read, link
|
|
121
|
+
* definitions included: they decide which brackets are links. A source
|
|
122
|
+
* without such a pin is returned as is, unparsed.
|
|
123
|
+
*/
|
|
124
|
+
export const escapePinnedIds = (
|
|
125
|
+
source: string,
|
|
126
|
+
format: SourceFormat
|
|
127
|
+
): string => {
|
|
128
|
+
const tree = hasPunctuatedPin(source) ? parse(source, format) : undefined;
|
|
129
|
+
if (!tree) {
|
|
130
|
+
return source;
|
|
131
|
+
}
|
|
132
|
+
const edits: Edit[] = [];
|
|
133
|
+
collectEdits(tree, source, edits);
|
|
134
|
+
let escaped = "";
|
|
135
|
+
let at = 0;
|
|
136
|
+
for (const edit of edits) {
|
|
137
|
+
escaped += source.slice(at, edit.from) + edit.text;
|
|
138
|
+
at = edit.end;
|
|
139
|
+
}
|
|
140
|
+
return escaped + source.slice(at);
|
|
141
|
+
};
|