blume 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -0
- package/dist/cli/{chunk-qwsrynx5.js → chunk-1d7ve1dm.js} +38 -19
- package/dist/cli/{chunk-qwsrynx5.js.map → chunk-1d7ve1dm.js.map} +4 -4
- package/dist/cli/{chunk-s6jhgk0q.js → chunk-2eytanqx.js} +2 -2
- package/dist/cli/{chunk-kdp5q7ke.js → chunk-2hsdwb9n.js} +18 -19
- package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-2hsdwb9n.js.map} +2 -2
- package/dist/cli/{chunk-hdpx1tax.js → chunk-2z928egk.js} +5 -5
- package/dist/cli/{chunk-jwyddg7y.js → chunk-35d4wj9f.js} +30 -24
- package/dist/cli/chunk-35d4wj9f.js.map +15 -0
- package/dist/cli/{chunk-ah61y8py.js → chunk-364znk6q.js} +2 -2
- package/dist/cli/{chunk-jts8mvcz.js → chunk-3em5wd2y.js} +25 -6
- package/dist/cli/{chunk-jts8mvcz.js.map → chunk-3em5wd2y.js.map} +3 -3
- package/dist/cli/{chunk-s1p84fyh.js → chunk-5f86nr5m.js} +63 -22
- package/dist/cli/chunk-5f86nr5m.js.map +11 -0
- package/dist/cli/{chunk-2hn4b8z7.js → chunk-5m5nmvyq.js} +170 -76
- package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
- package/dist/cli/{chunk-epjnccmv.js → chunk-5xvm6tfj.js} +18 -15
- package/dist/cli/chunk-5xvm6tfj.js.map +10 -0
- package/dist/cli/{chunk-fz5wtpmh.js → chunk-6dsbexzp.js} +18 -15
- package/dist/cli/chunk-6dsbexzp.js.map +10 -0
- package/dist/cli/{chunk-vtk4a6dg.js → chunk-8ktnccpt.js} +1 -1
- package/dist/cli/{chunk-6hsn950k.js → chunk-9t7a85s3.js} +178 -48
- package/dist/cli/chunk-9t7a85s3.js.map +10 -0
- package/dist/cli/{chunk-fxypxtvm.js → chunk-a58773jm.js} +2 -2
- package/dist/cli/{chunk-mb2919y2.js → chunk-acanzt5p.js} +17 -6
- package/dist/cli/chunk-acanzt5p.js.map +10 -0
- package/dist/cli/{chunk-27g6wdth.js → chunk-akbpwfxc.js} +90 -26
- package/dist/cli/chunk-akbpwfxc.js.map +10 -0
- package/dist/cli/{chunk-wm7js3j9.js → chunk-b07cmahc.js} +2 -2
- package/dist/cli/{chunk-wgm7m9qk.js → chunk-c8chx29p.js} +179 -51
- package/dist/cli/chunk-c8chx29p.js.map +36 -0
- package/dist/cli/{chunk-qs4q5p4e.js → chunk-crgn1q09.js} +32 -14
- package/dist/cli/chunk-crgn1q09.js.map +10 -0
- package/dist/cli/chunk-e04dxsz1.js +39 -0
- package/dist/cli/chunk-e04dxsz1.js.map +10 -0
- package/dist/cli/{chunk-yt5n7ppj.js → chunk-ey84smr6.js} +17 -7
- package/dist/cli/chunk-ey84smr6.js.map +10 -0
- package/dist/cli/{chunk-f2z5v128.js → chunk-g4hq16wv.js} +14 -15
- package/dist/cli/{chunk-f2z5v128.js.map → chunk-g4hq16wv.js.map} +2 -2
- package/dist/cli/{chunk-fa25z98p.js → chunk-ga0pf4aj.js} +9 -8
- package/dist/cli/chunk-ga0pf4aj.js.map +11 -0
- package/dist/cli/{chunk-kpf8rrjc.js → chunk-hm3vjy5s.js} +109 -58
- package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
- package/dist/cli/{chunk-fs23ddbb.js → chunk-j85vccga.js} +625 -750
- package/dist/cli/chunk-j85vccga.js.map +36 -0
- package/dist/cli/{chunk-ch6g3ar0.js → chunk-p3v96n38.js} +6 -6
- package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-p3v96n38.js.map} +3 -3
- package/dist/cli/{chunk-q5163e60.js → chunk-p73c0m7w.js} +21 -19
- package/dist/cli/chunk-p73c0m7w.js.map +11 -0
- package/dist/cli/{chunk-dh8cwk36.js → chunk-pehfxfta.js} +24 -9
- package/dist/cli/chunk-pehfxfta.js.map +10 -0
- package/dist/cli/{chunk-qkqwkpte.js → chunk-pv29h0wf.js} +3688 -1313
- package/dist/cli/chunk-pv29h0wf.js.map +190 -0
- package/dist/cli/{chunk-6vm74dry.js → chunk-q58y5e6a.js} +10 -10
- package/dist/cli/{chunk-6vm74dry.js.map → chunk-q58y5e6a.js.map} +3 -3
- package/dist/cli/{chunk-zxcczpyx.js → chunk-r20tn01b.js} +1 -1
- package/dist/cli/{chunk-m3vmjgmq.js → chunk-r9rcc4w7.js} +16 -8
- package/dist/cli/chunk-r9rcc4w7.js.map +10 -0
- package/dist/cli/{chunk-5shv93fd.js → chunk-tkacnehg.js} +2 -2
- package/dist/cli/{chunk-zxh4d9vy.js → chunk-tzmab476.js} +4 -4
- package/dist/cli/{chunk-yw7dm696.js → chunk-vg9r4eb9.js} +24 -11
- package/dist/cli/chunk-vg9r4eb9.js.map +14 -0
- package/dist/cli/{chunk-79jhk4py.js → chunk-wrr3j9w9.js} +260 -132
- package/dist/cli/chunk-wrr3j9w9.js.map +35 -0
- package/dist/cli/{chunk-6crbhc3x.js → chunk-yfyb25rh.js} +54 -27
- package/dist/cli/chunk-yfyb25rh.js.map +14 -0
- package/dist/cli/index.js +162 -35
- package/dist/cli/index.js.map +4 -4
- package/dist/types/ai/agent-surface.d.ts +32 -0
- package/dist/types/ai/api-catalog.d.ts +7 -1
- package/dist/types/ai/ask-context.d.ts +7 -0
- package/dist/types/ai/component-markdown.d.ts +4 -4
- package/dist/types/ai/link-headers.d.ts +8 -1
- package/dist/types/ai/openapi-components.d.ts +5 -2
- package/dist/types/ai/relative-links.d.ts +11 -4
- package/dist/types/ai/skills.d.ts +4 -1
- package/dist/types/ai/static-expression.d.ts +28 -0
- package/dist/types/ai/tar.d.ts +1 -3
- package/dist/types/analytics/databuddy.d.ts +43 -0
- package/dist/types/analytics/index.d.ts +4 -0
- package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
- package/dist/types/analytics/schema.d.ts +28 -0
- package/dist/types/astro/integration.d.ts +3 -2
- package/dist/types/cli/env.d.ts +5 -0
- package/dist/types/cli/init/scaffold.d.ts +19 -3
- package/dist/types/cli/init/starter-spec.d.ts +11 -0
- package/dist/types/core/base-path.d.ts +24 -1
- package/dist/types/core/config-input.d.ts +13 -11
- package/dist/types/core/config.d.ts +2 -2
- package/dist/types/core/directive-diagnostics.d.ts +12 -0
- package/dist/types/core/graph.d.ts +2 -0
- package/dist/types/core/heading-markers.d.ts +5 -7
- package/dist/types/core/i18n-ui.d.ts +31 -0
- package/dist/types/core/i18n.d.ts +9 -1
- package/dist/types/core/last-modified.d.ts +10 -0
- package/dist/types/core/links.d.ts +3 -1
- package/dist/types/core/load-module.d.ts +10 -0
- package/dist/types/core/locale-links.d.ts +12 -2
- package/dist/types/core/meta.d.ts +13 -1
- package/dist/types/core/nav-diagnostics.d.ts +10 -0
- package/dist/types/core/navigation.d.ts +38 -0
- package/dist/types/core/ordering-prefix.d.ts +4 -0
- package/dist/types/core/safe-links.d.ts +3 -1
- package/dist/types/core/schema.d.ts +59 -13
- package/dist/types/core/sources/github-releases.d.ts +5 -0
- package/dist/types/core/sources/lower.d.ts +38 -13
- package/dist/types/core/sources/normalize.d.ts +23 -2
- package/dist/types/core/sources/remote.d.ts +11 -1
- package/dist/types/core/sources/resolve.d.ts +12 -0
- package/dist/types/core/sources/types.d.ts +31 -0
- package/dist/types/core/sources/watch.d.ts +12 -5
- package/dist/types/core/standard-schema.d.ts +5 -0
- package/dist/types/core/types.d.ts +9 -0
- package/dist/types/deploy/adapters/node.d.ts +5 -2
- package/dist/types/deploy/adapters/types.d.ts +7 -0
- package/dist/types/deploy/artifacts.d.ts +6 -4
- package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
- package/dist/types/deploy/headers.d.ts +36 -7
- package/dist/types/deploy/node-headers.d.ts +43 -8
- package/dist/types/deploy/platforms/netlify.d.ts +27 -2
- package/dist/types/deploy/platforms/node.d.ts +6 -5
- package/dist/types/deploy/platforms/types.d.ts +15 -0
- package/dist/types/deploy/platforms/vercel.d.ts +3 -2
- package/dist/types/deploy/redirects.d.ts +31 -15
- package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
- package/dist/types/markdown/directives.d.ts +62 -0
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/markdown/mdast.d.ts +63 -0
- package/dist/types/openapi/asyncapi.d.ts +4 -2
- package/dist/types/openapi/model.d.ts +20 -6
- package/dist/types/search/sync/algolia.d.ts +3 -1
- package/docs/01-quickstart.mdx +3 -2
- package/docs/02-deployment.mdx +20 -9
- package/docs/08-faq.mdx +10 -3
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/advanced/custom-pages.mdx +7 -5
- package/docs/cli/audit.mdx +19 -3
- package/docs/cli/doctor.mdx +2 -2
- package/docs/cli/evals.mdx +4 -4
- package/docs/cli/index.mdx +4 -1
- package/docs/cli/translate.mdx +4 -4
- package/docs/cli/version.mdx +1 -1
- package/docs/configuration/analytics.mdx +42 -2
- package/docs/configuration/assistant.mdx +1 -1
- package/docs/configuration/customization.mdx +6 -3
- package/docs/configuration/index.mdx +5 -3
- package/docs/configuration/search.mdx +2 -2
- package/docs/content/components.mdx +2 -2
- package/docs/content/frontmatter.mdx +5 -1
- package/docs/content/i18n.mdx +3 -1
- package/docs/content/index.mdx +8 -4
- package/docs/content/islands.mdx +1 -1
- package/docs/content/meta.mdx +7 -3
- package/docs/content/navigation.mdx +29 -4
- package/docs/content/sources.mdx +18 -16
- package/docs/content/syntax.mdx +14 -0
- package/docs/content/versioning.mdx +2 -1
- package/docs/discoverability/agent-discovery.mdx +22 -8
- package/docs/discoverability/index.mdx +4 -3
- package/docs/discoverability/llms-txt.mdx +2 -5
- package/docs/discoverability/markdown.mdx +6 -4
- package/docs/discoverability/mcp.mdx +4 -0
- package/docs/discoverability/metadata.mdx +3 -2
- package/docs/discoverability/open-graph.mdx +8 -4
- package/docs/discoverability/rss.mdx +4 -2
- package/docs/references/asyncapi.mdx +2 -2
- package/docs/references/graphql.mdx +2 -2
- package/docs/references/openapi.mdx +6 -4
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +6 -6
- package/skills/blume-migrate/references/docusaurus.md +6 -6
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +3 -3
- package/skills/blume-migrate/references/monorepo.md +1 -1
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +5 -5
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +9 -4
- package/skills/blume-update-docs/references/audit-checklist.md +1 -1
- package/src/ai/agent-readability.ts +2 -2
- package/src/ai/agent-surface.ts +56 -0
- package/src/ai/ai-catalog.ts +6 -2
- package/src/ai/api/handlers.ts +2 -2
- package/src/ai/api/spec.ts +17 -4
- package/src/ai/api-catalog.ts +13 -3
- package/src/ai/ask-context.ts +14 -2
- package/src/ai/ask-data.ts +26 -12
- package/src/ai/changelog-markdown.ts +2 -2
- package/src/ai/component-markdown.ts +43 -37
- package/src/ai/link-headers.ts +22 -8
- package/src/ai/llms.ts +22 -10
- package/src/ai/markdown.ts +23 -11
- package/src/ai/mcp/discovery.ts +2 -2
- package/src/ai/mcp/query.ts +2 -2
- package/src/ai/mcp/server.ts +80 -9
- package/src/ai/openapi-components.ts +22 -6
- package/src/ai/relative-links.ts +99 -15
- package/src/ai/serializers.ts +2 -1
- package/src/ai/skills.ts +14 -1
- package/src/ai/static-expression.ts +416 -0
- package/src/ai/tar.ts +139 -12
- package/src/ai/visibility.ts +45 -14
- package/src/analytics/databuddy.ts +67 -0
- package/src/analytics/head.ts +8 -0
- package/src/analytics/index.ts +7 -0
- package/src/analytics/one-dollar-stats.ts +86 -0
- package/src/analytics/posthog.ts +21 -3
- package/src/analytics/schema.ts +4 -0
- package/src/astro/generate.ts +72 -28
- package/src/astro/include-hmr.ts +31 -12
- package/src/astro/include-refresh.ts +19 -8
- package/src/astro/integration.ts +108 -14
- package/src/astro/runtime-modules.ts +28 -13
- package/src/astro/templates.ts +139 -57
- package/src/audit/catalog.ts +2 -2
- package/src/audit/checks/assets.ts +25 -5
- package/src/audit/checks/content.ts +20 -2
- package/src/audit/checks/i18n.ts +46 -8
- package/src/audit/checks/indexability.ts +39 -19
- package/src/audit/checks/links.ts +22 -1
- package/src/audit/checks/llms.ts +6 -3
- package/src/audit/checks/network.ts +3 -1
- package/src/audit/checks/og-image.ts +10 -0
- package/src/audit/checks/robots.ts +6 -1
- package/src/audit/checks/sitemap.ts +56 -31
- package/src/audit/checks/social.ts +21 -2
- package/src/audit/crawl.ts +88 -15
- package/src/audit/graph.ts +3 -1
- package/src/audit/report.ts +54 -17
- package/src/audit/run.ts +13 -9
- package/src/audit/snapshot.ts +5 -0
- package/src/audit/types.ts +18 -0
- package/src/audit/url.ts +37 -6
- package/src/cli/args.ts +32 -0
- package/src/cli/build-failure.ts +50 -0
- package/src/cli/commands/audit.ts +11 -1
- package/src/cli/commands/build.ts +19 -5
- package/src/cli/commands/check.ts +2 -0
- package/src/cli/commands/doctor.ts +3 -1
- package/src/cli/commands/eval.ts +20 -18
- package/src/cli/commands/init.ts +15 -5
- package/src/cli/commands/preview.ts +15 -0
- package/src/cli/commands/sync.ts +2 -0
- package/src/cli/commands/translate.ts +18 -18
- package/src/cli/commands/upgrade.ts +11 -0
- package/src/cli/commands/validate.ts +3 -1
- package/src/cli/dev-lock.ts +157 -37
- package/src/cli/eject-scripts.ts +32 -7
- package/src/cli/env.ts +12 -1
- package/src/cli/init/scaffold.ts +117 -11
- package/src/cli/init/starter-spec.ts +235 -0
- package/src/cli/report-format.ts +11 -6
- package/src/components/colors.ts +19 -0
- package/src/components/content/AccordionItem.astro +26 -22
- package/src/components/content/Badge.astro +7 -12
- package/src/components/content/Card.astro +2 -2
- package/src/components/content/Component.astro +11 -5
- package/src/components/content/Expandable.astro +5 -1
- package/src/components/content/Frame.astro +2 -7
- package/src/components/content/GithubInfo.astro +12 -2
- package/src/components/content/Prompt.astro +2 -7
- package/src/components/content/Tab.astro +0 -1
- package/src/components/content/Tabs.astro +58 -5
- package/src/components/content/Tile.astro +1 -1
- package/src/components/content/Tooltip.astro +69 -7
- package/src/components/content/Tree.astro +7 -2
- package/src/components/content/TypeTable.astro +10 -5
- package/src/components/content/Update.astro +8 -2
- package/src/components/content/auto-type-table.ts +4 -1
- package/src/components/content/badge-color.ts +20 -0
- package/src/components/content/base-href.ts +14 -22
- package/src/components/content/inline-markdown.ts +27 -7
- package/src/components/copy-feedback.ts +35 -8
- package/src/components/islands/assistant.tsx +37 -6
- package/src/components/islands/base-path.ts +47 -10
- package/src/components/islands/hooks.ts +42 -21
- package/src/components/islands/webmcp.ts +16 -10
- package/src/components/layout/Banner.astro +23 -4
- package/src/components/layout/Breadcrumbs.astro +5 -2
- package/src/components/layout/DiscoveryLinks.astro +9 -5
- package/src/components/layout/Header.astro +23 -10
- package/src/components/layout/LanguageSwitcher.astro +2 -2
- package/src/components/layout/Logo.astro +5 -0
- package/src/components/layout/NavSelector.astro +8 -3
- package/src/components/layout/NavTabMenu.astro +133 -0
- package/src/components/layout/NavTree.astro +31 -12
- package/src/components/layout/NavTreeCache.astro +5 -2
- package/src/components/layout/NavTreeScript.astro +45 -5
- package/src/components/layout/PageActions.astro +4 -3
- package/src/components/layout/PageLayout.astro +28 -13
- package/src/components/layout/Pagination.astro +3 -3
- package/src/components/layout/ReferenceLayout.astro +4 -1
- package/src/components/layout/RootLayout.astro +25 -12
- package/src/components/layout/Search.astro +38 -14
- package/src/components/layout/VersionBanner.astro +2 -2
- package/src/components/layout/analytics-client.ts +14 -0
- package/src/components/layout/toc-active.ts +41 -0
- package/src/components/layout/toc-element.ts +8 -14
- package/src/components/openapi/ApiTagOperations.astro +2 -2
- package/src/components/openapi/AsyncApiOperation.astro +7 -4
- package/src/components/openapi/GraphqlChip.astro +2 -2
- package/src/components/openapi/GraphqlType.astro +11 -3
- package/src/components/openapi/MessageComposer.astro +1 -1
- package/src/components/openapi/Operation.astro +23 -5
- package/src/components/openapi/PanelTabs.astro +4 -1
- package/src/components/openapi/Playground.astro +8 -2
- package/src/components/openapi/RequestPanel.astro +13 -6
- package/src/components/openapi/SchemaProperty.astro +11 -33
- package/src/components/openapi/SchemaTable.astro +34 -52
- package/src/components/openapi/async.ts +38 -6
- package/src/components/openapi/helpers.ts +100 -12
- package/src/components/openapi/message-composer.ts +14 -1
- package/src/components/openapi/message-model.ts +12 -2
- package/src/components/openapi/message.ts +21 -3
- package/src/components/openapi/operation-model.ts +119 -17
- package/src/components/openapi/panel.ts +31 -5
- package/src/components/openapi/param-style.ts +181 -0
- package/src/components/openapi/playground-client.ts +110 -23
- package/src/components/openapi/playground-schema.ts +25 -6
- package/src/components/openapi/request.ts +192 -26
- package/src/components/openapi/schema-tree.ts +209 -0
- package/src/components/openapi/snippets.ts +113 -19
- package/src/components/openapi/validate-json.ts +1 -1
- package/src/components/openapi/ws-client.ts +18 -2
- package/src/core/base-path.ts +69 -8
- package/src/core/config-input.ts +13 -11
- package/src/core/config.ts +18 -4
- package/src/core/diagnostics.ts +219 -30
- package/src/core/directive-diagnostics.ts +99 -0
- package/src/core/frontmatter.ts +21 -18
- package/src/core/graph.ts +120 -11
- package/src/core/heading-markers.ts +5 -18
- package/src/core/i18n-ui.ts +39 -2
- package/src/core/i18n.ts +44 -5
- package/src/core/last-modified.ts +25 -3
- package/src/core/links.ts +7 -4
- package/src/core/load-module.ts +20 -0
- package/src/core/locale-links.ts +22 -18
- package/src/core/manifest.ts +3 -2
- package/src/core/meta.ts +89 -12
- package/src/core/nav-diagnostics.ts +56 -1
- package/src/core/navigation.ts +272 -83
- package/src/core/ordering-prefix.ts +27 -0
- package/src/core/project-graph.ts +50 -6
- package/src/core/safe-href.ts +53 -1
- package/src/core/safe-links.ts +11 -2
- package/src/core/schema.ts +94 -18
- package/src/core/sources/assets.ts +83 -41
- package/src/core/sources/contentful-rich-text.ts +28 -20
- package/src/core/sources/contentful.ts +25 -12
- package/src/core/sources/filesystem.ts +25 -3
- package/src/core/sources/github-releases.ts +131 -11
- package/src/core/sources/lexical.ts +30 -20
- package/src/core/sources/lower.ts +276 -56
- package/src/core/sources/mdx-remote.ts +52 -16
- package/src/core/sources/normalize.ts +456 -119
- package/src/core/sources/notion.ts +77 -34
- package/src/core/sources/obsidian.ts +40 -9
- package/src/core/sources/payload.ts +1 -0
- package/src/core/sources/portable-text.ts +85 -49
- package/src/core/sources/remote.ts +18 -2
- package/src/core/sources/resolve.ts +52 -33
- package/src/core/sources/sanity.ts +1 -0
- package/src/core/sources/strapi-blocks.ts +28 -19
- package/src/core/sources/strapi.ts +1 -0
- package/src/core/sources/types.ts +31 -0
- package/src/core/sources/watch.ts +20 -7
- package/src/core/standard-schema.ts +10 -6
- package/src/core/types.ts +9 -0
- package/src/core/ui-packs/ar.ts +13 -0
- package/src/core/ui-packs/bg.ts +13 -0
- package/src/core/ui-packs/bn.ts +13 -0
- package/src/core/ui-packs/ca.ts +13 -0
- package/src/core/ui-packs/cs.ts +13 -0
- package/src/core/ui-packs/da.ts +13 -0
- package/src/core/ui-packs/de.ts +13 -0
- package/src/core/ui-packs/el.ts +13 -0
- package/src/core/ui-packs/es.ts +13 -0
- package/src/core/ui-packs/fa.ts +13 -0
- package/src/core/ui-packs/fi.ts +13 -0
- package/src/core/ui-packs/fr.ts +13 -0
- package/src/core/ui-packs/he.ts +13 -0
- package/src/core/ui-packs/hi.ts +13 -0
- package/src/core/ui-packs/hr.ts +13 -0
- package/src/core/ui-packs/hu.ts +13 -0
- package/src/core/ui-packs/id.ts +13 -0
- package/src/core/ui-packs/it.ts +13 -0
- package/src/core/ui-packs/ja.ts +13 -0
- package/src/core/ui-packs/ko.ts +13 -0
- package/src/core/ui-packs/nl.ts +13 -0
- package/src/core/ui-packs/no.ts +13 -0
- package/src/core/ui-packs/pl.ts +13 -0
- package/src/core/ui-packs/pt-br.ts +13 -0
- package/src/core/ui-packs/pt.ts +13 -0
- package/src/core/ui-packs/ro.ts +13 -0
- package/src/core/ui-packs/ru.ts +13 -0
- package/src/core/ui-packs/sk.ts +13 -0
- package/src/core/ui-packs/sr.ts +13 -0
- package/src/core/ui-packs/sv.ts +13 -0
- package/src/core/ui-packs/th.ts +13 -0
- package/src/core/ui-packs/tr.ts +13 -0
- package/src/core/ui-packs/uk.ts +13 -0
- package/src/core/ui-packs/vi.ts +13 -0
- package/src/core/ui-packs/zh-tw.ts +13 -0
- package/src/core/ui-packs/zh.ts +13 -0
- package/src/core/version-cut.ts +69 -17
- package/src/core/versions.ts +4 -1
- package/src/deploy/adapters/node.ts +5 -2
- package/src/deploy/adapters/registry.ts +2 -1
- package/src/deploy/adapters/types.ts +13 -1
- package/src/deploy/artifacts.ts +52 -11
- package/src/deploy/cloudflare-negotiation.ts +27 -16
- package/src/deploy/headers.ts +75 -55
- package/src/deploy/node-headers.ts +148 -27
- package/src/deploy/platforms/cloudflare.ts +13 -3
- package/src/deploy/platforms/netlify.ts +83 -5
- package/src/deploy/platforms/node.ts +8 -5
- package/src/deploy/platforms/static.ts +2 -0
- package/src/deploy/platforms/types.ts +15 -0
- package/src/deploy/platforms/vercel.ts +10 -3
- package/src/deploy/redirects.ts +68 -21
- package/src/deploy/robots.ts +2 -2
- package/src/deploy/rss.ts +4 -3
- package/src/deploy/sitemap.ts +7 -5
- package/src/deploy/vercel-negotiation.ts +35 -3
- package/src/eval/agents.ts +10 -2
- package/src/eval/run.ts +25 -0
- package/src/markdown/base-links.ts +74 -29
- package/src/markdown/directives.ts +242 -36
- package/src/markdown/features.ts +17 -0
- package/src/markdown/index.ts +13 -9
- package/src/markdown/mdast.ts +5 -2
- package/src/markdown/relative-links.ts +15 -27
- package/src/markdown/route-snapshot.ts +37 -0
- package/src/og/card.ts +149 -9
- package/src/og/derive.ts +156 -5
- package/src/og/index.ts +1 -0
- package/src/openapi/asyncapi.ts +4 -2
- package/src/openapi/graphql-build.ts +28 -2
- package/src/openapi/model.ts +99 -27
- package/src/openapi/proxy.ts +63 -10
- package/src/openapi/render-mdx.ts +51 -3
- package/src/registry/eject.ts +297 -43
- package/src/search/adapters/version-scope.ts +30 -0
- package/src/search/documents.ts +89 -27
- package/src/search/popular.ts +2 -1
- package/src/search/sync/algolia.ts +36 -2
- package/src/seo/jsonld.ts +7 -3
- package/src/sources/registry.ts +5 -0
- package/src/theme/entry.ts +11 -4
- package/src/translate/agents.ts +6 -1
- package/src/translate/ledger.ts +26 -3
- package/src/translate/meta.ts +76 -24
- package/src/translate/run.ts +14 -8
- package/src/translate/validate.ts +14 -2
- package/src/translate/work-list.ts +91 -19
- package/src/upgrade/upgrade.ts +36 -4
- package/dist/cli/chunk-27g6wdth.js.map +0 -10
- package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
- package/dist/cli/chunk-6crbhc3x.js.map +0 -14
- package/dist/cli/chunk-6hsn950k.js.map +0 -10
- package/dist/cli/chunk-79jhk4py.js.map +0 -35
- package/dist/cli/chunk-82bbrxdn.js +0 -51
- package/dist/cli/chunk-82bbrxdn.js.map +0 -10
- package/dist/cli/chunk-abh8yjkn.js +0 -31
- package/dist/cli/chunk-abh8yjkn.js.map +0 -10
- package/dist/cli/chunk-ce574jw2.js +0 -23
- package/dist/cli/chunk-ce574jw2.js.map +0 -10
- package/dist/cli/chunk-dh8cwk36.js.map +0 -10
- package/dist/cli/chunk-epjnccmv.js.map +0 -10
- package/dist/cli/chunk-fa25z98p.js.map +0 -11
- package/dist/cli/chunk-fs23ddbb.js.map +0 -35
- package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
- package/dist/cli/chunk-jwyddg7y.js.map +0 -15
- package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
- package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
- package/dist/cli/chunk-mb2919y2.js.map +0 -10
- package/dist/cli/chunk-q5163e60.js.map +0 -11
- package/dist/cli/chunk-qkqwkpte.js.map +0 -182
- package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
- package/dist/cli/chunk-s1p84fyh.js.map +0 -10
- package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
- package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
- package/dist/cli/chunk-yw7dm696.js.map +0 -14
- /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-2eytanqx.js.map} +0 -0
- /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-2z928egk.js.map} +0 -0
- /package/dist/cli/{chunk-ah61y8py.js.map → chunk-364znk6q.js.map} +0 -0
- /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-8ktnccpt.js.map} +0 -0
- /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-a58773jm.js.map} +0 -0
- /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-b07cmahc.js.map} +0 -0
- /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-r20tn01b.js.map} +0 -0
- /package/dist/cli/{chunk-5shv93fd.js.map → chunk-tkacnehg.js.map} +0 -0
- /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-tzmab476.js.map} +0 -0
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
3
|
import GithubSlugger from "github-slugger";
|
|
4
|
+
import type { Nodes } from "mdast";
|
|
4
5
|
import { extname } from "pathe";
|
|
6
|
+
import { markdownToMdast } from "satteri";
|
|
5
7
|
|
|
6
|
-
import {
|
|
8
|
+
import { MARKDOWN_FEATURES } from "../../markdown/features.ts";
|
|
9
|
+
import { mountBasePath } from "../base-path.ts";
|
|
7
10
|
import { nextFenceState } from "../code-fences.ts";
|
|
8
11
|
import type { FenceState } from "../code-fences.ts";
|
|
9
12
|
import { diagnosticsFromIssues, diagnosticsFromZod } from "../diagnostics.ts";
|
|
10
13
|
import { occupySlug, parseHeadingMarkers } from "../heading-markers.ts";
|
|
11
14
|
import { localePlacement, localizeRoute } from "../i18n.ts";
|
|
15
|
+
import { titleWord } from "../navigation.ts";
|
|
16
|
+
import { stripOrderingPrefix } from "../ordering-prefix.ts";
|
|
12
17
|
import { pageMetaSchema } from "../schema.ts";
|
|
13
18
|
import type {
|
|
14
19
|
FrontmatterExtend,
|
|
@@ -20,18 +25,25 @@ import type { Diagnostic, Heading, PageLink, PageRecord } from "../types.ts";
|
|
|
20
25
|
import { detectVersionRef, versionizeRoute } from "../versions.ts";
|
|
21
26
|
import type { NormalizeContext, SourceEntry } from "./types.ts";
|
|
22
27
|
|
|
23
|
-
const NUMERIC_PREFIX = /^\d+[-_.]/u;
|
|
24
28
|
const GROUP_FOLDER = /^\((?<label>.+)\)$/u;
|
|
25
29
|
const WORD_SPLIT = /[-_]/u;
|
|
26
30
|
|
|
27
31
|
/** Strip a leading numeric ordering prefix (`01-intro` -> `intro`). */
|
|
28
32
|
const stripNumericPrefix = (segment: string): string =>
|
|
29
|
-
segment
|
|
33
|
+
stripOrderingPrefix(segment);
|
|
30
34
|
|
|
31
35
|
/** Detect a group folder `(name)` and return its label, else null. */
|
|
32
36
|
const groupLabel = (segment: string): string | null =>
|
|
33
37
|
segment.match(GROUP_FOLDER)?.groups?.label ?? null;
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* {@link groupLabel} for a file or folder name, which may carry its ordering
|
|
41
|
+
* prefix outside the parentheses: `01-(guides)` is the `guides` group, sorted
|
|
42
|
+
* first, the way `01-guides` is the `guides` folder.
|
|
43
|
+
*/
|
|
44
|
+
const orderedGroupLabel = (segment: string): string | null =>
|
|
45
|
+
groupLabel(segment) ?? groupLabel(stripNumericPrefix(segment));
|
|
46
|
+
|
|
35
47
|
/**
|
|
36
48
|
* Slugify a content/route slug (Sanity, Notion, frontmatter `slug`). Heading
|
|
37
49
|
* anchor ids are *not* slugged here — they use a `github-slugger` in
|
|
@@ -63,13 +75,9 @@ export const slugify = (text: string): string =>
|
|
|
63
75
|
export const slugifyPath = (text: string): string =>
|
|
64
76
|
text.split("/").map(slugify).filter(Boolean).join("/");
|
|
65
77
|
|
|
66
|
-
/** Title-case a slug segment for display. */
|
|
78
|
+
/** Title-case a slug segment for display, acronyms as the sidebar spells them. */
|
|
67
79
|
const titleCase = (value: string): string =>
|
|
68
|
-
value
|
|
69
|
-
.split(WORD_SPLIT)
|
|
70
|
-
.filter(Boolean)
|
|
71
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
72
|
-
.join(" ");
|
|
80
|
+
value.split(WORD_SPLIT).filter(Boolean).map(titleWord).join(" ");
|
|
73
81
|
|
|
74
82
|
/**
|
|
75
83
|
* Strip characters that cannot survive the route → URL → output-file round
|
|
@@ -77,29 +85,68 @@ const titleCase = (value: string): string =>
|
|
|
77
85
|
* scheme (`Guide: Architecture.md` → `guide:`), which crashes Astro's
|
|
78
86
|
* prerender write with "The URL must be of scheme file"; control characters
|
|
79
87
|
* (an embedded newline in a filename) are silently dropped by the URL parser,
|
|
80
|
-
* desyncing the route from its output path.
|
|
81
|
-
*
|
|
88
|
+
* desyncing the route from its output path. `#` and `?` start a URL's
|
|
89
|
+
* fragment and query, so an unescaped `/sdks/c#` link lands on `/sdks/c`
|
|
90
|
+
* while Astro writes the page to `c%23/`; a `%` starts an escape, and a bare
|
|
91
|
+
* one is an invalid URL Astro can't decode (and one it escapes, `%25`, finds
|
|
92
|
+
* no static path). All are legal in macOS/Linux filenames, so they are
|
|
93
|
+
* removed here rather than rejected: `100%.md` publishes at `/100`, and a
|
|
94
|
+
* slug of `sdks/c#` at `/sdks/c`. A file whose own path holds `#` or `?`
|
|
95
|
+
* never gets this far — Astro can't load it (see
|
|
96
|
+
* {@link unloadablePathDiagnostic}).
|
|
82
97
|
*/
|
|
83
98
|
const sanitizeSegment = (segment: string): string =>
|
|
84
|
-
segment.replaceAll(/[
|
|
99
|
+
segment.replaceAll(/[:#?%\p{Cc}]/gu, "");
|
|
100
|
+
|
|
101
|
+
// Astro's content loader reads each entry at `new URL("./" + encodeURI(entry),
|
|
102
|
+
// base)`. `encodeURI` escapes `%` but leaves `#` and `?` alone, so in a path
|
|
103
|
+
// they start the URL's fragment and query, and the read misses the file.
|
|
104
|
+
const UNLOADABLE_PATH = /[#?]/u;
|
|
85
105
|
|
|
86
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* The error for a content file Astro's content loader can't read, or
|
|
108
|
+
* undefined when it can. A `#` or `?` anywhere in the path the loader is
|
|
109
|
+
* handed (`sdks/c#.md`, `faq/why?.md`) truncates the file URL it reads
|
|
110
|
+
* through, so the read fails with ENOENT and the page renders "Page not
|
|
111
|
+
* found" at its route. A source leaves such a file out of its scan and
|
|
112
|
+
* reports this instead of publishing a route that can never render.
|
|
113
|
+
*/
|
|
114
|
+
export const unloadablePathDiagnostic = (
|
|
115
|
+
path: string,
|
|
116
|
+
file: string
|
|
117
|
+
): Diagnostic | undefined =>
|
|
118
|
+
UNLOADABLE_PATH.test(path)
|
|
119
|
+
? {
|
|
120
|
+
code: "BLUME_UNLOADABLE_FILE_NAME",
|
|
121
|
+
file,
|
|
122
|
+
message: `"${path}" has a "#" or "?" in its path, which Astro's content loader reads as the start of a URL fragment or query, so it can't load the file. It was left out of the site.`,
|
|
123
|
+
severity: "error",
|
|
124
|
+
suggestion:
|
|
125
|
+
'Rename the file (or its folder) without "#" or "?". Both are dropped from the page\'s URL anyway, so the page keeps its route.',
|
|
126
|
+
}
|
|
127
|
+
: undefined;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Fold one raw path part into the accumulating route segments/groups.
|
|
131
|
+
* `ordered` parts are file or folder names, whose ordering prefix is dropped.
|
|
132
|
+
*/
|
|
87
133
|
const addRouteSegment = (
|
|
88
134
|
part: string,
|
|
89
135
|
segments: string[],
|
|
90
|
-
groups: string[]
|
|
136
|
+
groups: string[],
|
|
137
|
+
ordered: boolean
|
|
91
138
|
): void => {
|
|
92
139
|
// A leading/trailing/double slash yields an empty part; keeping it would
|
|
93
140
|
// produce a malformed route (`//foo`, `/foo/`) that nothing can link to.
|
|
94
141
|
if (part === "") {
|
|
95
142
|
return;
|
|
96
143
|
}
|
|
97
|
-
const group = groupLabel(part);
|
|
144
|
+
const group = ordered ? orderedGroupLabel(part) : groupLabel(part);
|
|
98
145
|
if (group !== null) {
|
|
99
146
|
groups.push(group);
|
|
100
147
|
return;
|
|
101
148
|
}
|
|
102
|
-
const clean = stripNumericPrefix(part);
|
|
149
|
+
const clean = ordered ? stripNumericPrefix(part) : part;
|
|
103
150
|
if (clean === "index") {
|
|
104
151
|
return;
|
|
105
152
|
}
|
|
@@ -119,22 +166,31 @@ interface MappedRoute {
|
|
|
119
166
|
}
|
|
120
167
|
|
|
121
168
|
/**
|
|
122
|
-
* Convert a content-root-relative path into URL +
|
|
123
|
-
*
|
|
169
|
+
* Convert a source's route prefix and a content-root-relative path into URL +
|
|
170
|
+
* nav metadata. Only the path's parts lose an ordering prefix, and only when
|
|
171
|
+
* `ordered` says they are file and folder names: the route prefix, a slug,
|
|
172
|
+
* a release tag, or a CMS slug is a route spelled out, kept as written. Not
|
|
173
|
+
* exported: a source that needs to predict a route goes through
|
|
124
174
|
* {@link resolveEntryRoute}, so there is exactly one derivation.
|
|
125
175
|
*/
|
|
126
|
-
const mapRoute = (
|
|
176
|
+
const mapRoute = (
|
|
177
|
+
prefix: string | undefined,
|
|
178
|
+
relativePath: string,
|
|
179
|
+
ordered: boolean
|
|
180
|
+
): MappedRoute => {
|
|
127
181
|
const withoutExt = relativePath.slice(
|
|
128
182
|
0,
|
|
129
183
|
relativePath.length - extname(relativePath).length
|
|
130
184
|
);
|
|
131
|
-
const rawParts = withoutExt.split("/");
|
|
132
185
|
|
|
133
186
|
const segments: string[] = [];
|
|
134
187
|
const groups: string[] = [];
|
|
135
188
|
|
|
136
|
-
for (const part of
|
|
137
|
-
addRouteSegment(part, segments, groups);
|
|
189
|
+
for (const part of prefix ? prefix.split("/") : []) {
|
|
190
|
+
addRouteSegment(part, segments, groups, false);
|
|
191
|
+
}
|
|
192
|
+
for (const part of withoutExt.split("/")) {
|
|
193
|
+
addRouteSegment(part, segments, groups, ordered);
|
|
138
194
|
}
|
|
139
195
|
|
|
140
196
|
const route = segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
@@ -156,7 +212,6 @@ const SETEXT_UNDERLINE = /^ {0,3}(?<marker>=+|-+)\s*$/u;
|
|
|
156
212
|
const PARAGRAPH_INTERRUPT = /^ {0,3}(?:[-+*][ \t]|\d{1,9}[.)][ \t]|>)/u;
|
|
157
213
|
const THEMATIC_BREAK =
|
|
158
214
|
/^ {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$/u;
|
|
159
|
-
const FRONT_MATTER_CLOSE = /^(?:-{3}|\.{3})\s*$/u;
|
|
160
215
|
// `<Prompt>` renders its children into a permanently `hidden` DOM node (see
|
|
161
216
|
// `Prompt.astro`) — the agent-facing prompt text is never visible page
|
|
162
217
|
// content, only read by client JS for the copy button. Any `##` inside it
|
|
@@ -173,41 +228,13 @@ const PROMPT_OPEN = /^<Prompt(?![\w-])/u;
|
|
|
173
228
|
// children text (`...copy this.</Prompt>`), not just sit on its own line.
|
|
174
229
|
const PROMPT_CLOSE = /<\/Prompt>/u;
|
|
175
230
|
|
|
176
|
-
/**
|
|
177
|
-
* The body lines, minus a leading front matter block, plus the height of the
|
|
178
|
-
* block that was dropped (`offset`) so line numbers can be reported against
|
|
179
|
-
* the whole body. Bodies from the normalize pipeline are already
|
|
180
|
-
* frontmatter-stripped, but `scanBody` also runs on raw documents — where a
|
|
181
|
-
* leading `---` block (closed by `---` or `...`) is front matter, not a
|
|
182
|
-
* thematic break whose closing `---` would underline the last metadata line
|
|
183
|
-
* into a phantom setext heading.
|
|
184
|
-
*/
|
|
185
|
-
const linesWithoutFrontMatter = (body: string) => {
|
|
186
|
-
const lines = body.split("\n");
|
|
187
|
-
if (!/^-{3}\s*$/u.test(lines[0] ?? "")) {
|
|
188
|
-
return { lines, offset: 0 };
|
|
189
|
-
}
|
|
190
|
-
// A blank line directly after the dashes means the body *opens* with a
|
|
191
|
-
// thematic break, not front matter — YAML metadata starts on the very next
|
|
192
|
-
// line. Treating it as an unclosed block ate everything up to the next
|
|
193
|
-
// `---`/`...` line of an already-stripped body.
|
|
194
|
-
if ((lines[1] ?? "").trim() === "") {
|
|
195
|
-
return { lines, offset: 0 };
|
|
196
|
-
}
|
|
197
|
-
const close = lines.findIndex(
|
|
198
|
-
(line, index) => index > 0 && FRONT_MATTER_CLOSE.test(line)
|
|
199
|
-
);
|
|
200
|
-
return close === -1
|
|
201
|
-
? { lines, offset: 0 }
|
|
202
|
-
: { lines: lines.slice(close + 1), offset: close + 1 };
|
|
203
|
-
};
|
|
204
|
-
|
|
205
231
|
// A trailing `{#id}` heading marker written without a backslash escape. Both
|
|
206
|
-
// pipelines resolve escapes before parsing markers (
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
232
|
+
// pipelines resolve escapes before parsing markers (the scan parses each
|
|
233
|
+
// heading with the renderer's grammar — see `renderHeading`), so `\{#id\}` is
|
|
234
|
+
// the same marker — and the only spelling that survives the MDX parser, where
|
|
235
|
+
// a bare `{…}` is a JSX expression and `#id` is not a valid one (`Could not
|
|
236
|
+
// parse expression with acorn`). Further bracket markers may follow the brace
|
|
237
|
+
// (`{#id} [toc]`), nothing else.
|
|
211
238
|
const BARE_CURLY_MARKER =
|
|
212
239
|
/(?<!\\)\{#(?<id>[^\s}]+)\}(?:\s*\[(?:#[^\s\]]+|!?toc)\])*\s*$/u;
|
|
213
240
|
|
|
@@ -241,6 +268,8 @@ interface HeadingScanState {
|
|
|
241
268
|
* end so a tag wrapped over several lines still matches.
|
|
242
269
|
*/
|
|
243
270
|
anchorLines: string[];
|
|
271
|
+
/** The multi-line comment the scan is inside, if any — see `scanCommentLine`. */
|
|
272
|
+
comment: "html" | "jsx" | null;
|
|
244
273
|
curlyMarkers: CurlyMarker[];
|
|
245
274
|
fence: FenceState;
|
|
246
275
|
/** 1-based body line of the line being scanned. */
|
|
@@ -299,12 +328,31 @@ const finishPromptTag = (
|
|
|
299
328
|
* collapses `--`; github-slugger keeps it) and resolves repeated headings the
|
|
300
329
|
* same way (`setup`, `setup-1`).
|
|
301
330
|
*/
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
const
|
|
331
|
+
// A heading's inline Markdown is parsed with the renderer's own grammar:
|
|
332
|
+
// Blume's `.md` feature set plus the Astro defaults Blume never turns off, GFM
|
|
333
|
+
// and smart punctuation (Astro's `smartypants`). Front matter is off — the
|
|
334
|
+
// source is a lone heading. The display text is read without smart
|
|
335
|
+
// punctuation (see `toHeading`).
|
|
336
|
+
const HEADING_FEATURES = {
|
|
337
|
+
...MARKDOWN_FEATURES,
|
|
338
|
+
frontmatter: false,
|
|
339
|
+
gfm: true,
|
|
340
|
+
};
|
|
341
|
+
const HEADING_PARSE = {
|
|
342
|
+
features: { ...HEADING_FEATURES, smartPunctuation: true },
|
|
343
|
+
};
|
|
344
|
+
const HEADING_TEXT_PARSE = {
|
|
345
|
+
features: { ...HEADING_FEATURES, smartPunctuation: false },
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Characters that can make a heading's rendered text differ from its source:
|
|
349
|
+
// escapes, code spans, emphasis/strikethrough/sub/superscript markers, link and
|
|
350
|
+
// image brackets, raw HTML and autolinks, entities, and the quotes, dashes, and
|
|
351
|
+
// ellipses smart punctuation rewrites. A heading with none of them renders as
|
|
352
|
+
// written, so it skips the parse.
|
|
353
|
+
const INLINE_MARKUP = /[\\`*_~^[\]<&'"]|--|\.\.\./u;
|
|
354
|
+
// The subset smart punctuation rewrites (`"a"` → `“a”`, `--` → `–`, `...` → `…`).
|
|
355
|
+
const SMART_PUNCTUATION = /['"]|--|\.\.\./u;
|
|
308
356
|
|
|
309
357
|
// The start of a link-reference definition, as the renderer accepts it:
|
|
310
358
|
// `[label]:` after up to 3 leading spaces, optionally inside block-quote or
|
|
@@ -322,10 +370,13 @@ const REF_DEFINITION =
|
|
|
322
370
|
|
|
323
371
|
/**
|
|
324
372
|
* The normalized labels of every link-reference definition in the body
|
|
325
|
-
* (outside fenced code)
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
373
|
+
* (outside fenced code), footnote definitions (`^1`) included. A heading
|
|
374
|
+
* bracket whose label is defined is a CommonMark reference link —
|
|
375
|
+
* `[text][label]`, or a shortcut `[label]` that would otherwise read as a
|
|
376
|
+
* trailing `[toc]`/`[#id]` marker — or a GFM footnote reference, so each
|
|
377
|
+
* heading is parsed together with the definitions it names (see
|
|
378
|
+
* {@link definitionsFor}). Labels match case-insensitively with collapsed
|
|
379
|
+
* internal whitespace (CommonMark).
|
|
329
380
|
*
|
|
330
381
|
* Only a *valid* definition defines a label: the label must contain a
|
|
331
382
|
* non-whitespace character, and a destination must follow — on the same line
|
|
@@ -357,14 +408,179 @@ const refDefinitionLabels = (lines: readonly string[]): Set<string> => {
|
|
|
357
408
|
};
|
|
358
409
|
|
|
359
410
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
411
|
+
* The link-reference and footnote definitions a heading's brackets may name,
|
|
412
|
+
* as source lines to parse the heading with. A definition the heading doesn't
|
|
413
|
+
* use changes nothing, so matching is loose (the label anywhere in the folded
|
|
414
|
+
* text). A footnote label (`^1`) is defined too, so `[^1]` parses as the
|
|
415
|
+
* footnote reference it renders as, not as literal text.
|
|
416
|
+
*/
|
|
417
|
+
const definitionsFor = (raw: string, labels: ReadonlySet<string>): string => {
|
|
418
|
+
if (!raw.includes("[")) {
|
|
419
|
+
return "";
|
|
420
|
+
}
|
|
421
|
+
const folded = raw.replaceAll(/\s+/gu, " ").toLowerCase();
|
|
422
|
+
let definitions = "";
|
|
423
|
+
for (const label of labels) {
|
|
424
|
+
if (folded.includes(label)) {
|
|
425
|
+
definitions += `\n\n[${label}]: /`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return definitions;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The number each footnote renders as, keyed by identifier. GFM numbers
|
|
433
|
+
* footnotes in the order the body first references them, whatever order their
|
|
434
|
+
* definitions sit in, so a `[^b]` cited before `[^a]` renders as 1. A
|
|
435
|
+
* reference inside a footnote definition is left out: the renderer reaches
|
|
436
|
+
* those only after the body.
|
|
437
|
+
*/
|
|
438
|
+
const footnoteNumbers = (body: string): Map<string, number> => {
|
|
439
|
+
const numbers = new Map<string, number>();
|
|
440
|
+
const visit = (nodes: readonly Nodes[]): void => {
|
|
441
|
+
for (const node of nodes) {
|
|
442
|
+
if (node.type === "footnoteReference") {
|
|
443
|
+
if (!numbers.has(node.identifier)) {
|
|
444
|
+
numbers.set(node.identifier, numbers.size + 1);
|
|
445
|
+
}
|
|
446
|
+
} else if (node.type !== "footnoteDefinition" && "children" in node) {
|
|
447
|
+
visit(node.children);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const tree = markdownToMdast(body, HEADING_PARSE);
|
|
452
|
+
visit("children" in tree ? tree.children : []);
|
|
453
|
+
return numbers;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
/** What reading one heading needs from the rest of its body. */
|
|
457
|
+
interface HeadingContext {
|
|
458
|
+
/**
|
|
459
|
+
* The number each footnote renders as (see {@link footnoteNumbers}),
|
|
460
|
+
* computed on first use: only a heading that cites a footnote needs it.
|
|
461
|
+
*/
|
|
462
|
+
footnotes: () => ReadonlyMap<string, number>;
|
|
463
|
+
/** The body's link-reference and footnote definition labels. */
|
|
464
|
+
labels: ReadonlySet<string>;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The text content of mdast inline nodes, the way the rendered heading reads
|
|
469
|
+
* it: link and emphasis text, code-span contents, decoded entities, and a
|
|
470
|
+
* footnote reference's number. Raw HTML tags add no text (the text between
|
|
471
|
+
* them is its own node), and an image's alt is an attribute rather than text
|
|
472
|
+
* content.
|
|
473
|
+
*/
|
|
474
|
+
const inlineText = (nodes: readonly Nodes[], context: HeadingContext): string =>
|
|
475
|
+
nodes
|
|
476
|
+
.map((node) => {
|
|
477
|
+
if (
|
|
478
|
+
node.type === "html" ||
|
|
479
|
+
node.type === "image" ||
|
|
480
|
+
node.type === "imageReference"
|
|
481
|
+
) {
|
|
482
|
+
return "";
|
|
483
|
+
}
|
|
484
|
+
if (node.type === "break") {
|
|
485
|
+
return "\n";
|
|
486
|
+
}
|
|
487
|
+
if (node.type === "footnoteReference") {
|
|
488
|
+
// A reference the body's own parse never numbered (a heading the scan
|
|
489
|
+
// finds inside an HTML block) is no footnote to the renderer either.
|
|
490
|
+
const number = context.footnotes().get(node.identifier);
|
|
491
|
+
return number === undefined
|
|
492
|
+
? `[^${node.label ?? node.identifier}]`
|
|
493
|
+
: String(number);
|
|
494
|
+
}
|
|
495
|
+
if ("children" in node) {
|
|
496
|
+
return inlineText(node.children, context);
|
|
497
|
+
}
|
|
498
|
+
return "value" in node ? node.value : "";
|
|
499
|
+
})
|
|
500
|
+
.join("");
|
|
501
|
+
|
|
502
|
+
/** A heading's rendered text plus the trailing markers the renderer strips. */
|
|
503
|
+
interface RenderedHeading {
|
|
504
|
+
/** Author-pinned anchor id from `[#id]`/`{#id}`, used verbatim. */
|
|
505
|
+
id?: string;
|
|
506
|
+
/** The text content the renderer slugs, markers stripped, untrimmed. */
|
|
507
|
+
text: string;
|
|
508
|
+
toc?: "hide" | "only";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** A heading whose source is its text: one text node, markers stripped. */
|
|
512
|
+
const plainHeading = (raw: string): RenderedHeading => {
|
|
513
|
+
const markers = parseHeadingMarkers(raw);
|
|
514
|
+
// A heading that is nothing but markers keeps them as literal text.
|
|
515
|
+
return markers.text === ""
|
|
516
|
+
? { text: raw }
|
|
517
|
+
: { id: markers.id, text: markers.text, toc: markers.toc };
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Parse a heading's inline Markdown and read it the way
|
|
522
|
+
* `markdown/heading-anchors` does: the heading's text content, with trailing
|
|
523
|
+
* markers stripped from its last node only when that node is text (a heading
|
|
524
|
+
* ending in inline code or a link has no marker position) and some heading
|
|
525
|
+
* text remains. Parsing (rather than regex-stripping) resolves escapes —
|
|
526
|
+
* `\[toc]` still reaches the marker parse as `[toc]` — and emphasis the way
|
|
527
|
+
* CommonMark does, so `snake_case` stays whole while `_note_` loses its
|
|
528
|
+
* underscores.
|
|
529
|
+
*/
|
|
530
|
+
const renderHeading = (
|
|
531
|
+
raw: string,
|
|
532
|
+
form: "atx" | "setext",
|
|
533
|
+
context: HeadingContext,
|
|
534
|
+
parse: typeof HEADING_PARSE
|
|
535
|
+
): RenderedHeading => {
|
|
536
|
+
if (!INLINE_MARKUP.test(raw)) {
|
|
537
|
+
return plainHeading(raw);
|
|
538
|
+
}
|
|
539
|
+
const source = form === "atx" ? `# ${raw}` : `${raw}\n=`;
|
|
540
|
+
const tree = markdownToMdast(
|
|
541
|
+
`${source}${definitionsFor(raw, context.labels)}`,
|
|
542
|
+
parse
|
|
543
|
+
);
|
|
544
|
+
const heading = "children" in tree ? tree.children.at(0) : undefined;
|
|
545
|
+
// The scan's paragraph tracking is coarser than the parser's: underlined
|
|
546
|
+
// text that opens with an HTML block or a definition is no heading to the
|
|
547
|
+
// renderer. It is read as written.
|
|
548
|
+
if (heading?.type !== "heading") {
|
|
549
|
+
return plainHeading(raw);
|
|
550
|
+
}
|
|
551
|
+
const { children } = heading;
|
|
552
|
+
const text = inlineText(children, context);
|
|
553
|
+
const last = children.at(-1);
|
|
554
|
+
if (last?.type !== "text") {
|
|
555
|
+
return { text };
|
|
556
|
+
}
|
|
557
|
+
const markers = parseHeadingMarkers(last.value);
|
|
558
|
+
const stripped = last.value.length - markers.text.length;
|
|
559
|
+
if (stripped === 0 || (markers.text === "" && children.length === 1)) {
|
|
560
|
+
return { text };
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
id: markers.id,
|
|
564
|
+
text: text.slice(0, text.length - stripped),
|
|
565
|
+
toc: markers.toc,
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* A heading record from raw heading text, read exactly as the renderer reads
|
|
571
|
+
* it: inline Markdown reduced to its rendered text (see {@link renderHeading})
|
|
572
|
+
* and trailing markers stripped. The slug is the renderer's slug of that text.
|
|
573
|
+
* A `[#custom-id]` pin becomes the slug verbatim and — matching the renderer —
|
|
574
|
+
* occupies its id in the slugger, so a later heading whose auto-slug collides
|
|
575
|
+
* disambiguates (`setup` → `setup-1`). `[!toc]`/`[toc]` headings stay in the
|
|
576
|
+
* record: their ids exist in the rendered page, so links to them are valid
|
|
577
|
+
* anchors regardless of TOC visibility.
|
|
578
|
+
*
|
|
579
|
+
* The record's `text` — a page's fallback title and sidebar label — is the
|
|
580
|
+
* same rendered text with whitespace collapsed, but read without smart
|
|
581
|
+
* punctuation: it keeps the straight quotes and double hyphens the author
|
|
582
|
+
* typed, the way a frontmatter `title` does, and the way an Obsidian
|
|
583
|
+
* `[[Note#It's here]]` heading link spells the heading it resolves against.
|
|
368
584
|
*/
|
|
369
585
|
/** A scanned heading plus whether its id came from an author pin. */
|
|
370
586
|
interface ScannedHeading {
|
|
@@ -375,23 +591,23 @@ interface ScannedHeading {
|
|
|
375
591
|
const toHeading = (
|
|
376
592
|
depth: number,
|
|
377
593
|
raw: string,
|
|
594
|
+
form: "atx" | "setext",
|
|
378
595
|
slugger: GithubSlugger,
|
|
379
|
-
|
|
596
|
+
context: HeadingContext
|
|
380
597
|
): ScannedHeading => {
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
if (markers.id !== undefined) {
|
|
391
|
-
occupySlug(slugger, markers.id);
|
|
392
|
-
return { heading: { depth, slug: markers.id, text }, pinned: true };
|
|
598
|
+
const rendered = renderHeading(raw, form, context, HEADING_PARSE);
|
|
599
|
+
const display = SMART_PUNCTUATION.test(raw)
|
|
600
|
+
? renderHeading(raw, form, context, HEADING_TEXT_PARSE).text
|
|
601
|
+
: rendered.text;
|
|
602
|
+
const text = display.replaceAll(/[\t\n\f\r ]+/gu, " ").trim();
|
|
603
|
+
if (rendered.id !== undefined) {
|
|
604
|
+
occupySlug(slugger, rendered.id);
|
|
605
|
+
return { heading: { depth, slug: rendered.id, text }, pinned: true };
|
|
393
606
|
}
|
|
394
|
-
return {
|
|
607
|
+
return {
|
|
608
|
+
heading: { depth, slug: slugger.slug(rendered.text), text },
|
|
609
|
+
pinned: false,
|
|
610
|
+
};
|
|
395
611
|
};
|
|
396
612
|
|
|
397
613
|
/** Record a heading and where a trailing marker would be written for it. */
|
|
@@ -423,7 +639,7 @@ const scanContentLine = (
|
|
|
423
639
|
state: HeadingScanState,
|
|
424
640
|
slugger: GithubSlugger,
|
|
425
641
|
headings: Heading[],
|
|
426
|
-
|
|
642
|
+
context: HeadingContext
|
|
427
643
|
): void => {
|
|
428
644
|
// Inline code is masked so a documented `<a id="…">` isn't an anchor.
|
|
429
645
|
state.anchorLines.push(line.replaceAll(INLINE_CODE, ""));
|
|
@@ -434,7 +650,7 @@ const scanContentLine = (
|
|
|
434
650
|
pushHeading(
|
|
435
651
|
headings,
|
|
436
652
|
state,
|
|
437
|
-
toHeading(depth, text, slugger,
|
|
653
|
+
toHeading(depth, text, "atx", slugger, context),
|
|
438
654
|
state.line
|
|
439
655
|
);
|
|
440
656
|
noteCurlyMarker(text, state.line, state);
|
|
@@ -444,15 +660,17 @@ const scanContentLine = (
|
|
|
444
660
|
const setext = line.match(SETEXT_UNDERLINE);
|
|
445
661
|
if (setext?.groups && state.paragraph.length > 0) {
|
|
446
662
|
// Setext wins over thematic break when it closes a paragraph (CommonMark);
|
|
447
|
-
// a multi-line paragraph renders as one heading
|
|
663
|
+
// a multi-line paragraph renders as one heading. Its lines keep their line
|
|
664
|
+
// breaks, which the rendered text content keeps too (and the slugger
|
|
665
|
+
// drops, so `Multi\nline` anchors as `multiline`).
|
|
448
666
|
const depth = setext.groups.marker?.startsWith("=") ? 1 : 2;
|
|
449
|
-
const text = state.paragraph.join("
|
|
667
|
+
const text = state.paragraph.join("\n");
|
|
450
668
|
// A setext heading's markers trail its last text line, just above the
|
|
451
669
|
// underline — that is where a pin is appended.
|
|
452
670
|
pushHeading(
|
|
453
671
|
headings,
|
|
454
672
|
state,
|
|
455
|
-
toHeading(depth, text, slugger,
|
|
673
|
+
toHeading(depth, text, "setext", slugger, context),
|
|
456
674
|
state.line - 1
|
|
457
675
|
);
|
|
458
676
|
noteCurlyMarker(text, state.paragraphStart, state);
|
|
@@ -473,14 +691,68 @@ const scanContentLine = (
|
|
|
473
691
|
state.paragraph.push(line.trim());
|
|
474
692
|
};
|
|
475
693
|
|
|
694
|
+
// A comment that opens a line hides everything up to its close from the
|
|
695
|
+
// reader: an HTML comment (`<!--` … `-->`, a CommonMark HTML block, so up to
|
|
696
|
+
// 3 leading spaces) or an MDX JSX one (`{/*` … `*/}`, any indentation — MDX
|
|
697
|
+
// has no indented code).
|
|
698
|
+
const HTML_COMMENT_OPEN = /^ {0,3}<!--/u;
|
|
699
|
+
const JSX_COMMENT_OPEN = /^\s*\{\/\*/u;
|
|
700
|
+
const JSX_COMMENT_CLOSE = /\*\/\s*\}/u;
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Advance the multi-line comment state over one line, returning whether the
|
|
704
|
+
* line belongs to a comment. A commented-out `# Old title` renders nothing, so
|
|
705
|
+
* it must not become a heading — the page title, a TOC entry, or an anchor.
|
|
706
|
+
* A comment that closes on its opening line is left to the regular scan,
|
|
707
|
+
* which never reads a line starting with `<` or `{` as a heading. The
|
|
708
|
+
* CommonMark end condition is any `-->` on the line, the opening one
|
|
709
|
+
* included.
|
|
710
|
+
*/
|
|
711
|
+
const scanCommentLine = (line: string, state: HeadingScanState): boolean => {
|
|
712
|
+
if (state.comment === "html") {
|
|
713
|
+
if (line.includes("-->")) {
|
|
714
|
+
state.comment = null;
|
|
715
|
+
}
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
if (state.comment === "jsx") {
|
|
719
|
+
if (JSX_COMMENT_CLOSE.test(line)) {
|
|
720
|
+
state.comment = null;
|
|
721
|
+
}
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
if (HTML_COMMENT_OPEN.test(line) && !line.includes("-->")) {
|
|
725
|
+
state.comment = "html";
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
const jsx = JSX_COMMENT_OPEN.exec(line);
|
|
729
|
+
if (jsx && !JSX_COMMENT_CLOSE.test(line.slice(jsx[0].length))) {
|
|
730
|
+
state.comment = "jsx";
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
};
|
|
735
|
+
|
|
476
736
|
/** Scan one line for a heading, advancing the fence/paragraph state. */
|
|
477
737
|
const scanHeadingLine = (
|
|
478
738
|
line: string,
|
|
479
739
|
state: HeadingScanState,
|
|
480
740
|
slugger: GithubSlugger,
|
|
481
741
|
headings: Heading[],
|
|
482
|
-
|
|
742
|
+
context: HeadingContext
|
|
483
743
|
): void => {
|
|
744
|
+
// Comments hide fences too, and fences and prompts hide comments. A comment
|
|
745
|
+
// line still goes to the anchor pass, which strips HTML comments itself.
|
|
746
|
+
if (
|
|
747
|
+
state.fence === null &&
|
|
748
|
+
state.promptDepth === 0 &&
|
|
749
|
+
!state.promptTag &&
|
|
750
|
+
scanCommentLine(line, state)
|
|
751
|
+
) {
|
|
752
|
+
state.anchorLines.push(line.replaceAll(INLINE_CODE, ""));
|
|
753
|
+
state.paragraph = [];
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
484
756
|
const next = nextFenceState(line, state.fence);
|
|
485
757
|
// Skip fence delimiter lines themselves and anything inside a fence. A fence
|
|
486
758
|
// also ends any open paragraph, so no underline can reach across it.
|
|
@@ -511,7 +783,7 @@ const scanHeadingLine = (
|
|
|
511
783
|
state.paragraph = [];
|
|
512
784
|
return;
|
|
513
785
|
}
|
|
514
|
-
scanContentLine(line, state, slugger, headings,
|
|
786
|
+
scanContentLine(line, state, slugger, headings, context);
|
|
515
787
|
};
|
|
516
788
|
|
|
517
789
|
/** Where a heading's text ends in the scanned text, for appending a marker. */
|
|
@@ -541,13 +813,17 @@ export interface BodyScan {
|
|
|
541
813
|
/**
|
|
542
814
|
* Scan a body for its headings, explicit HTML anchors, and unescaped `{#id}`
|
|
543
815
|
* markers in one fence-aware walk (the same walk `extractHeadings` exposes for
|
|
544
|
-
* headings alone).
|
|
816
|
+
* headings alone). The body is the page's content with its front matter
|
|
817
|
+
* already stripped — the renderers read front matter off the page and never
|
|
818
|
+
* again from its body — so a leading `---` block is a thematic break and
|
|
819
|
+
* content, in `.md` and `.mdx` alike.
|
|
545
820
|
*/
|
|
546
821
|
export const scanBody = (body: string): BodyScan => {
|
|
547
822
|
const headings: Heading[] = [];
|
|
548
823
|
const slugger = new GithubSlugger();
|
|
549
824
|
const state: HeadingScanState = {
|
|
550
825
|
anchorLines: [],
|
|
826
|
+
comment: null,
|
|
551
827
|
curlyMarkers: [],
|
|
552
828
|
fence: null,
|
|
553
829
|
line: 0,
|
|
@@ -558,13 +834,18 @@ export const scanBody = (body: string): BodyScan => {
|
|
|
558
834
|
sites: [],
|
|
559
835
|
};
|
|
560
836
|
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
837
|
+
const lines = body.split("\n");
|
|
838
|
+
let footnotes: Map<string, number> | undefined;
|
|
839
|
+
const context: HeadingContext = {
|
|
840
|
+
footnotes: () => {
|
|
841
|
+
footnotes ??= footnoteNumbers(lines.join("\n"));
|
|
842
|
+
return footnotes;
|
|
843
|
+
},
|
|
844
|
+
labels: refDefinitionLabels(lines),
|
|
845
|
+
};
|
|
565
846
|
for (const [index, line] of lines.entries()) {
|
|
566
|
-
state.line = index +
|
|
567
|
-
scanHeadingLine(line, state, slugger, headings,
|
|
847
|
+
state.line = index + 1;
|
|
848
|
+
scanHeadingLine(line, state, slugger, headings, context);
|
|
568
849
|
}
|
|
569
850
|
|
|
570
851
|
const anchors = new Set<string>();
|
|
@@ -792,6 +1073,14 @@ export const strippedLineOffset = (
|
|
|
792
1073
|
): number =>
|
|
793
1074
|
raw ? Math.max(0, raw.split("\n").length - body.split("\n").length) : 0;
|
|
794
1075
|
|
|
1076
|
+
/**
|
|
1077
|
+
* How many lines of the entry's source file sit above its body: what the
|
|
1078
|
+
* source reported (`bodyLineOffset`, when `raw` is a rewrite of the file), or
|
|
1079
|
+
* else the height of `raw`'s stripped front matter.
|
|
1080
|
+
*/
|
|
1081
|
+
const entryLineOffset = (entry: SourceEntry): number =>
|
|
1082
|
+
entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
|
|
1083
|
+
|
|
795
1084
|
/**
|
|
796
1085
|
* Map links extracted from include-expanded text back to the file and raw
|
|
797
1086
|
* line each expanded line came from, so a broken link inside a partial is
|
|
@@ -834,10 +1123,7 @@ const entryLinks = (entry: SourceEntry): PageLink[] =>
|
|
|
834
1123
|
entry.expanded.origins,
|
|
835
1124
|
entry.sourcePath
|
|
836
1125
|
)
|
|
837
|
-
: extractLinks(
|
|
838
|
-
entry.body.text,
|
|
839
|
-
strippedLineOffset(entry.raw, entry.body.text)
|
|
840
|
-
);
|
|
1126
|
+
: extractLinks(entry.body.text, entryLineOffset(entry));
|
|
841
1127
|
|
|
842
1128
|
/**
|
|
843
1129
|
* Diagnostics for `{#id}` heading markers in an `.mdx` page. The MDX parser
|
|
@@ -860,8 +1146,7 @@ const curlyMarkerDiagnostics = (
|
|
|
860
1146
|
return {
|
|
861
1147
|
code: "BLUME_MDX_CURLY_ANCHOR",
|
|
862
1148
|
file: origin?.file ?? page,
|
|
863
|
-
line:
|
|
864
|
-
origin?.line ?? line + strippedLineOffset(entry.raw, entry.body.text),
|
|
1149
|
+
line: origin?.line ?? line + entryLineOffset(entry),
|
|
865
1150
|
message: inPartial
|
|
866
1151
|
? `\`{#${id}}\` is a JSX expression once this partial is included in ${page} (.mdx), so that page fails to compile.`
|
|
867
1152
|
: `\`{#${id}}\` is a JSX expression in .mdx, so this page fails to compile.`,
|
|
@@ -882,8 +1167,17 @@ const deriveTitle = (
|
|
|
882
1167
|
if (firstHeading) {
|
|
883
1168
|
return firstHeading.text;
|
|
884
1169
|
}
|
|
885
|
-
const
|
|
886
|
-
|
|
1170
|
+
const parts = id.split("/");
|
|
1171
|
+
const base = parts.at(-1) ?? id;
|
|
1172
|
+
const stem = stripNumericPrefix(base.replace(extname(base), ""));
|
|
1173
|
+
// An index page stands for its folder, so an untitled one takes the label
|
|
1174
|
+
// the sidebar gives that folder (`guides/index.md` is "Guides"). The
|
|
1175
|
+
// content root's own index has no folder to borrow from.
|
|
1176
|
+
const folder = stem === "index" ? parts.at(-2) : undefined;
|
|
1177
|
+
if (folder !== undefined) {
|
|
1178
|
+
return titleCase(stripNumericPrefix(orderedGroupLabel(folder) ?? folder));
|
|
1179
|
+
}
|
|
1180
|
+
return titleCase(stem);
|
|
887
1181
|
};
|
|
888
1182
|
|
|
889
1183
|
/** Strip habitual leading/trailing slashes (`/getting-started`, `guides/`). */
|
|
@@ -902,6 +1196,13 @@ const withPrefix = (prefix: string | undefined, path: string): string => {
|
|
|
902
1196
|
|
|
903
1197
|
/** What a route resolution needs from the owning source and the config. */
|
|
904
1198
|
export type RouteContext = Pick<NormalizeContext, "i18n" | "versions"> & {
|
|
1199
|
+
/**
|
|
1200
|
+
* Whether the entry's ref is a path of file and folder names whose ordering
|
|
1201
|
+
* prefixes (`01-intro`) sort the sidebar and drop from the route: true for
|
|
1202
|
+
* filesystem sources. A staged source's ref is a slug, a release tag, or a
|
|
1203
|
+
* note name, and keeps its leading numbers.
|
|
1204
|
+
*/
|
|
1205
|
+
orderingPrefixes?: boolean;
|
|
905
1206
|
/** The source's route prefix (`NormalizeContext["source"]["prefix"]`). */
|
|
906
1207
|
prefix?: string;
|
|
907
1208
|
};
|
|
@@ -970,7 +1271,9 @@ export interface EntryRoute extends Pick<
|
|
|
970
1271
|
* A frontmatter `slug` wins, then the adapter-supplied `entry.slug` (the typed
|
|
971
1272
|
* SPI's "logical route input; defaults to ref if omitted"), then the ref. The
|
|
972
1273
|
* extension is re-appended so `mapRoute`'s extname strip can't eat a dotted
|
|
973
|
-
* slug segment (`v1.2`). A slug that trims to nothing falls back.
|
|
1274
|
+
* slug segment (`v1.2`). A slug that trims to nothing falls back. Only a
|
|
1275
|
+
* filesystem ref (`ctx.orderingPrefixes`) loses its ordering prefixes; a slug
|
|
1276
|
+
* is a route spelled out, so `2024-year-in-review` stays whole. The version
|
|
974
1277
|
* prefixes the mapped route *after* `mapRoute` runs: the mapped route is the
|
|
975
1278
|
* version-agnostic key, the config id is prepended verbatim (never
|
|
976
1279
|
* numeric-prefix-stripped), a frontmatter `slug` gets versionized so snapshots
|
|
@@ -987,8 +1290,18 @@ export const resolveEntryRoute = (
|
|
|
987
1290
|
const { locales, navPath, version } = placeEntryRef(entry.ref, ext, ctx);
|
|
988
1291
|
const slugInput = frontmatterSlug ?? entry.slug;
|
|
989
1292
|
const slug = slugInput ? trimSlashes(slugInput) : "";
|
|
990
|
-
|
|
991
|
-
|
|
1293
|
+
// A frontmatter slug is a route spelled out; an adapter's `entry.slug` is
|
|
1294
|
+
// one too unless the source's names are ordered file names (a vault note's
|
|
1295
|
+
// path), which lose their prefixes like the ref does.
|
|
1296
|
+
const ordered =
|
|
1297
|
+
ctx.orderingPrefixes === true && frontmatterSlug === undefined;
|
|
1298
|
+
const {
|
|
1299
|
+
segments,
|
|
1300
|
+
groups,
|
|
1301
|
+
route: versionKey,
|
|
1302
|
+
} = slug
|
|
1303
|
+
? mapRoute(ctx.prefix, `${slug}${ext}`, ordered)
|
|
1304
|
+
: mapRoute(ctx.prefix, navPath, ctx.orderingPrefixes === true);
|
|
992
1305
|
return {
|
|
993
1306
|
groups,
|
|
994
1307
|
locales,
|
|
@@ -1081,6 +1394,25 @@ const validateCustomKeys = (
|
|
|
1081
1394
|
};
|
|
1082
1395
|
};
|
|
1083
1396
|
|
|
1397
|
+
// A `.` or `..` path segment, which a browser resolves away before requesting.
|
|
1398
|
+
const DOT_SEGMENT = /(?:^|\/)\.{1,2}(?:\/|$)/u;
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* A frontmatter `slug` with a `.` or `..` segment names no reachable URL: a
|
|
1402
|
+
* browser normalizes the link (`guides/./x` → `guides/x`, `../x` → `/x`)
|
|
1403
|
+
* before requesting it, and `..` would have Astro write the page outside the
|
|
1404
|
+
* build output. Rejected alongside the schema's own errors.
|
|
1405
|
+
*/
|
|
1406
|
+
const slugIssues = (slug: string | undefined): CustomKeyIssue[] =>
|
|
1407
|
+
slug !== undefined && DOT_SEGMENT.test(slug)
|
|
1408
|
+
? [
|
|
1409
|
+
{
|
|
1410
|
+
message: `"${slug}" has a "." or ".." segment, which browsers resolve away, so no link could reach the page. Write the route out from the content root.`,
|
|
1411
|
+
path: ["slug"],
|
|
1412
|
+
},
|
|
1413
|
+
]
|
|
1414
|
+
: [];
|
|
1415
|
+
|
|
1084
1416
|
/**
|
|
1085
1417
|
* Parse an entry's frontmatter: built-in keys through the strict page schema,
|
|
1086
1418
|
* custom keys (`frontmatter.extend` plus the page type's
|
|
@@ -1119,8 +1451,12 @@ const parseEntryMeta = (
|
|
|
1119
1451
|
|
|
1120
1452
|
const result = pageMetaSchema.safeParse(known);
|
|
1121
1453
|
const customResult = extend ? validateCustomKeys(entry.data, extend) : null;
|
|
1454
|
+
const issues = [
|
|
1455
|
+
...(result.success ? slugIssues(result.data.slug) : []),
|
|
1456
|
+
...(customResult?.issues ?? []),
|
|
1457
|
+
];
|
|
1122
1458
|
|
|
1123
|
-
if (result.success &&
|
|
1459
|
+
if (result.success && issues.length === 0) {
|
|
1124
1460
|
return { custom: customResult?.custom, meta: result.data };
|
|
1125
1461
|
}
|
|
1126
1462
|
|
|
@@ -1140,9 +1476,7 @@ const parseEntryMeta = (
|
|
|
1140
1476
|
return {
|
|
1141
1477
|
diagnostics: [
|
|
1142
1478
|
...(result.success ? [] : diagnosticsFromZod(result.error, location)),
|
|
1143
|
-
...(
|
|
1144
|
-
? diagnosticsFromIssues(customResult.issues, location)
|
|
1145
|
-
: []),
|
|
1479
|
+
...diagnosticsFromIssues(issues, location),
|
|
1146
1480
|
],
|
|
1147
1481
|
};
|
|
1148
1482
|
};
|
|
@@ -1192,6 +1526,7 @@ export const normalizeEntry = (
|
|
|
1192
1526
|
versionKey,
|
|
1193
1527
|
} = resolveEntryRoute(entry, ext, meta.slug, {
|
|
1194
1528
|
i18n: ctx.i18n,
|
|
1529
|
+
orderingPrefixes: !ctx.source.staged || ctx.source.orderedNames === true,
|
|
1195
1530
|
prefix: ctx.source.prefix,
|
|
1196
1531
|
versions: ctx.versions,
|
|
1197
1532
|
});
|
|
@@ -1237,10 +1572,12 @@ export const normalizeEntry = (
|
|
|
1237
1572
|
// `basePath` is applied outermost — after locale prefixing — so the route
|
|
1238
1573
|
// reads `{basePath}/{locale?}/{prefix?}/…`; `navPath` and `translationKey`
|
|
1239
1574
|
// stay base-less so the nav tree and translation matching are unaffected.
|
|
1575
|
+
// The base is mounted unconditionally: a `docs/` folder under a `/docs`
|
|
1576
|
+
// base is a real `/docs/docs/…` route, not an already-based one.
|
|
1240
1577
|
const pages = locales.map((locale) => ({
|
|
1241
1578
|
...base,
|
|
1242
1579
|
locale,
|
|
1243
|
-
route:
|
|
1580
|
+
route: mountBasePath(
|
|
1244
1581
|
ctx.basePath ?? "",
|
|
1245
1582
|
localizedRoute(logicalRoute, locale, ctx.i18n)
|
|
1246
1583
|
),
|