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
|
@@ -3,6 +3,7 @@ import { isAbsolute, relative } from "pathe";
|
|
|
3
3
|
import { CHANGELOG_INDEX_ROUTE, hasChangelogIndex } from "./changelog-index.ts";
|
|
4
4
|
import { loadConfig } from "./config.ts";
|
|
5
5
|
import { customStaticRoutes, discoverPages } from "./custom-pages.ts";
|
|
6
|
+
import { unknownDirectiveDiagnostics } from "./directive-diagnostics.ts";
|
|
6
7
|
import { buildContentGraph } from "./graph.ts";
|
|
7
8
|
import { i18nDiagnostics } from "./i18n.ts";
|
|
8
9
|
import { expandIncludes, hasIncludeStatements } from "./includes.ts";
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
gitLastModifiedTimes,
|
|
11
12
|
gitRepositoryRoot,
|
|
12
13
|
lastModifiedShallowWarning,
|
|
14
|
+
realPath,
|
|
13
15
|
resolveLastModifiedConfig,
|
|
14
16
|
} from "./last-modified.ts";
|
|
15
17
|
import { buildManifest } from "./manifest.ts";
|
|
@@ -184,6 +186,7 @@ const normalizeLoadedEntries = (
|
|
|
184
186
|
source: {
|
|
185
187
|
monolingual: source.monolingual,
|
|
186
188
|
name: source.name,
|
|
189
|
+
orderedNames: source.orderedNames,
|
|
187
190
|
prefix: source.prefix,
|
|
188
191
|
staged: source.staged,
|
|
189
192
|
},
|
|
@@ -195,11 +198,34 @@ const normalizeLoadedEntries = (
|
|
|
195
198
|
}
|
|
196
199
|
pages.push(...normalized.pages);
|
|
197
200
|
allDiagnostics.push(...normalized.diagnostics);
|
|
201
|
+
if (normalized.pages.length > 0) {
|
|
202
|
+
allDiagnostics.push(...unknownDirectiveDiagnostics(entry, source.name));
|
|
203
|
+
}
|
|
198
204
|
}
|
|
199
205
|
}
|
|
200
206
|
return { diagnostics: allDiagnostics, droppedPages, pages };
|
|
201
207
|
};
|
|
202
208
|
|
|
209
|
+
/**
|
|
210
|
+
* The local sources' on-disk roots a `git log` pathspec can cover: those
|
|
211
|
+
* inside the repository at `gitRoot`. A root outside it would fail the log
|
|
212
|
+
* outright and can never yield dates. Compared by real path, since git spells
|
|
213
|
+
* its toplevel with every symlink resolved (`/private/tmp/site` for a project
|
|
214
|
+
* opened as `/tmp/site`); the roots themselves keep the project's spelling.
|
|
215
|
+
*/
|
|
216
|
+
const gitContentRoots = (
|
|
217
|
+
sources: readonly ContentSource[],
|
|
218
|
+
gitRoot: string | null
|
|
219
|
+
): string[] => {
|
|
220
|
+
if (gitRoot === null) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
const top = realPath(gitRoot);
|
|
224
|
+
return sources
|
|
225
|
+
.flatMap((source) => (source.contentRoot ? [source.contentRoot] : []))
|
|
226
|
+
.filter((dir) => isWithin(top, realPath(dir)));
|
|
227
|
+
};
|
|
228
|
+
|
|
203
229
|
/**
|
|
204
230
|
* Run the full core pipeline for a project root: load config, resolve paths,
|
|
205
231
|
* discover content and folder meta, build the graph, and assemble the manifest.
|
|
@@ -215,6 +241,19 @@ const isLogoShorthand = (
|
|
|
215
241
|
const logoHref = (logo: ResolvedConfig["logo"]): string =>
|
|
216
242
|
logo === undefined || isLogoShorthand(logo) ? "/" : (logo.href ?? "/");
|
|
217
243
|
|
|
244
|
+
/** Narrows the banner config's text shorthand from its object form. */
|
|
245
|
+
const isBannerShorthand = (
|
|
246
|
+
banner: NonNullable<ResolvedConfig["banner"]>
|
|
247
|
+
): banner is string => typeof banner === "string";
|
|
248
|
+
|
|
249
|
+
/** The configured banner link target, when the banner has a link. */
|
|
250
|
+
const bannerLinkHref = (
|
|
251
|
+
banner: ResolvedConfig["banner"]
|
|
252
|
+
): string | undefined =>
|
|
253
|
+
banner === undefined || isBannerShorthand(banner)
|
|
254
|
+
? undefined
|
|
255
|
+
: banner.link?.href;
|
|
256
|
+
|
|
218
257
|
export const scanProject = async (
|
|
219
258
|
root: string,
|
|
220
259
|
options: {
|
|
@@ -255,7 +294,14 @@ export const scanProject = async (
|
|
|
255
294
|
const metaSources: FolderMetaSource[] = sources.flatMap((source) =>
|
|
256
295
|
source.staged || !source.contentRoot
|
|
257
296
|
? []
|
|
258
|
-
: [
|
|
297
|
+
: [
|
|
298
|
+
{
|
|
299
|
+
exclude: source.exclude,
|
|
300
|
+
include: source.include,
|
|
301
|
+
prefix: source.prefix,
|
|
302
|
+
root: source.contentRoot,
|
|
303
|
+
},
|
|
304
|
+
]
|
|
259
305
|
);
|
|
260
306
|
|
|
261
307
|
// Run every source's `load()` in parallel, then funnel each entry through the
|
|
@@ -341,12 +387,9 @@ export const scanProject = async (
|
|
|
341
387
|
// The git pathspecs must cover where the pages actually live: each local
|
|
342
388
|
// source's own on-disk root — a filesystem source's `root`, or a staged
|
|
343
389
|
// local source's tree (an Obsidian vault) — which diverges from the global
|
|
344
|
-
// `content.root`.
|
|
345
|
-
// and can never yield dates, so those are dropped up front.
|
|
390
|
+
// `content.root`.
|
|
346
391
|
const gitRoot = gitRepositoryRoot(context.root);
|
|
347
|
-
const contentRoots = sources
|
|
348
|
-
.flatMap((source) => (source.contentRoot ? [source.contentRoot] : []))
|
|
349
|
-
.filter((dir) => gitRoot !== null && isWithin(gitRoot, dir));
|
|
392
|
+
const contentRoots = gitContentRoots(sources, gitRoot);
|
|
350
393
|
const gitTimes = gitLastModifiedTimes(
|
|
351
394
|
context.root,
|
|
352
395
|
contentRoots,
|
|
@@ -382,6 +425,7 @@ export const scanProject = async (
|
|
|
382
425
|
? await discoverPages(context.pagesRoot)
|
|
383
426
|
: [];
|
|
384
427
|
const graph = buildContentGraph(pages, {
|
|
428
|
+
bannerHref: bannerLinkHref(config.banner),
|
|
385
429
|
basePath: config.basePath,
|
|
386
430
|
brandHref: logoHref(config.logo),
|
|
387
431
|
extraRoutes: new Set([
|
package/src/core/safe-href.ts
CHANGED
|
@@ -11,6 +11,58 @@ const SAFE_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
|
|
|
11
11
|
|
|
12
12
|
const SCHEME = /^(?<scheme>[a-z][\d+.a-z-]*):/iu;
|
|
13
13
|
|
|
14
|
+
// Markdown decodes a destination before the browser sees it: a backslash
|
|
15
|
+
// escape (`javascript\:`) and a character reference (`javascript:`,
|
|
16
|
+
// `javascript:`) both reach the `href` as the plain character, so the
|
|
17
|
+
// check reads the destination decoded. Of HTML's named references, these are
|
|
18
|
+
// the only ones that decode to a scheme character, a colon, or a character
|
|
19
|
+
// the URL parser strips; every other one leaves the scheme a browser reads
|
|
20
|
+
// unchanged.
|
|
21
|
+
const MARKDOWN_DECODED =
|
|
22
|
+
/\\(?<escaped>[!-/:-@[-`{-~])|&(?:#[Xx](?<hex>[\dA-Fa-f]{1,6})|#(?<decimal>\d{1,7})|(?<named>colon|fjlig|NewLine|period|plus|Tab));/gu;
|
|
23
|
+
|
|
24
|
+
const NAMED_REFERENCES = new Map([
|
|
25
|
+
["colon", ":"],
|
|
26
|
+
["fjlig", "fj"],
|
|
27
|
+
["NewLine", "\n"],
|
|
28
|
+
["period", "."],
|
|
29
|
+
["plus", "+"],
|
|
30
|
+
["Tab", "\t"],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const MAX_CODE_POINT = 0x10_ff_ff;
|
|
34
|
+
const SURROGATES = { end: 0xdf_ff, start: 0xd8_00 };
|
|
35
|
+
|
|
36
|
+
/** A numeric reference's character; U+FFFD for NUL, surrogates, and overflow. */
|
|
37
|
+
const fromReference = (code: number): string =>
|
|
38
|
+
code === 0 ||
|
|
39
|
+
code > MAX_CODE_POINT ||
|
|
40
|
+
(code >= SURROGATES.start && code <= SURROGATES.end)
|
|
41
|
+
? "�"
|
|
42
|
+
: String.fromCodePoint(code);
|
|
43
|
+
|
|
44
|
+
const decodeMarkdown = (href: string): string =>
|
|
45
|
+
href.replaceAll(
|
|
46
|
+
MARKDOWN_DECODED,
|
|
47
|
+
(
|
|
48
|
+
_match: string,
|
|
49
|
+
escaped?: string,
|
|
50
|
+
hex?: string,
|
|
51
|
+
decimal?: string,
|
|
52
|
+
named?: string
|
|
53
|
+
) => {
|
|
54
|
+
if (escaped !== undefined) {
|
|
55
|
+
return escaped;
|
|
56
|
+
}
|
|
57
|
+
if (named !== undefined) {
|
|
58
|
+
return NAMED_REFERENCES.get(named) ?? "";
|
|
59
|
+
}
|
|
60
|
+
const code =
|
|
61
|
+
hex === undefined ? Number(decimal) : Number.parseInt(hex, 16);
|
|
62
|
+
return fromReference(code);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
14
66
|
// The URL parser removes ASCII tabs and newlines anywhere, and strips leading
|
|
15
67
|
// C0 controls and spaces, before it reads a scheme — so `java\tscript:` is a
|
|
16
68
|
// `javascript:` URL to a browser, and the check must see it the same way.
|
|
@@ -18,7 +70,7 @@ const URL_IGNORED = /[\t\n\r]/gu;
|
|
|
18
70
|
|
|
19
71
|
/** Whether a link destination from imported content is safe to keep. */
|
|
20
72
|
export const isSafeHref = (href: string): boolean => {
|
|
21
|
-
const cleaned = href.replaceAll(URL_IGNORED, "");
|
|
73
|
+
const cleaned = decodeMarkdown(href).replaceAll(URL_IGNORED, "");
|
|
22
74
|
let start = 0;
|
|
23
75
|
while (start < cleaned.length && (cleaned.codePointAt(start) ?? 0) <= 0x20) {
|
|
24
76
|
start += 1;
|
package/src/core/safe-links.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Nodes, Root } from "mdast";
|
|
2
2
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
3
|
+
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
3
4
|
import { toString } from "mdast-util-to-string";
|
|
5
|
+
import { gfm } from "micromark-extension-gfm";
|
|
4
6
|
|
|
5
7
|
import { isSafeHref } from "./safe-href.ts";
|
|
6
8
|
import { escapeMarkdownText } from "./sources/lower.ts";
|
|
@@ -54,10 +56,17 @@ export const unsafeLinkSpans = (tree: Root): UnsafeLinkSpan[] => {
|
|
|
54
56
|
/**
|
|
55
57
|
* Markdown with every unsafe link reduced to its label text and every unsafe
|
|
56
58
|
* link definition removed. Everything else — safe links, raw HTML, code —
|
|
57
|
-
* passes through as written.
|
|
59
|
+
* passes through as written. The Markdown is read as GitHub-flavored, the way
|
|
60
|
+
* the site renders it: a link inside a footnote definition or a table cell is
|
|
61
|
+
* only a link to a GFM parser.
|
|
58
62
|
*/
|
|
59
63
|
export const neutralizeUnsafeLinks = (markdown: string): string => {
|
|
60
|
-
const spans = unsafeLinkSpans(
|
|
64
|
+
const spans = unsafeLinkSpans(
|
|
65
|
+
fromMarkdown(markdown, {
|
|
66
|
+
extensions: [gfm()],
|
|
67
|
+
mdastExtensions: [gfmFromMarkdown()],
|
|
68
|
+
})
|
|
69
|
+
);
|
|
61
70
|
let out = "";
|
|
62
71
|
let cursor = 0;
|
|
63
72
|
for (const span of spans) {
|
package/src/core/schema.ts
CHANGED
|
@@ -397,20 +397,27 @@ const contentConfigSchema = z
|
|
|
397
397
|
*/
|
|
398
398
|
types: z.record(z.string(), contentTypeConfigSchema).default({}),
|
|
399
399
|
})
|
|
400
|
-
.superRefine(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
for (const key of FILESYSTEM_SHORTHAND_KEYS) {
|
|
405
|
-
if (value[key] !== undefined) {
|
|
406
|
-
ctx.addIssue({
|
|
407
|
-
code: z.ZodIssueCode.custom,
|
|
408
|
-
message: `content.${key} is shorthand for a single filesystem() source and can't be combined with content.sources — move it into a filesystem({ ${key} }) entry in content.sources.`,
|
|
409
|
-
path: [key],
|
|
410
|
-
});
|
|
400
|
+
.superRefine(
|
|
401
|
+
(value, ctx) => {
|
|
402
|
+
if (!value.sources) {
|
|
403
|
+
return;
|
|
411
404
|
}
|
|
412
|
-
|
|
413
|
-
|
|
405
|
+
for (const key of FILESYSTEM_SHORTHAND_KEYS) {
|
|
406
|
+
if (value[key] !== undefined) {
|
|
407
|
+
ctx.addIssue({
|
|
408
|
+
code: z.ZodIssueCode.custom,
|
|
409
|
+
message: `content.${key} is shorthand for a single filesystem() source and can't be combined with content.sources — move it into a filesystem({ ${key} }) entry in content.sources.`,
|
|
410
|
+
path: [key],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
// Zod skips a refinement once a field has failed, so without this a
|
|
416
|
+
// Blume 1 config (`{ type: "filesystem" }` sources beside `content.root`)
|
|
417
|
+
// would only learn about the shorthand after fixing every source entry.
|
|
418
|
+
// Any object gets the check; a non-object already failed on its own.
|
|
419
|
+
{ when: (payload) => isObjectLike(payload.value) }
|
|
420
|
+
)
|
|
414
421
|
.transform(({ exclude, include, root, sources, ...rest }) => ({
|
|
415
422
|
...rest,
|
|
416
423
|
sources: sources ?? [
|
|
@@ -456,7 +463,11 @@ const navTabSchema = z.strictObject({
|
|
|
456
463
|
)
|
|
457
464
|
.optional(),
|
|
458
465
|
label: localizableLabelSchema,
|
|
459
|
-
|
|
466
|
+
// Canonical like the routes it's matched against — one leading slash, no
|
|
467
|
+
// trailing one — so `/guides/` still marks its pages' tab and scopes their
|
|
468
|
+
// sidebar, and `api` links to `/api` rather than resolving relative to the
|
|
469
|
+
// current page.
|
|
470
|
+
path: z.string().transform(normalizeRoute),
|
|
460
471
|
});
|
|
461
472
|
|
|
462
473
|
const navSelectorItemSchema = z.strictObject({
|
|
@@ -1154,9 +1165,30 @@ const versionsConfigSchema = z
|
|
|
1154
1165
|
const REDIRECT_PATTERN = /(?:^|\/):[A-Za-z_]|\*/u;
|
|
1155
1166
|
const URL_ORIGIN = /^[a-z][\d+.a-z-]*:\/\/[^/]*/iu;
|
|
1156
1167
|
|
|
1168
|
+
/**
|
|
1169
|
+
* The shape each end must start with: `from` is always a path on this site,
|
|
1170
|
+
* `to` may also leave it. A path without its leading slash would never gain
|
|
1171
|
+
* `basePath` or the deployment base, and hosts read it relative to nothing.
|
|
1172
|
+
*/
|
|
1173
|
+
const REDIRECT_START = {
|
|
1174
|
+
from: {
|
|
1175
|
+
allows: (path: string) => path.startsWith("/"),
|
|
1176
|
+
message:
|
|
1177
|
+
"redirects take root-relative paths: `from` must start with `/` (`/old-page`, not `old-page`).",
|
|
1178
|
+
},
|
|
1179
|
+
to: {
|
|
1180
|
+
allows: (path: string) => path.startsWith("/") || URL_ORIGIN.test(path),
|
|
1181
|
+
message:
|
|
1182
|
+
"redirects take root-relative paths or full URLs: `to` must start with `/` (`/new-page`, not `new-page`) or be an absolute URL (`https://…`).",
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1157
1186
|
const exactRedirectPath = (end: "from" | "to") =>
|
|
1158
1187
|
z
|
|
1159
1188
|
.string()
|
|
1189
|
+
.refine(REDIRECT_START[end].allows, {
|
|
1190
|
+
message: REDIRECT_START[end].message,
|
|
1191
|
+
})
|
|
1160
1192
|
.refine((path) => !REDIRECT_PATTERN.test(path.replace(URL_ORIGIN, "")), {
|
|
1161
1193
|
message: `redirects take exact paths: \`${end}\` can't hold a \`:param\` segment or a \`*\` wildcard. Add one redirect per path, or put pattern rules in your host's redirect config (vercel.json, _redirects).`,
|
|
1162
1194
|
});
|
|
@@ -1271,9 +1303,9 @@ const ogConfigSchema = z.strictObject({
|
|
|
1271
1303
|
*/
|
|
1272
1304
|
enabled: z.boolean().optional(),
|
|
1273
1305
|
/**
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1306
|
+
* Fonts for the generated card, replacing the default stack (Takumi's
|
|
1307
|
+
* Latin-only built-in font plus a Noto fallback per script). Google
|
|
1308
|
+
* families are fetched from Google Fonts at build.
|
|
1277
1309
|
*/
|
|
1278
1310
|
fonts: z.array(ogFontSchema).optional(),
|
|
1279
1311
|
/**
|
|
@@ -1645,6 +1677,30 @@ const lastModifiedConfigSchema = z.union(
|
|
|
1645
1677
|
* build machine's zone. `dateStyle` is a preset that can't be combined with the
|
|
1646
1678
|
* individual component fields (`year`, `month`, …), matching `Intl`'s own rule.
|
|
1647
1679
|
*/
|
|
1680
|
+
/** The `dateFormat` fields that choose the date's shape (length, components). */
|
|
1681
|
+
const DATE_FORM_FIELDS = [
|
|
1682
|
+
"dateStyle",
|
|
1683
|
+
"weekday",
|
|
1684
|
+
"era",
|
|
1685
|
+
"year",
|
|
1686
|
+
"month",
|
|
1687
|
+
"day",
|
|
1688
|
+
] as const;
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Whether `Intl` knows a time zone. An unknown one (`Asia/Tokio`) throws a
|
|
1692
|
+
* RangeError the first time a date is formatted, deep in a page render, so it
|
|
1693
|
+
* is caught here where the error can point at `dateFormat.timeZone`.
|
|
1694
|
+
*/
|
|
1695
|
+
const isKnownTimeZone = (timeZone: string): boolean => {
|
|
1696
|
+
try {
|
|
1697
|
+
Intl.DateTimeFormat("en", { timeZone });
|
|
1698
|
+
return true;
|
|
1699
|
+
} catch {
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1648
1704
|
const dateFormatConfigSchema = z
|
|
1649
1705
|
.strictObject({
|
|
1650
1706
|
/** Calendar system (e.g. `japanese`, `buddhist`). */
|
|
@@ -1660,7 +1716,13 @@ const dateFormatConfigSchema = z
|
|
|
1660
1716
|
/** Numbering system (e.g. `latn`, `arab`). */
|
|
1661
1717
|
numberingSystem: z.string().optional(),
|
|
1662
1718
|
/** IANA time zone (e.g. `Asia/Tokyo`). Defaults to `UTC`. */
|
|
1663
|
-
timeZone: z
|
|
1719
|
+
timeZone: z
|
|
1720
|
+
.string()
|
|
1721
|
+
.refine(isKnownTimeZone, {
|
|
1722
|
+
message:
|
|
1723
|
+
'Unknown time zone; use an IANA name like "Asia/Tokyo" or "UTC".',
|
|
1724
|
+
})
|
|
1725
|
+
.optional(),
|
|
1664
1726
|
/** Weekday representation. */
|
|
1665
1727
|
weekday: z.enum(["long", "short", "narrow"]).optional(),
|
|
1666
1728
|
/** Year representation. */
|
|
@@ -1678,6 +1740,14 @@ const dateFormatConfigSchema = z
|
|
|
1678
1740
|
message:
|
|
1679
1741
|
"dateFormat.dateStyle can't be combined with weekday/era/year/month/day; use one or the other.",
|
|
1680
1742
|
}
|
|
1743
|
+
)
|
|
1744
|
+
// `timeZone`, `calendar`, and `numberingSystem` don't pick a shape, so a
|
|
1745
|
+
// format that sets only those keeps the long default instead of falling to
|
|
1746
|
+
// Intl's bare numeric date (`7/21/2026`).
|
|
1747
|
+
.transform((value) =>
|
|
1748
|
+
DATE_FORM_FIELDS.some((field) => value[field] !== undefined)
|
|
1749
|
+
? value
|
|
1750
|
+
: { ...value, dateStyle: "long" as const }
|
|
1681
1751
|
);
|
|
1682
1752
|
|
|
1683
1753
|
/** Code rendering options (`markdown.code`). */
|
|
@@ -1806,6 +1876,12 @@ export const blumeConfigSchema = z
|
|
|
1806
1876
|
*/
|
|
1807
1877
|
basePath: z
|
|
1808
1878
|
.string()
|
|
1879
|
+
// Normalizing would turn a URL into `/https:/docs.example.com/docs`
|
|
1880
|
+
// and keep a query or fragment inside every route.
|
|
1881
|
+
.refine((value) => !URL_ORIGIN.test(value) && !/[#?]/u.test(value), {
|
|
1882
|
+
message:
|
|
1883
|
+
'basePath takes a path like "/docs", not a URL: no scheme, host, query, or fragment. The site\'s origin goes in deployment.site.',
|
|
1884
|
+
})
|
|
1809
1885
|
.optional()
|
|
1810
1886
|
.transform((value) => normalizeBasePath(value)),
|
|
1811
1887
|
content: contentConfigSchema.prefault({}),
|
|
@@ -13,7 +13,14 @@ import { extname, join } from "pathe";
|
|
|
13
13
|
import type { Diagnostic } from "../types.ts";
|
|
14
14
|
import { hashText } from "./cache.ts";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// A Markdown image as the lowerers write it (`image()` in `lower.ts`): the
|
|
17
|
+
// alt text can hold backslash escapes (`\]`), and a destination holding
|
|
18
|
+
// spaces or parentheses sits in angle brackets, both with its own escapes.
|
|
19
|
+
const MD_IMAGE =
|
|
20
|
+
/!\[(?<alt>(?:\\.|[^\\\]])*)\]\((?:<(?<pointy>(?:\\.|[^\\<>\n])*)>|(?<bare>(?:\\.|[^\\\s)])+))\)/gu;
|
|
21
|
+
// A backslash escape inside a destination, which Markdown reads as the
|
|
22
|
+
// character it escapes.
|
|
23
|
+
const DESTINATION_ESCAPE = /\\(?<char>[!-/:-@[-`{-~])/gu;
|
|
17
24
|
// A `<video>` tag's `src`, split so the rewrite can swap the URL and keep the
|
|
18
25
|
// surrounding attributes untouched. Notion's uploaded videos arrive as signed,
|
|
19
26
|
// expiring URLs exactly like its images, so they rot the same way. `[^>]`
|
|
@@ -21,35 +28,47 @@ const MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
|
|
|
21
28
|
const HTML_VIDEO_SRC =
|
|
22
29
|
/(?<open><video\b[^>]*?\ssrc=")(?<url>[^"]+)(?<close>")/gu;
|
|
23
30
|
const REMOTE = /^https?:\/\//u;
|
|
24
|
-
const SAFE_EXT = /^\.[a-z0-9]+$/iu;
|
|
25
31
|
const CODE_FENCE_BLOCK =
|
|
26
32
|
/^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
|
|
27
33
|
// NUL delimiters cannot appear in authored markdown, so tokens never collide.
|
|
28
34
|
// oxlint-disable-next-line no-control-regex -- the NUL is the collision guard.
|
|
29
35
|
const FENCE_TOKEN = /\u0000blume-fence-(?<index>\d+)\u0000/gu;
|
|
30
36
|
|
|
31
|
-
// The extension
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
37
|
+
// The extension an asset is saved under, keyed by the media type the server
|
|
38
|
+
// reports. Only images and videos are ever written: the file is served from
|
|
39
|
+
// the docs origin by its extension, so an HTML page (a Vimeo/Loom watch page,
|
|
40
|
+
// or a hostile "image" URL) saved as `.html` would render as a page on the
|
|
41
|
+
// site. A static host serves by extension, so a `.png` holding JPEG bytes is
|
|
42
|
+
// mislabeled and a `.png` holding a video is refused by strict players — the
|
|
43
|
+
// response is the source of truth, not the reference kind.
|
|
35
44
|
const EXT_BY_MIME = new Map([
|
|
45
|
+
["image/apng", ".apng"],
|
|
36
46
|
["image/avif", ".avif"],
|
|
47
|
+
["image/bmp", ".bmp"],
|
|
37
48
|
["image/gif", ".gif"],
|
|
38
49
|
["image/jpeg", ".jpg"],
|
|
50
|
+
["image/jpg", ".jpg"],
|
|
39
51
|
["image/png", ".png"],
|
|
40
52
|
["image/svg+xml", ".svg"],
|
|
53
|
+
["image/tiff", ".tiff"],
|
|
54
|
+
["image/vnd.microsoft.icon", ".ico"],
|
|
41
55
|
["image/webp", ".webp"],
|
|
56
|
+
["image/x-icon", ".ico"],
|
|
42
57
|
["video/mp4", ".mp4"],
|
|
43
58
|
["video/ogg", ".ogv"],
|
|
44
59
|
["video/quicktime", ".mov"],
|
|
45
60
|
["video/webm", ".webm"],
|
|
46
61
|
]);
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
// The same images and videos as a URL's path names them, for a response
|
|
63
|
+
// whose type says nothing about the bytes.
|
|
64
|
+
const MEDIA_EXTS = new Set([...EXT_BY_MIME.values(), ".jpeg"]);
|
|
65
|
+
// Response types that don't say what the bytes are; object stores answer
|
|
66
|
+
// uploads this way, so the URL's extension decides.
|
|
67
|
+
const OPAQUE_TYPES = new Set([
|
|
68
|
+
"",
|
|
69
|
+
"application/octet-stream",
|
|
70
|
+
"binary/octet-stream",
|
|
71
|
+
]);
|
|
53
72
|
const PART_SUFFIX = ".part";
|
|
54
73
|
// Generous enough for a multi-hundred-megabyte recording on an ordinary
|
|
55
74
|
// connection; its job is to fail a stalled download rather than hang the build.
|
|
@@ -78,13 +97,17 @@ export interface AssetContext {
|
|
|
78
97
|
timeoutMs?: number;
|
|
79
98
|
}
|
|
80
99
|
|
|
81
|
-
/** The extension in a URL's path, or null when it
|
|
100
|
+
/** The image or video extension in a URL's path, or null when it names none. */
|
|
82
101
|
const extFromUrl = (url: string): string | null => {
|
|
83
102
|
const clean = url.split("?")[0] ?? url;
|
|
84
|
-
const ext = extname(clean);
|
|
85
|
-
return
|
|
103
|
+
const ext = extname(clean).toLowerCase();
|
|
104
|
+
return MEDIA_EXTS.has(ext) ? ext : null;
|
|
86
105
|
};
|
|
87
106
|
|
|
107
|
+
/** An image's destination as the URL it names, its escapes decoded. */
|
|
108
|
+
const imageUrl = (pointy?: string, bare?: string): string =>
|
|
109
|
+
(pointy ?? bare ?? "").replaceAll(DESTINATION_ESCAPE, "$<char>");
|
|
110
|
+
|
|
88
111
|
/** The response's media type, lowercased and stripped of parameters. */
|
|
89
112
|
const mediaType = (res: Response): string =>
|
|
90
113
|
res.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
@@ -100,9 +123,10 @@ const exists = async (path: string): Promise<boolean> => {
|
|
|
100
123
|
|
|
101
124
|
/**
|
|
102
125
|
* The completed file for a stem whose extension came from an earlier response
|
|
103
|
-
* rather than the URL, so
|
|
104
|
-
*
|
|
105
|
-
*
|
|
126
|
+
* rather than the URL, so the asset is not fetched on every poll. Only a lone
|
|
127
|
+
* image or video counts: query-less URLs can collide, picking between two
|
|
128
|
+
* files would be a guess, and a partial download (or any other file an older
|
|
129
|
+
* version saved) is never reused.
|
|
106
130
|
*/
|
|
107
131
|
const completedFor = async (
|
|
108
132
|
dir: string,
|
|
@@ -115,11 +139,28 @@ const completedFor = async (
|
|
|
115
139
|
return null;
|
|
116
140
|
}
|
|
117
141
|
const candidates = names.filter(
|
|
118
|
-
(name) => name.startsWith(
|
|
142
|
+
(name) => name.startsWith(stem) && MEDIA_EXTS.has(name.slice(stem.length))
|
|
119
143
|
);
|
|
120
144
|
return candidates.length === 1 ? (candidates[0] ?? null) : null;
|
|
121
145
|
};
|
|
122
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The extension to save a response under: the image or video type it
|
|
149
|
+
* reports, or the URL's image or video extension when the type says nothing.
|
|
150
|
+
* Anything else is refused rather than written where the site would serve it.
|
|
151
|
+
*/
|
|
152
|
+
const extFor = (type: string, urlExt: string | null): string => {
|
|
153
|
+
const ext = EXT_BY_MIME.get(type) ?? (OPAQUE_TYPES.has(type) ? urlExt : null);
|
|
154
|
+
if (ext) {
|
|
155
|
+
return ext;
|
|
156
|
+
}
|
|
157
|
+
throw new Error(
|
|
158
|
+
OPAQUE_TYPES.has(type)
|
|
159
|
+
? `responded with ${type || "no content type"} and its URL names no image or video file`
|
|
160
|
+
: `responded with ${type}, not an image or video`
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
123
164
|
/**
|
|
124
165
|
* Download remote media referenced in a Markdown body into the asset dir and
|
|
125
166
|
* rewrite the reference to the local public path. Markdown images and the
|
|
@@ -148,26 +189,26 @@ export const materializeAssets = async (
|
|
|
148
189
|
return `\u0000blume-fence-${fences.length - 1}\u0000`;
|
|
149
190
|
});
|
|
150
191
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
192
|
+
const referenced = [
|
|
193
|
+
...[...masked.matchAll(MD_IMAGE)].map((match) =>
|
|
194
|
+
imageUrl(match.groups?.pointy, match.groups?.bare)
|
|
195
|
+
),
|
|
196
|
+
...[...masked.matchAll(HTML_VIDEO_SRC)].map(
|
|
197
|
+
(match) => match.groups?.url ?? ""
|
|
198
|
+
),
|
|
199
|
+
];
|
|
200
|
+
const urls = new Set(referenced.filter((url) => REMOTE.test(url)));
|
|
160
201
|
|
|
161
202
|
/** Fetch one asset into the asset dir and return its file name. */
|
|
162
203
|
const download = async (url: string, stem: string): Promise<string> => {
|
|
163
204
|
const urlExt = extFromUrl(url);
|
|
164
|
-
// The name is known up front
|
|
165
|
-
// Notion upload
|
|
205
|
+
// The name is usually known up front from the path's extension (every
|
|
206
|
+
// Notion upload has one), so a file from an earlier run is reused as is;
|
|
166
207
|
// otherwise the extension came from the last response, so look for it.
|
|
167
208
|
if (urlExt && (await exists(join(ctx.assetsDir, `${stem}${urlExt}`)))) {
|
|
168
209
|
return `${stem}${urlExt}`;
|
|
169
210
|
}
|
|
170
|
-
const completed =
|
|
211
|
+
const completed = await completedFor(ctx.assetsDir, stem);
|
|
171
212
|
if (completed) {
|
|
172
213
|
return completed;
|
|
173
214
|
}
|
|
@@ -176,15 +217,13 @@ export const materializeAssets = async (
|
|
|
176
217
|
throw new Error(`${res.status}`);
|
|
177
218
|
}
|
|
178
219
|
// Writing a watch page or an API response out as `.mp4` gives a player
|
|
179
|
-
// that can't play and a green build,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
throw new Error(`responded with ${type}, not a media file`);
|
|
183
|
-
}
|
|
220
|
+
// that can't play and a green build, and one saved as `.html` would be
|
|
221
|
+
// served as a page on the docs origin: only images and videos are kept.
|
|
222
|
+
const ext = extFor(mediaType(res), urlExt);
|
|
184
223
|
if (!res.body) {
|
|
185
224
|
throw new Error("empty response body");
|
|
186
225
|
}
|
|
187
|
-
const file = `${stem}${
|
|
226
|
+
const file = `${stem}${ext}`;
|
|
188
227
|
const target = join(ctx.assetsDir, file);
|
|
189
228
|
await mkdir(ctx.assetsDir, { recursive: true });
|
|
190
229
|
// Stream the body to disk rather than buffering it: a video is hundreds of
|
|
@@ -258,10 +297,13 @@ export const materializeAssets = async (
|
|
|
258
297
|
);
|
|
259
298
|
|
|
260
299
|
const rewritten = masked
|
|
261
|
-
.replaceAll(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
300
|
+
.replaceAll(
|
|
301
|
+
MD_IMAGE,
|
|
302
|
+
(match: string, alt: string, pointy?: string, bare?: string) => {
|
|
303
|
+
const local = rewrites.get(imageUrl(pointy, bare));
|
|
304
|
+
return local ? `` : match;
|
|
305
|
+
}
|
|
306
|
+
)
|
|
265
307
|
.replaceAll(HTML_VIDEO_SRC, (match, open, url, close) => {
|
|
266
308
|
const local = rewrites.get(url);
|
|
267
309
|
return local ? `${open}${local}${close}` : match;
|