blume 2.0.2 → 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 +65 -0
- package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
- package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
- package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
- package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
- package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
- package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
- package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
- package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
- package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
- package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
- package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
- package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
- package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
- package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
- package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
- package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
- package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
- package/dist/cli/chunk-9t7a85s3.js.map +10 -0
- package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
- package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
- package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
- package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
- package/dist/cli/chunk-akbpwfxc.js.map +10 -0
- package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
- package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
- package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
- package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
- 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-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
- package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
- package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
- package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
- package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
- package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
- package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
- package/dist/cli/chunk-j85vccga.js.map +36 -0
- package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
- package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
- package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
- package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
- package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
- package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
- package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
- package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
- package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
- package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
- package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
- package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
- package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
- package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
- package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
- package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
- package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
- package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
- package/dist/cli/chunk-yfyb25rh.js.map +14 -0
- package/dist/cli/index.js +20 -18
- package/dist/cli/index.js.map +3 -3
- 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 +9 -7
- package/dist/types/ai/skills.d.ts +4 -1
- package/dist/types/ai/tar.d.ts +1 -3
- package/dist/types/analytics/index.d.ts +2 -0
- package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
- package/dist/types/analytics/schema.d.ts +14 -0
- package/dist/types/astro/integration.d.ts +3 -2
- package/dist/types/core/base-path.d.ts +21 -7
- package/dist/types/core/config-input.d.ts +9 -7
- package/dist/types/core/directive-diagnostics.d.ts +12 -0
- package/dist/types/core/heading-markers.d.ts +5 -7
- package/dist/types/core/i18n.d.ts +2 -0
- package/dist/types/core/last-modified.d.ts +10 -0
- package/dist/types/core/meta.d.ts +8 -0
- package/dist/types/core/schema.d.ts +7 -0
- package/dist/types/core/sources/lower.d.ts +29 -16
- package/dist/types/core/sources/normalize.d.ts +13 -1
- package/dist/types/core/sources/watch.d.ts +12 -5
- package/dist/types/core/standard-schema.d.ts +5 -0
- package/dist/types/deploy/artifacts.d.ts +6 -4
- package/dist/types/deploy/headers.d.ts +5 -0
- package/dist/types/deploy/platforms/types.d.ts +8 -0
- package/dist/types/deploy/redirects.d.ts +24 -13
- 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/docs/02-deployment.mdx +4 -2
- package/docs/08-faq.mdx +1 -1
- package/docs/advanced/custom-pages.mdx +3 -3
- package/docs/cli/audit.mdx +17 -1
- package/docs/cli/evals.mdx +3 -3
- package/docs/cli/translate.mdx +3 -3
- package/docs/cli/version.mdx +1 -1
- package/docs/configuration/analytics.mdx +20 -1
- package/docs/configuration/customization.mdx +2 -0
- package/docs/configuration/search.mdx +1 -1
- package/docs/content/components.mdx +1 -1
- package/docs/content/frontmatter.mdx +1 -1
- package/docs/content/i18n.mdx +2 -0
- package/docs/content/index.mdx +4 -2
- package/docs/content/islands.mdx +1 -1
- package/docs/content/meta.mdx +3 -1
- package/docs/content/sources.mdx +1 -5
- package/docs/content/syntax.mdx +14 -0
- package/docs/content/versioning.mdx +1 -1
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/index.mdx +2 -1
- package/docs/discoverability/markdown.mdx +2 -2
- package/docs/discoverability/open-graph.mdx +5 -3
- package/docs/discoverability/rss.mdx +1 -1
- package/docs/references/asyncapi.mdx +1 -1
- package/docs/references/graphql.mdx +1 -1
- package/docs/references/openapi.mdx +5 -3
- package/package.json +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/nextra.md +1 -1
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
- package/src/ai/agent-readability.ts +2 -2
- package/src/ai/ai-catalog.ts +6 -2
- package/src/ai/api/handlers.ts +2 -2
- package/src/ai/api/spec.ts +2 -2
- package/src/ai/api-catalog.ts +6 -2
- package/src/ai/changelog-markdown.ts +2 -2
- package/src/ai/link-headers.ts +14 -5
- package/src/ai/llms.ts +18 -7
- package/src/ai/mcp/discovery.ts +2 -2
- package/src/ai/mcp/query.ts +2 -2
- package/src/ai/mcp/server.ts +2 -2
- package/src/ai/openapi-components.ts +22 -6
- package/src/ai/relative-links.ts +42 -26
- package/src/ai/serializers.ts +2 -1
- package/src/ai/skills.ts +14 -1
- package/src/ai/tar.ts +139 -12
- package/src/analytics/head.ts +4 -0
- package/src/analytics/index.ts +5 -0
- package/src/analytics/one-dollar-stats.ts +86 -0
- package/src/analytics/schema.ts +2 -0
- package/src/astro/generate.ts +1 -1
- 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 +81 -37
- package/src/audit/checks/links.ts +8 -1
- package/src/audit/graph.ts +3 -1
- package/src/audit/run.ts +12 -9
- package/src/audit/snapshot.ts +5 -0
- package/src/cli/args.ts +32 -0
- package/src/cli/commands/audit.ts +4 -1
- package/src/cli/commands/doctor.ts +3 -1
- package/src/cli/commands/eval.ts +19 -11
- package/src/cli/commands/translate.ts +9 -12
- package/src/cli/commands/validate.ts +3 -1
- package/src/cli/dev-lock.ts +157 -37
- package/src/cli/report-format.ts +11 -6
- package/src/components/colors.ts +19 -0
- package/src/components/content/Badge.astro +5 -3
- package/src/components/content/Component.astro +2 -2
- package/src/components/content/Tab.astro +0 -1
- package/src/components/content/Tabs.astro +4 -0
- package/src/components/content/badge-color.ts +5 -2
- package/src/components/content/base-href.ts +13 -36
- package/src/components/islands/assistant.tsx +21 -4
- package/src/components/islands/base-path.ts +47 -10
- package/src/components/islands/hooks.ts +35 -19
- package/src/components/islands/webmcp.ts +4 -2
- package/src/components/layout/Breadcrumbs.astro +5 -2
- package/src/components/layout/DiscoveryLinks.astro +9 -5
- package/src/components/layout/Header.astro +2 -2
- package/src/components/layout/LanguageSwitcher.astro +2 -2
- package/src/components/layout/NavSelector.astro +2 -2
- package/src/components/layout/NavTabMenu.astro +3 -3
- package/src/components/layout/NavTree.astro +16 -6
- package/src/components/layout/PageActions.astro +4 -3
- package/src/components/layout/PageLayout.astro +7 -6
- package/src/components/layout/Pagination.astro +3 -3
- package/src/components/layout/RootLayout.astro +5 -5
- package/src/components/layout/Search.astro +33 -13
- package/src/components/layout/VersionBanner.astro +2 -2
- package/src/components/layout/analytics-client.ts +12 -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/Operation.astro +2 -0
- package/src/components/openapi/Playground.astro +4 -4
- package/src/components/openapi/RequestPanel.astro +5 -2
- package/src/components/openapi/SchemaTable.astro +7 -0
- package/src/components/openapi/async.ts +38 -6
- package/src/components/openapi/helpers.ts +19 -9
- package/src/components/openapi/message-composer.ts +6 -1
- package/src/components/openapi/message.ts +17 -2
- package/src/components/openapi/operation-model.ts +81 -11
- package/src/components/openapi/panel.ts +4 -2
- package/src/components/openapi/param-style.ts +181 -0
- package/src/components/openapi/playground-client.ts +73 -16
- package/src/components/openapi/request.ts +190 -26
- package/src/components/openapi/schema-tree.ts +30 -24
- package/src/components/openapi/snippets.ts +85 -6
- package/src/components/openapi/ws-client.ts +18 -2
- package/src/core/base-path.ts +62 -16
- package/src/core/config-input.ts +9 -7
- package/src/core/diagnostics.ts +2 -0
- package/src/core/directive-diagnostics.ts +99 -0
- package/src/core/frontmatter.ts +21 -18
- package/src/core/heading-markers.ts +5 -18
- package/src/core/i18n.ts +31 -3
- package/src/core/last-modified.ts +25 -3
- package/src/core/locale-links.ts +5 -1
- package/src/core/meta.ts +33 -19
- package/src/core/navigation.ts +28 -8
- package/src/core/project-graph.ts +27 -5
- package/src/core/schema.ts +24 -4
- package/src/core/sources/contentful-rich-text.ts +27 -20
- package/src/core/sources/filesystem.ts +20 -2
- package/src/core/sources/github-releases.ts +25 -6
- package/src/core/sources/lexical.ts +23 -18
- package/src/core/sources/lower.ts +201 -34
- package/src/core/sources/mdx-remote.ts +51 -16
- package/src/core/sources/normalize.ts +325 -90
- package/src/core/sources/notion.ts +52 -25
- package/src/core/sources/obsidian.ts +23 -6
- package/src/core/sources/portable-text.ts +38 -21
- package/src/core/sources/strapi-blocks.ts +20 -16
- package/src/core/sources/watch.ts +20 -7
- package/src/core/standard-schema.ts +10 -6
- package/src/core/version-cut.ts +52 -15
- package/src/deploy/artifacts.ts +27 -6
- package/src/deploy/cloudflare-negotiation.ts +4 -12
- package/src/deploy/headers.ts +8 -4
- package/src/deploy/node-headers.ts +1 -1
- package/src/deploy/platforms/cloudflare.ts +12 -3
- package/src/deploy/platforms/netlify.ts +1 -0
- package/src/deploy/platforms/node.ts +1 -0
- package/src/deploy/platforms/static.ts +1 -0
- package/src/deploy/platforms/types.ts +8 -0
- package/src/deploy/platforms/vercel.ts +1 -0
- package/src/deploy/redirects.ts +54 -18
- package/src/deploy/robots.ts +2 -2
- package/src/deploy/rss.ts +4 -3
- package/src/deploy/sitemap.ts +7 -5
- package/src/markdown/base-links.ts +34 -36
- package/src/markdown/directives.ts +242 -36
- package/src/markdown/features.ts +17 -0
- package/src/markdown/index.ts +7 -6
- package/src/markdown/mdast.ts +5 -2
- package/src/markdown/relative-links.ts +12 -4
- package/src/og/card.ts +129 -5
- package/src/og/derive.ts +41 -31
- package/src/og/index.ts +1 -0
- package/src/openapi/asyncapi.ts +4 -2
- package/src/openapi/model.ts +22 -14
- package/src/openapi/proxy.ts +63 -10
- package/src/openapi/render-mdx.ts +41 -2
- package/src/registry/eject.ts +181 -24
- package/src/search/adapters/version-scope.ts +30 -0
- package/src/search/documents.ts +67 -26
- package/src/search/popular.ts +2 -1
- package/src/seo/jsonld.ts +7 -3
- package/src/translate/meta.ts +68 -24
- package/src/translate/run.ts +3 -3
- package/src/translate/validate.ts +4 -1
- package/src/translate/work-list.ts +56 -17
- package/dist/cli/chunk-5r8g91qn.js.map +0 -34
- package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
- package/dist/cli/chunk-ddndchfr.js.map +0 -14
- package/dist/cli/chunk-esh98wmb.js +0 -23
- package/dist/cli/chunk-esh98wmb.js.map +0 -10
- package/dist/cli/chunk-hr8ne106.js.map +0 -13
- package/dist/cli/chunk-j85scx15.js.map +0 -10
- package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
- package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
- /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
- /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
- /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
- /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
- /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
- /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
- /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
- /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
- /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
- /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
- /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
- /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
- /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
- /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
- /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
- /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
- /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
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
|
|
|
8
|
+
import { MARKDOWN_FEATURES } from "../../markdown/features.ts";
|
|
6
9
|
import { mountBasePath } from "../base-path.ts";
|
|
7
10
|
import { nextFenceState } from "../code-fences.ts";
|
|
8
11
|
import type { FenceState } from "../code-fences.ts";
|
|
@@ -33,6 +36,14 @@ const stripNumericPrefix = (segment: string): string =>
|
|
|
33
36
|
const groupLabel = (segment: string): string | null =>
|
|
34
37
|
segment.match(GROUP_FOLDER)?.groups?.label ?? null;
|
|
35
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
|
+
|
|
36
47
|
/**
|
|
37
48
|
* Slugify a content/route slug (Sanity, Notion, frontmatter `slug`). Heading
|
|
38
49
|
* anchor ids are *not* slugged here — they use a `github-slugger` in
|
|
@@ -74,11 +85,46 @@ const titleCase = (value: string): string =>
|
|
|
74
85
|
* scheme (`Guide: Architecture.md` → `guide:`), which crashes Astro's
|
|
75
86
|
* prerender write with "The URL must be of scheme file"; control characters
|
|
76
87
|
* (an embedded newline in a filename) are silently dropped by the URL parser,
|
|
77
|
-
* desyncing the route from its output path.
|
|
78
|
-
*
|
|
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}).
|
|
79
97
|
*/
|
|
80
98
|
const sanitizeSegment = (segment: string): string =>
|
|
81
|
-
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;
|
|
105
|
+
|
|
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;
|
|
82
128
|
|
|
83
129
|
/**
|
|
84
130
|
* Fold one raw path part into the accumulating route segments/groups.
|
|
@@ -95,7 +141,7 @@ const addRouteSegment = (
|
|
|
95
141
|
if (part === "") {
|
|
96
142
|
return;
|
|
97
143
|
}
|
|
98
|
-
const group = groupLabel(part);
|
|
144
|
+
const group = ordered ? orderedGroupLabel(part) : groupLabel(part);
|
|
99
145
|
if (group !== null) {
|
|
100
146
|
groups.push(group);
|
|
101
147
|
return;
|
|
@@ -166,7 +212,6 @@ const SETEXT_UNDERLINE = /^ {0,3}(?<marker>=+|-+)\s*$/u;
|
|
|
166
212
|
const PARAGRAPH_INTERRUPT = /^ {0,3}(?:[-+*][ \t]|\d{1,9}[.)][ \t]|>)/u;
|
|
167
213
|
const THEMATIC_BREAK =
|
|
168
214
|
/^ {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$/u;
|
|
169
|
-
const FRONT_MATTER_CLOSE = /^(?:-{3}|\.{3})\s*$/u;
|
|
170
215
|
// `<Prompt>` renders its children into a permanently `hidden` DOM node (see
|
|
171
216
|
// `Prompt.astro`) — the agent-facing prompt text is never visible page
|
|
172
217
|
// content, only read by client JS for the copy button. Any `##` inside it
|
|
@@ -183,41 +228,13 @@ const PROMPT_OPEN = /^<Prompt(?![\w-])/u;
|
|
|
183
228
|
// children text (`...copy this.</Prompt>`), not just sit on its own line.
|
|
184
229
|
const PROMPT_CLOSE = /<\/Prompt>/u;
|
|
185
230
|
|
|
186
|
-
/**
|
|
187
|
-
* The body lines, minus a leading front matter block, plus the height of the
|
|
188
|
-
* block that was dropped (`offset`) so line numbers can be reported against
|
|
189
|
-
* the whole body. Bodies from the normalize pipeline are already
|
|
190
|
-
* frontmatter-stripped, but `scanBody` also runs on raw documents — where a
|
|
191
|
-
* leading `---` block (closed by `---` or `...`) is front matter, not a
|
|
192
|
-
* thematic break whose closing `---` would underline the last metadata line
|
|
193
|
-
* into a phantom setext heading.
|
|
194
|
-
*/
|
|
195
|
-
const linesWithoutFrontMatter = (body: string) => {
|
|
196
|
-
const lines = body.split("\n");
|
|
197
|
-
if (!/^-{3}\s*$/u.test(lines[0] ?? "")) {
|
|
198
|
-
return { lines, offset: 0 };
|
|
199
|
-
}
|
|
200
|
-
// A blank line directly after the dashes means the body *opens* with a
|
|
201
|
-
// thematic break, not front matter — YAML metadata starts on the very next
|
|
202
|
-
// line. Treating it as an unclosed block ate everything up to the next
|
|
203
|
-
// `---`/`...` line of an already-stripped body.
|
|
204
|
-
if ((lines[1] ?? "").trim() === "") {
|
|
205
|
-
return { lines, offset: 0 };
|
|
206
|
-
}
|
|
207
|
-
const close = lines.findIndex(
|
|
208
|
-
(line, index) => index > 0 && FRONT_MATTER_CLOSE.test(line)
|
|
209
|
-
);
|
|
210
|
-
return close === -1
|
|
211
|
-
? { lines, offset: 0 }
|
|
212
|
-
: { lines: lines.slice(close + 1), offset: close + 1 };
|
|
213
|
-
};
|
|
214
|
-
|
|
215
231
|
// A trailing `{#id}` heading marker written without a backslash escape. Both
|
|
216
|
-
// pipelines resolve escapes before parsing markers (
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
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.
|
|
221
238
|
const BARE_CURLY_MARKER =
|
|
222
239
|
/(?<!\\)\{#(?<id>[^\s}]+)\}(?:\s*\[(?:#[^\s\]]+|!?toc)\])*\s*$/u;
|
|
223
240
|
|
|
@@ -311,12 +328,31 @@ const finishPromptTag = (
|
|
|
311
328
|
* collapses `--`; github-slugger keeps it) and resolves repeated headings the
|
|
312
329
|
* same way (`setup`, `setup-1`).
|
|
313
330
|
*/
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
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;
|
|
320
356
|
|
|
321
357
|
// The start of a link-reference definition, as the renderer accepts it:
|
|
322
358
|
// `[label]:` after up to 3 leading spaces, optionally inside block-quote or
|
|
@@ -334,10 +370,13 @@ const REF_DEFINITION =
|
|
|
334
370
|
|
|
335
371
|
/**
|
|
336
372
|
* The normalized labels of every link-reference definition in the body
|
|
337
|
-
* (outside fenced code)
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
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).
|
|
341
380
|
*
|
|
342
381
|
* Only a *valid* definition defines a label: the label must contain a
|
|
343
382
|
* non-whitespace character, and a destination must follow — on the same line
|
|
@@ -369,14 +408,179 @@ const refDefinitionLabels = (lines: readonly string[]): Set<string> => {
|
|
|
369
408
|
};
|
|
370
409
|
|
|
371
410
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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.
|
|
380
584
|
*/
|
|
381
585
|
/** A scanned heading plus whether its id came from an author pin. */
|
|
382
586
|
interface ScannedHeading {
|
|
@@ -387,23 +591,23 @@ interface ScannedHeading {
|
|
|
387
591
|
const toHeading = (
|
|
388
592
|
depth: number,
|
|
389
593
|
raw: string,
|
|
594
|
+
form: "atx" | "setext",
|
|
390
595
|
slugger: GithubSlugger,
|
|
391
|
-
|
|
596
|
+
context: HeadingContext
|
|
392
597
|
): ScannedHeading => {
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
};
|
|
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 };
|
|
401
606
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
return { heading: { depth, slug: slugger.slug(text), text }, pinned: false };
|
|
607
|
+
return {
|
|
608
|
+
heading: { depth, slug: slugger.slug(rendered.text), text },
|
|
609
|
+
pinned: false,
|
|
610
|
+
};
|
|
407
611
|
};
|
|
408
612
|
|
|
409
613
|
/** Record a heading and where a trailing marker would be written for it. */
|
|
@@ -435,7 +639,7 @@ const scanContentLine = (
|
|
|
435
639
|
state: HeadingScanState,
|
|
436
640
|
slugger: GithubSlugger,
|
|
437
641
|
headings: Heading[],
|
|
438
|
-
|
|
642
|
+
context: HeadingContext
|
|
439
643
|
): void => {
|
|
440
644
|
// Inline code is masked so a documented `<a id="…">` isn't an anchor.
|
|
441
645
|
state.anchorLines.push(line.replaceAll(INLINE_CODE, ""));
|
|
@@ -446,7 +650,7 @@ const scanContentLine = (
|
|
|
446
650
|
pushHeading(
|
|
447
651
|
headings,
|
|
448
652
|
state,
|
|
449
|
-
toHeading(depth, text, slugger,
|
|
653
|
+
toHeading(depth, text, "atx", slugger, context),
|
|
450
654
|
state.line
|
|
451
655
|
);
|
|
452
656
|
noteCurlyMarker(text, state.line, state);
|
|
@@ -456,15 +660,17 @@ const scanContentLine = (
|
|
|
456
660
|
const setext = line.match(SETEXT_UNDERLINE);
|
|
457
661
|
if (setext?.groups && state.paragraph.length > 0) {
|
|
458
662
|
// Setext wins over thematic break when it closes a paragraph (CommonMark);
|
|
459
|
-
// 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`).
|
|
460
666
|
const depth = setext.groups.marker?.startsWith("=") ? 1 : 2;
|
|
461
|
-
const text = state.paragraph.join("
|
|
667
|
+
const text = state.paragraph.join("\n");
|
|
462
668
|
// A setext heading's markers trail its last text line, just above the
|
|
463
669
|
// underline — that is where a pin is appended.
|
|
464
670
|
pushHeading(
|
|
465
671
|
headings,
|
|
466
672
|
state,
|
|
467
|
-
toHeading(depth, text, slugger,
|
|
673
|
+
toHeading(depth, text, "setext", slugger, context),
|
|
468
674
|
state.line - 1
|
|
469
675
|
);
|
|
470
676
|
noteCurlyMarker(text, state.paragraphStart, state);
|
|
@@ -533,7 +739,7 @@ const scanHeadingLine = (
|
|
|
533
739
|
state: HeadingScanState,
|
|
534
740
|
slugger: GithubSlugger,
|
|
535
741
|
headings: Heading[],
|
|
536
|
-
|
|
742
|
+
context: HeadingContext
|
|
537
743
|
): void => {
|
|
538
744
|
// Comments hide fences too, and fences and prompts hide comments. A comment
|
|
539
745
|
// line still goes to the anchor pass, which strips HTML comments itself.
|
|
@@ -577,7 +783,7 @@ const scanHeadingLine = (
|
|
|
577
783
|
state.paragraph = [];
|
|
578
784
|
return;
|
|
579
785
|
}
|
|
580
|
-
scanContentLine(line, state, slugger, headings,
|
|
786
|
+
scanContentLine(line, state, slugger, headings, context);
|
|
581
787
|
};
|
|
582
788
|
|
|
583
789
|
/** Where a heading's text ends in the scanned text, for appending a marker. */
|
|
@@ -607,7 +813,10 @@ export interface BodyScan {
|
|
|
607
813
|
/**
|
|
608
814
|
* Scan a body for its headings, explicit HTML anchors, and unescaped `{#id}`
|
|
609
815
|
* markers in one fence-aware walk (the same walk `extractHeadings` exposes for
|
|
610
|
-
* 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.
|
|
611
820
|
*/
|
|
612
821
|
export const scanBody = (body: string): BodyScan => {
|
|
613
822
|
const headings: Heading[] = [];
|
|
@@ -625,13 +834,18 @@ export const scanBody = (body: string): BodyScan => {
|
|
|
625
834
|
sites: [],
|
|
626
835
|
};
|
|
627
836
|
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
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
|
+
};
|
|
632
846
|
for (const [index, line] of lines.entries()) {
|
|
633
|
-
state.line = index +
|
|
634
|
-
scanHeadingLine(line, state, slugger, headings,
|
|
847
|
+
state.line = index + 1;
|
|
848
|
+
scanHeadingLine(line, state, slugger, headings, context);
|
|
635
849
|
}
|
|
636
850
|
|
|
637
851
|
const anchors = new Set<string>();
|
|
@@ -961,7 +1175,7 @@ const deriveTitle = (
|
|
|
961
1175
|
// content root's own index has no folder to borrow from.
|
|
962
1176
|
const folder = stem === "index" ? parts.at(-2) : undefined;
|
|
963
1177
|
if (folder !== undefined) {
|
|
964
|
-
return titleCase(stripNumericPrefix(
|
|
1178
|
+
return titleCase(stripNumericPrefix(orderedGroupLabel(folder) ?? folder));
|
|
965
1179
|
}
|
|
966
1180
|
return titleCase(stem);
|
|
967
1181
|
};
|
|
@@ -1180,6 +1394,25 @@ const validateCustomKeys = (
|
|
|
1180
1394
|
};
|
|
1181
1395
|
};
|
|
1182
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
|
+
|
|
1183
1416
|
/**
|
|
1184
1417
|
* Parse an entry's frontmatter: built-in keys through the strict page schema,
|
|
1185
1418
|
* custom keys (`frontmatter.extend` plus the page type's
|
|
@@ -1218,8 +1451,12 @@ const parseEntryMeta = (
|
|
|
1218
1451
|
|
|
1219
1452
|
const result = pageMetaSchema.safeParse(known);
|
|
1220
1453
|
const customResult = extend ? validateCustomKeys(entry.data, extend) : null;
|
|
1454
|
+
const issues = [
|
|
1455
|
+
...(result.success ? slugIssues(result.data.slug) : []),
|
|
1456
|
+
...(customResult?.issues ?? []),
|
|
1457
|
+
];
|
|
1221
1458
|
|
|
1222
|
-
if (result.success &&
|
|
1459
|
+
if (result.success && issues.length === 0) {
|
|
1223
1460
|
return { custom: customResult?.custom, meta: result.data };
|
|
1224
1461
|
}
|
|
1225
1462
|
|
|
@@ -1239,9 +1476,7 @@ const parseEntryMeta = (
|
|
|
1239
1476
|
return {
|
|
1240
1477
|
diagnostics: [
|
|
1241
1478
|
...(result.success ? [] : diagnosticsFromZod(result.error, location)),
|
|
1242
|
-
...(
|
|
1243
|
-
? diagnosticsFromIssues(customResult.issues, location)
|
|
1244
|
-
: []),
|
|
1479
|
+
...diagnosticsFromIssues(issues, location),
|
|
1245
1480
|
],
|
|
1246
1481
|
};
|
|
1247
1482
|
};
|