blume 2.1.3 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +134 -0
- package/README.md +4 -4
- package/dist/cli/{chunk-y4msrwm9.js → chunk-080mfch6.js} +15 -15
- package/dist/cli/{chunk-gfc5fqvf.js → chunk-1ch3ct3r.js} +18 -18
- package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-1ch3ct3r.js.map} +1 -1
- package/dist/cli/{chunk-32khwcb6.js → chunk-2ssh60qh.js} +42 -69
- package/dist/cli/{chunk-32khwcb6.js.map → chunk-2ssh60qh.js.map} +9 -10
- package/dist/cli/{chunk-9qzgg6mj.js → chunk-34wap3ed.js} +3 -3
- package/dist/cli/{chunk-k3gr1nm9.js → chunk-40vk8kgf.js} +13 -16
- package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-40vk8kgf.js.map} +3 -3
- package/dist/cli/chunk-487b95qy.js +329 -0
- package/dist/cli/chunk-487b95qy.js.map +13 -0
- package/dist/cli/chunk-4zxh25hc.js +157 -0
- package/dist/cli/chunk-4zxh25hc.js.map +10 -0
- package/dist/cli/chunk-69rjf786.js +146 -0
- package/dist/cli/chunk-69rjf786.js.map +11 -0
- package/dist/cli/{chunk-dqhg54mr.js → chunk-9h8mh2tk.js} +3 -3
- package/dist/cli/{chunk-dqhg54mr.js.map → chunk-9h8mh2tk.js.map} +2 -2
- package/dist/cli/{chunk-bqxaqn5p.js → chunk-9jqmq5hv.js} +8 -8
- package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-9jqmq5hv.js.map} +1 -1
- package/dist/cli/{chunk-dv2z0rzf.js → chunk-aafhwx74.js} +38 -13
- package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-aafhwx74.js.map} +5 -4
- package/dist/cli/{chunk-desvm72z.js → chunk-b680sv30.js} +22 -26
- package/dist/cli/chunk-b680sv30.js.map +13 -0
- package/dist/cli/{chunk-0w6hs479.js → chunk-beb0z983.js} +30 -18
- package/dist/cli/chunk-beb0z983.js.map +10 -0
- package/dist/cli/{chunk-1f882900.js → chunk-c5bjxq8v.js} +2 -2
- package/dist/cli/chunk-e1n41w9m.js +155 -0
- package/dist/cli/chunk-e1n41w9m.js.map +11 -0
- package/dist/cli/{chunk-sc93apfs.js → chunk-etckvbrh.js} +17 -17
- package/dist/cli/{chunk-sc93apfs.js.map → chunk-etckvbrh.js.map} +2 -2
- package/dist/cli/{chunk-bxwhtvw8.js → chunk-f260jdrn.js} +5 -5
- package/dist/cli/{chunk-74wfhv79.js → chunk-fnxgtjeq.js} +1 -1
- package/dist/cli/{chunk-ms9z66nz.js → chunk-gwy55cx3.js} +2 -2
- package/dist/cli/{chunk-ms9z66nz.js.map → chunk-gwy55cx3.js.map} +2 -2
- package/dist/cli/{chunk-dhwtqnnd.js → chunk-gxhcf2hs.js} +2 -2
- package/dist/cli/{chunk-tfycddbt.js → chunk-h0qwdbem.js} +2 -2
- package/dist/cli/{chunk-nbyvgnnm.js → chunk-hktq4j5r.js} +109 -49
- package/dist/cli/chunk-hktq4j5r.js.map +36 -0
- package/dist/cli/{chunk-twy7psdz.js → chunk-hn3g2b89.js} +14 -10
- package/dist/cli/chunk-hn3g2b89.js.map +10 -0
- package/dist/cli/{chunk-j1x92mx9.js → chunk-j4nfgn2j.js} +1 -1
- package/dist/cli/{chunk-dpy8qdjf.js → chunk-k45sq16q.js} +74 -183
- package/dist/cli/chunk-k45sq16q.js.map +18 -0
- package/dist/cli/{chunk-5cmts6dv.js → chunk-kmswjxj9.js} +7 -7
- package/dist/cli/{chunk-5cmts6dv.js.map → chunk-kmswjxj9.js.map} +1 -1
- package/dist/cli/{chunk-36bxs9c7.js → chunk-nqna8dzp.js} +27 -23
- package/dist/cli/chunk-nqna8dzp.js.map +10 -0
- package/dist/cli/{chunk-a57yh0h9.js → chunk-p834rkcn.js} +7 -7
- package/dist/cli/{chunk-a57yh0h9.js.map → chunk-p834rkcn.js.map} +1 -1
- package/dist/cli/{chunk-60hz265r.js → chunk-ppa6mg4d.js} +24 -11
- package/dist/cli/chunk-ppa6mg4d.js.map +10 -0
- package/dist/cli/{chunk-s0bdnc7v.js → chunk-q1wwrxzw.js} +5 -4
- package/dist/cli/chunk-q1wwrxzw.js.map +10 -0
- package/dist/cli/{chunk-ave1wcbr.js → chunk-qcyxkj44.js} +1 -1
- package/dist/cli/{chunk-124fvmzq.js → chunk-r5nm9w0c.js} +11183 -8806
- package/dist/cli/{chunk-124fvmzq.js.map → chunk-r5nm9w0c.js.map} +82 -66
- package/dist/cli/{chunk-ckrcm08h.js → chunk-rtvgmg2j.js} +48 -62
- package/dist/cli/{chunk-ckrcm08h.js.map → chunk-rtvgmg2j.js.map} +4 -5
- package/dist/cli/{chunk-297ajdcv.js → chunk-t2h8pwy1.js} +6 -6
- package/dist/cli/{chunk-297ajdcv.js.map → chunk-t2h8pwy1.js.map} +3 -3
- package/dist/cli/{chunk-gxtg6ed2.js → chunk-x60e15st.js} +21 -21
- package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-x60e15st.js.map} +3 -3
- package/dist/cli/{chunk-58bt3hag.js → chunk-x7bh6d9j.js} +4 -4
- package/dist/cli/{chunk-hvcxng5f.js → chunk-ydqzkhjn.js} +6 -6
- package/dist/cli/{chunk-kdpzw38a.js → chunk-yg8ckc3g.js} +95 -7
- package/dist/cli/chunk-yg8ckc3g.js.map +11 -0
- package/dist/cli/{chunk-0pmb4tr4.js → chunk-zxd5jrgx.js} +1205 -222
- package/dist/cli/chunk-zxd5jrgx.js.map +44 -0
- package/dist/cli/index.js +78 -42
- package/dist/cli/index.js.map +7 -6
- package/dist/types/ai/changelog-components.d.ts +18 -0
- package/dist/types/ai/changelog-markdown.d.ts +12 -0
- package/dist/types/ai/openapi-components.d.ts +7 -7
- package/dist/types/ai/operation-details.d.ts +39 -0
- package/dist/types/ai/relative-links.d.ts +3 -1
- package/dist/types/ai/serializers.d.ts +6 -4
- package/dist/types/analytics/head.d.ts +4 -2
- package/dist/types/analytics/script.d.ts +8 -2
- package/dist/types/components/content/youtube.d.ts +20 -0
- package/dist/types/components/layout/nav-utils.d.ts +8 -0
- package/dist/types/components/openapi/example-panels.d.ts +33 -0
- package/dist/types/components/openapi/helpers.d.ts +53 -11
- package/dist/types/components/openapi/snippets.d.ts +7 -1
- package/dist/types/core/alert-diagnostics.d.ts +10 -0
- package/dist/types/core/builtin-tags.d.ts +16 -0
- package/dist/types/core/changelog-list.d.ts +48 -0
- package/dist/types/core/closest-match.d.ts +6 -0
- package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
- package/dist/types/core/code-fences.d.ts +8 -1
- package/dist/types/core/config-input.d.ts +38 -6
- package/dist/types/core/content-assets.d.ts +16 -18
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/diagnostics.d.ts +14 -0
- package/dist/types/core/directive-diagnostics.d.ts +22 -8
- package/dist/types/core/former-routes.d.ts +14 -0
- package/dist/types/core/github.d.ts +5 -4
- package/dist/types/core/heading-markers.d.ts +65 -2
- package/dist/types/core/hidden-pages.d.ts +3 -0
- package/dist/types/core/includes.d.ts +23 -0
- package/dist/types/core/last-modified.d.ts +18 -6
- package/dist/types/core/links.d.ts +42 -2
- package/dist/types/core/load-module.d.ts +19 -4
- package/dist/types/core/manifest.d.ts +14 -1
- package/dist/types/core/markdown-mirrors.d.ts +22 -0
- package/dist/types/core/mcp-clients.d.ts +47 -0
- package/dist/types/core/mdx-elements.d.ts +32 -0
- package/dist/types/core/mdx-syntax.d.ts +100 -0
- package/dist/types/core/meta-diagnostics.d.ts +7 -0
- package/dist/types/core/meta.d.ts +32 -5
- package/dist/types/core/nav-diagnostics.d.ts +16 -2
- package/dist/types/core/navigation.d.ts +13 -0
- package/dist/types/core/ordering-prefix.d.ts +6 -0
- package/dist/types/core/pinned-ids.d.ts +23 -0
- package/dist/types/core/project-graph.d.ts +9 -1
- package/dist/types/core/redirect-patterns.d.ts +18 -0
- package/dist/types/core/relative-files.d.ts +31 -0
- package/dist/types/core/schema.d.ts +322 -11
- package/dist/types/core/sources/normalize.d.ts +101 -9
- package/dist/types/core/sources/types.d.ts +16 -0
- package/dist/types/core/static-files.d.ts +10 -0
- package/dist/types/core/static-host.d.ts +42 -0
- package/dist/types/core/svg-images.d.ts +26 -0
- package/dist/types/core/syntax-diagnostics.d.ts +14 -0
- package/dist/types/core/types.d.ts +42 -1
- package/dist/types/deploy/generated-files.d.ts +41 -0
- package/dist/types/deploy/redirects.d.ts +21 -7
- package/dist/types/markdown/code-title.d.ts +9 -5
- package/dist/types/markdown/directives.d.ts +8 -0
- package/dist/types/markdown/fence-language.d.ts +51 -0
- package/dist/types/markdown/fence-meta.d.ts +5 -0
- package/dist/types/markdown/github-alerts.d.ts +35 -0
- package/dist/types/openapi/asyncapi.d.ts +6 -5
- package/dist/types/openapi/checks.d.ts +10 -5
- package/dist/types/openapi/model.d.ts +55 -8
- package/dist/types/openapi/parse.d.ts +4 -1
- package/dist/types/ratelimit/index.d.ts +2 -0
- package/dist/types/ratelimit/memory.d.ts +1 -1
- package/dist/types/ratelimit/schema.d.ts +40 -0
- package/dist/types/ratelimit/unkey.d.ts +48 -0
- package/dist/types/sources/filesystem.d.ts +14 -4
- package/dist/types/sources/mdx-remote.d.ts +6 -3
- package/dist/types/sources/registry.d.ts +3 -3
- package/dist/types/theme/color.d.ts +33 -0
- package/dist/types/theme/palette.d.ts +122 -0
- package/docs/01-quickstart.mdx +1 -1
- package/docs/02-deployment.mdx +24 -8
- package/docs/04-migrating.mdx +16 -4
- package/docs/advanced/changelog.mdx +27 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +1 -1
- package/docs/cli/doctor.mdx +2 -2
- package/docs/cli/evals.mdx +1 -1
- package/docs/cli/index.mdx +4 -2
- package/docs/cli/validate.mdx +10 -7
- package/docs/configuration/analytics.mdx +2 -0
- package/docs/configuration/customization.mdx +3 -1
- package/docs/configuration/index.mdx +7 -6
- package/docs/configuration/rate-limiting.mdx +20 -1
- package/docs/configuration/theming.mdx +5 -1
- package/docs/content/components.mdx +26 -6
- package/docs/content/frontmatter.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/includes.mdx +6 -2
- package/docs/content/index.mdx +11 -1
- package/docs/content/islands.mdx +2 -0
- package/docs/content/meta.mdx +9 -3
- package/docs/content/navigation.mdx +14 -6
- package/docs/content/sources/obsidian.mdx +1 -1
- package/docs/content/sources/remote-mdx.mdx +3 -1
- package/docs/content/syntax.mdx +76 -8
- package/docs/content/variables.mdx +3 -1
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/json-api.mdx +1 -1
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/discoverability/markdown.mdx +2 -2
- package/docs/discoverability/mcp.mdx +40 -8
- package/docs/discoverability/open-graph.mdx +1 -1
- package/docs/discoverability/sitemap-and-robots.mdx +1 -1
- package/docs/references/graphql.mdx +2 -0
- package/docs/references/openapi.mdx +45 -7
- package/package.json +4 -4
- package/skills/blume/SKILL.md +2 -2
- package/skills/blume-migrate/SKILL.md +51 -29
- package/skills/blume-migrate/references/docsify.md +262 -0
- package/skills/blume-migrate/references/docus.md +298 -0
- package/skills/blume-migrate/references/docusaurus.md +5 -5
- package/skills/blume-migrate/references/fern.md +327 -0
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/gitbook.md +267 -0
- package/skills/blume-migrate/references/github-wiki.md +259 -0
- package/skills/blume-migrate/references/jekyll.md +253 -0
- package/skills/blume-migrate/references/mdbook.md +256 -0
- package/skills/blume-migrate/references/mintlify.md +5 -5
- package/skills/blume-migrate/references/mkdocs.md +236 -0
- package/skills/blume-migrate/references/monorepo.md +20 -8
- package/skills/blume-migrate/references/nextra.md +5 -5
- package/skills/blume-migrate/references/readme.md +257 -0
- package/skills/blume-migrate/references/redocly.md +198 -0
- package/skills/blume-migrate/references/starlight.md +3 -3
- package/skills/blume-migrate/references/vitepress.md +204 -0
- package/skills/blume-migrate/references/vuepress.md +271 -0
- package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
- package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
- package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
- package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
- package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
- package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
- package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
- package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
- package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
- package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
- package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
- package/src/ai/changelog-components.ts +24 -0
- package/src/ai/changelog-markdown.ts +45 -41
- package/src/ai/component-markdown.ts +22 -9
- package/src/ai/llms.ts +36 -5
- package/src/ai/markdown.ts +2 -2
- package/src/ai/openapi-components.ts +25 -9
- package/src/ai/operation-details.ts +208 -0
- package/src/ai/relative-links.ts +48 -8
- package/src/ai/serializers.ts +7 -6
- package/src/ai/site-skill.ts +6 -2
- package/src/analytics/head.ts +11 -5
- package/src/analytics/script.ts +13 -3
- package/src/astro/generate.ts +47 -11
- package/src/astro/index.ts +1 -0
- package/src/astro/integration.ts +27 -1
- package/src/astro/mdx-source-errors.ts +46 -0
- package/src/astro/templates.ts +93 -189
- package/src/audit/checks/redirects.ts +5 -1
- package/src/audit/redirects.ts +36 -19
- package/src/audit/report.ts +22 -9
- package/src/audit/run.ts +12 -28
- package/src/audit/terms.ts +23 -0
- package/src/audit/url.ts +2 -2
- package/src/cli/build-failure.ts +256 -7
- package/src/cli/command-meta.ts +1 -1
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.ts +12 -3
- package/src/cli/commands/check.ts +13 -2
- package/src/cli/commands/dev.ts +1 -1
- package/src/cli/commands/doctor.ts +4 -2
- package/src/cli/commands/init.ts +3 -3
- package/src/cli/commands/preview.ts +17 -1
- package/src/cli/commands/validate.ts +22 -10
- package/src/cli/prepare.ts +1 -1
- package/src/cli/preview-pages.ts +161 -0
- package/src/cli/required-secrets.ts +0 -1
- package/src/cli/unknown-flags.ts +1 -42
- package/src/components/code-copy.ts +51 -0
- package/src/components/code-language.ts +75 -0
- package/src/components/content/AccordionItem.astro +16 -1
- package/src/components/content/Callout.astro +11 -1
- package/src/components/content/Changelog.astro +189 -0
- package/src/components/content/CodeBlock.astro +8 -11
- package/src/components/content/Tabs.astro +16 -2
- package/src/components/content/YouTube.astro +6 -4
- package/src/components/content/code-block.ts +33 -0
- package/src/components/content/content-strings.ts +5 -1
- package/src/components/content/inline-markdown.ts +35 -8
- package/src/components/content/youtube.ts +49 -7
- package/src/components/layout/Analytics.astro +5 -1
- package/src/components/layout/NavTree.astro +8 -3
- package/src/components/layout/PageActions.astro +81 -23
- package/src/components/layout/RootLayout.astro +35 -32
- package/src/components/layout/SiteFooter.astro +31 -8
- package/src/components/layout/nav-utils.ts +14 -0
- package/src/components/layout/search/pagefind.ts +2 -1
- package/src/components/openapi/Operation.astro +4 -0
- package/src/components/openapi/PanelTabs.astro +9 -3
- package/src/components/openapi/RequestBody.astro +42 -8
- package/src/components/openapi/RequestPanel.astro +34 -53
- package/src/components/openapi/example-panels.ts +110 -0
- package/src/components/openapi/helpers.ts +195 -20
- package/src/components/openapi/operation-model.ts +63 -26
- package/src/components/openapi/request.ts +13 -0
- package/src/components/openapi/snippets.ts +14 -2
- package/src/core/alert-diagnostics.ts +89 -0
- package/src/core/builtin-tags.ts +76 -0
- package/src/core/changelog-list.ts +87 -0
- package/src/core/closest-match.ts +41 -0
- package/src/core/code-fence-diagnostics.ts +267 -0
- package/src/core/code-fences.ts +43 -7
- package/src/core/component-diagnostics.ts +65 -12
- package/src/core/config-input.ts +39 -5
- package/src/core/config.ts +46 -1
- package/src/core/content-assets.ts +64 -144
- package/src/core/data.ts +10 -3
- package/src/core/diagnostics.ts +36 -3
- package/src/core/directive-diagnostics.ts +265 -47
- package/src/core/former-routes.ts +46 -0
- package/src/core/git-ignored.ts +53 -0
- package/src/core/github.ts +5 -4
- package/src/core/heading-markers.ts +156 -2
- package/src/core/hidden-pages.ts +64 -0
- package/src/core/includes.ts +181 -41
- package/src/core/last-modified.ts +47 -10
- package/src/core/links.ts +438 -83
- package/src/core/load-module.ts +98 -9
- package/src/core/manifest.ts +12 -9
- package/src/core/markdown-mirrors.ts +52 -0
- package/src/core/mcp-clients.ts +87 -0
- package/src/core/mdx-elements.ts +153 -0
- package/src/core/mdx-syntax.ts +377 -0
- package/src/core/meta-diagnostics.ts +134 -0
- package/src/core/meta.ts +154 -55
- package/src/core/nav-diagnostics.ts +41 -4
- package/src/core/navigation.ts +70 -10
- package/src/core/ordering-prefix.ts +22 -2
- package/src/core/pinned-ids.ts +141 -0
- package/src/core/project-graph.ts +221 -29
- package/src/core/redirect-patterns.ts +41 -0
- package/src/core/relative-files.ts +145 -0
- package/src/core/schema.ts +110 -6
- package/src/core/sources/mdx-remote.ts +75 -14
- package/src/core/sources/normalize.ts +885 -179
- package/src/core/sources/notion.ts +5 -4
- package/src/core/sources/obsidian.ts +2 -5
- package/src/core/sources/types.ts +17 -0
- package/src/core/static-files.ts +40 -0
- package/src/core/static-host.ts +125 -0
- package/src/core/svg-images.ts +98 -0
- package/src/core/syntax-diagnostics.ts +491 -0
- package/src/core/types.ts +43 -1
- package/src/deploy/artifacts.ts +2 -28
- package/src/deploy/generated-files.ts +128 -0
- package/src/deploy/platforms/vercel.ts +25 -1
- package/src/deploy/redirects.ts +43 -16
- package/src/deploy/sitemap.ts +2 -1
- package/src/markdown/base-links.ts +87 -6
- package/src/markdown/code-title.ts +29 -13
- package/src/markdown/content-assets.ts +210 -0
- package/src/markdown/directives.ts +69 -5
- package/src/markdown/fence-language.ts +106 -0
- package/src/markdown/fence-meta.ts +42 -0
- package/src/markdown/github-alerts.ts +172 -0
- package/src/markdown/heading-anchors.ts +265 -56
- package/src/markdown/include.ts +30 -3
- package/src/markdown/index.ts +145 -40
- package/src/markdown/prompt-text.ts +95 -0
- package/src/markdown/relative-links.ts +37 -2
- package/src/markdown/route-snapshot.ts +22 -12
- package/src/markdown/svg-fallback.ts +51 -0
- package/src/markdown/title-heading.ts +134 -0
- package/src/migrate/migrate.ts +127 -2
- package/src/og/card.ts +14 -4
- package/src/openapi/asyncapi.ts +14 -6
- package/src/openapi/checks.ts +160 -15
- package/src/openapi/model.ts +172 -33
- package/src/openapi/parse.ts +13 -4
- package/src/openapi/render-mdx.ts +55 -9
- package/src/openapi/source.ts +87 -6
- package/src/ratelimit/index.ts +2 -0
- package/src/ratelimit/memory.ts +1 -1
- package/src/ratelimit/runtime.ts +81 -0
- package/src/ratelimit/schema.ts +2 -0
- package/src/ratelimit/unkey.ts +61 -0
- package/src/registry/eject.ts +0 -1
- package/src/registry/registry.ts +1 -1
- package/src/search/documents.ts +10 -2
- package/src/sources/filesystem.ts +28 -3
- package/src/sources/mdx-remote.ts +7 -15
- package/src/theme/color.ts +14 -0
- package/src/theme/entry.ts +22 -1
- package/src/theme/palette.ts +74 -1
- package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
- package/dist/cli/chunk-0w6hs479.js.map +0 -10
- package/dist/cli/chunk-150jk7km.js +0 -52
- package/dist/cli/chunk-150jk7km.js.map +0 -10
- package/dist/cli/chunk-36bxs9c7.js.map +0 -10
- package/dist/cli/chunk-53jqw8z0.js +0 -89
- package/dist/cli/chunk-53jqw8z0.js.map +0 -10
- package/dist/cli/chunk-60hz265r.js.map +0 -10
- package/dist/cli/chunk-desvm72z.js.map +0 -14
- package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
- package/dist/cli/chunk-es8h0v3j.js +0 -441
- package/dist/cli/chunk-es8h0v3j.js.map +0 -11
- package/dist/cli/chunk-kdpzw38a.js.map +0 -11
- package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
- package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
- package/dist/cli/chunk-twy7psdz.js.map +0 -10
- package/dist/cli/chunk-ydp3065p.js +0 -64
- package/dist/cli/chunk-ydp3065p.js.map +0 -10
- /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-080mfch6.js.map} +0 -0
- /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-34wap3ed.js.map} +0 -0
- /package/dist/cli/{chunk-1f882900.js.map → chunk-c5bjxq8v.js.map} +0 -0
- /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-f260jdrn.js.map} +0 -0
- /package/dist/cli/{chunk-74wfhv79.js.map → chunk-fnxgtjeq.js.map} +0 -0
- /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-gxhcf2hs.js.map} +0 -0
- /package/dist/cli/{chunk-tfycddbt.js.map → chunk-h0qwdbem.js.map} +0 -0
- /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-j4nfgn2j.js.map} +0 -0
- /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-qcyxkj44.js.map} +0 -0
- /package/dist/cli/{chunk-58bt3hag.js.map → chunk-x7bh6d9j.js.map} +0 -0
- /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-ydqzkhjn.js.map} +0 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// include-excerpts.mjs — generate the code excerpts a migrated site includes.
|
|
3
|
+
//
|
|
4
|
+
// Blume's `<include>` splices a whole file from inside the content root. The
|
|
5
|
+
// tools sites migrate from could include part of a file from anywhere in the
|
|
6
|
+
// repo: mdBook by `ANCHOR:` markers or line range, VitePress by `#region`
|
|
7
|
+
// markers, MkDocs snippets by line range. Pasting each excerpt into its page
|
|
8
|
+
// would freeze code the project still compiles and tests, so this script
|
|
9
|
+
// regenerates the excerpts before every build instead. Copy it into the
|
|
10
|
+
// project, next to its manifest, and run it ahead of `blume dev`, `build`,
|
|
11
|
+
// and `validate`:
|
|
12
|
+
//
|
|
13
|
+
// "build": "node include-excerpts.mjs && blume build"
|
|
14
|
+
//
|
|
15
|
+
// The manifest (`excerpts.json` by default) names each excerpt file, under
|
|
16
|
+
// `out`, and what goes in it. Every path is relative to the manifest's folder.
|
|
17
|
+
// `out` must be a `_`-prefixed folder inside the content root: Blume never
|
|
18
|
+
// publishes it, and `<include>/_excerpts/x.rs</include>` reads from it.
|
|
19
|
+
//
|
|
20
|
+
// {
|
|
21
|
+
// "out": "docs/_excerpts",
|
|
22
|
+
// "excerpts": {
|
|
23
|
+
// "abi.rs": "../examples/counter/src/lib.rs:contract_abi",
|
|
24
|
+
// "state.ts": { "file": "../src/state.ts", "region": "state", "dedent": true },
|
|
25
|
+
// "main.rs": { "file": "../src/main.rs", "lines": "12-30" },
|
|
26
|
+
// "transfer.rs": [
|
|
27
|
+
// { "text": "impl Contract for Token {" },
|
|
28
|
+
// { "file": "../src/contract.rs", "anchor": "transfer" },
|
|
29
|
+
// { "text": "}" }
|
|
30
|
+
// ]
|
|
31
|
+
// }
|
|
32
|
+
// }
|
|
33
|
+
//
|
|
34
|
+
// An excerpt is one part or an array of parts, joined line by line:
|
|
35
|
+
//
|
|
36
|
+
// "path:selector" mdBook's {{#include}} argument, selected the way
|
|
37
|
+
// mdBook does: "path" (the whole file), "path:name"
|
|
38
|
+
// (an anchor), "path:N" (line N), "path:N:M",
|
|
39
|
+
// "path:N:" (to the end), "path::M" (from the start).
|
|
40
|
+
// Where mdBook showed an empty or cut-short block (a
|
|
41
|
+
// missing anchor, a range past the end), this reports
|
|
42
|
+
// a problem instead.
|
|
43
|
+
// { "file" } the whole file.
|
|
44
|
+
// { "file", "anchor" } the lines between `ANCHOR: name` and
|
|
45
|
+
// `ANCHOR_END: name`, minus every ANCHOR line, as
|
|
46
|
+
// mdBook does. An anchor that never ends runs to the
|
|
47
|
+
// end of the file.
|
|
48
|
+
// { "file", "region" } the lines between `#region name` and its
|
|
49
|
+
// `#endregion`, in any comment style VS Code folds
|
|
50
|
+
// (`// #region`, `# region`, `<!-- #region -->`,
|
|
51
|
+
// `/* #region */`, `#pragma region`), minus every
|
|
52
|
+
// region marker. An `#endregion` without a name closes
|
|
53
|
+
// the innermost open region. VitePress pairs nested
|
|
54
|
+
// markers its own way (1.x ignores an unnamed
|
|
55
|
+
// `#endregion`, 2.x closes on the first one), so
|
|
56
|
+
// compare a nested region with the old build.
|
|
57
|
+
// { "file", "lines" } 1-based and inclusive: "12-30", "12-" (to the end),
|
|
58
|
+
// "-30" (from the start), "12" (that line only).
|
|
59
|
+
// { "text" } literal lines, for code around a selection.
|
|
60
|
+
//
|
|
61
|
+
// A file part may add "dedent": true to remove the indentation its lines share
|
|
62
|
+
// (VitePress dedents a `<<<` region; mdBook doesn't).
|
|
63
|
+
//
|
|
64
|
+
// Before writing anything, every entry is checked, and every problem is
|
|
65
|
+
// reported in one pass: a missing file, anchor, or region, a line range past
|
|
66
|
+
// the end of its file, a selection with no lines. Then it exits 1. mdBook
|
|
67
|
+
// showed a missing anchor as an empty block, without a warning, so a migrated
|
|
68
|
+
// book usually has several.
|
|
69
|
+
//
|
|
70
|
+
// A line range keeps selecting the same line numbers when the file changes
|
|
71
|
+
// around them, so it goes stale without an error. Prefer an anchor or region
|
|
72
|
+
// marker in the source wherever you can add one.
|
|
73
|
+
//
|
|
74
|
+
// Usage:
|
|
75
|
+
//
|
|
76
|
+
// node include-excerpts.mjs # excerpts.json: check, then write
|
|
77
|
+
// node include-excerpts.mjs path/to/excerpts.json
|
|
78
|
+
// node include-excerpts.mjs --check # report only: exit 1 on a problem
|
|
79
|
+
// # or an excerpt that's out of date
|
|
80
|
+
//
|
|
81
|
+
// It rewrites only the files whose content changed, and deletes the files it
|
|
82
|
+
// wrote before that the manifest no longer names. It lists what it wrote in
|
|
83
|
+
// `out/.include-excerpts.json` and never deletes a file it didn't write, nor
|
|
84
|
+
// writes through a symbolic link.
|
|
85
|
+
//
|
|
86
|
+
// Zero dependencies: runs on a bare Node.js 22 with nothing installed.
|
|
87
|
+
|
|
88
|
+
import {
|
|
89
|
+
existsSync,
|
|
90
|
+
lstatSync,
|
|
91
|
+
mkdirSync,
|
|
92
|
+
readdirSync,
|
|
93
|
+
readFileSync,
|
|
94
|
+
realpathSync,
|
|
95
|
+
rmdirSync,
|
|
96
|
+
rmSync,
|
|
97
|
+
statSync,
|
|
98
|
+
writeFileSync,
|
|
99
|
+
} from "node:fs";
|
|
100
|
+
import path from "node:path";
|
|
101
|
+
|
|
102
|
+
const USAGE = [
|
|
103
|
+
"Usage: node include-excerpts.mjs [manifest] [--check]",
|
|
104
|
+
"",
|
|
105
|
+
" manifest the excerpts file (default: excerpts.json)",
|
|
106
|
+
" --check report problems and out-of-date excerpts; write nothing",
|
|
107
|
+
].join("\n");
|
|
108
|
+
|
|
109
|
+
// --- Manifest values -----------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
// The manifest comes from JSON.parse: strings, numbers, booleans, null,
|
|
112
|
+
// arrays, and plain objects.
|
|
113
|
+
const isText = (value) => value === String(value);
|
|
114
|
+
const isRecord = (value) =>
|
|
115
|
+
value !== null &&
|
|
116
|
+
value !== undefined &&
|
|
117
|
+
!Array.isArray(value) &&
|
|
118
|
+
Object.getPrototypeOf(value) === Object.prototype;
|
|
119
|
+
|
|
120
|
+
const PART_KEYS = new Set(["anchor", "dedent", "file", "lines", "region"]);
|
|
121
|
+
|
|
122
|
+
// --- Selection -----------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
// mdBook's own patterns (crates/mdbook-driver, links/take_lines.rs). Rust's
|
|
125
|
+
// `\w` is Unicode-aware, so an anchor can be `café`.
|
|
126
|
+
const ANCHOR_NAME = String.raw`[\p{Alphabetic}\p{M}\p{Nd}\p{Pc}\p{Join_Control}-]+`;
|
|
127
|
+
const ANCHOR_START = new RegExp(
|
|
128
|
+
String.raw`ANCHOR:\s*(?<name>${ANCHOR_NAME})`,
|
|
129
|
+
"u"
|
|
130
|
+
);
|
|
131
|
+
const ANCHOR_END = new RegExp(
|
|
132
|
+
String.raw`ANCHOR_END:\s*(?<name>${ANCHOR_NAME})`,
|
|
133
|
+
"u"
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// VS Code's folding markers, one alternative per comment syntax: line comments
|
|
137
|
+
// (`//`, with the `#` optional as in JS and TS; `--`, `::`, and `REM` with it
|
|
138
|
+
// required), hash comments (`#region`, `# region`, `# #region`,
|
|
139
|
+
// `#pragma region`, Visual Basic's `#Region` and `#End Region`), HTML and
|
|
140
|
+
// Markdown, CSS, and F#. A marker is the whole line, so a comment that only
|
|
141
|
+
// mentions a region (`// region: us-east`) isn't one.
|
|
142
|
+
const REGION_MARKER =
|
|
143
|
+
/^\s*(?:\/\/\s*#?|(?:--|::|@?rem\s)\s*#|#\s*(?:#\s*|pragma\s+)?|<!--\s*#?|\/\*\s*#|\(\*\s*#)(?<end>end\s?)?region(?:\s+(?:"(?<quoted>[^"]*)"|(?<name>[\w.-]+)))?\s*(?:-->|\*\/|\*\))?\s*$/iu;
|
|
144
|
+
|
|
145
|
+
const regionMarker = (line) => {
|
|
146
|
+
const match = REGION_MARKER.exec(line);
|
|
147
|
+
if (!match) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const { end, name, quoted } = match.groups;
|
|
151
|
+
return { end: Boolean(end), name: quoted ?? name ?? "" };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** The lines between `ANCHOR: name` and `ANCHOR_END: name`, as mdBook takes them. */
|
|
155
|
+
const anchorLines = (lines, name) => {
|
|
156
|
+
const kept = [];
|
|
157
|
+
let open = false;
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
if (!open) {
|
|
160
|
+
open = ANCHOR_START.exec(line)?.groups.name === name;
|
|
161
|
+
} else if (ANCHOR_END.exec(line)?.groups.name === name) {
|
|
162
|
+
return kept;
|
|
163
|
+
} else if (!(ANCHOR_START.test(line) || ANCHOR_END.test(line))) {
|
|
164
|
+
kept.push(line);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return open ? kept : undefined;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** The index of the line that closes the region opened at `start`, else the end. */
|
|
171
|
+
const regionEnd = (lines, start) => {
|
|
172
|
+
const open = [regionMarker(lines[start]).name];
|
|
173
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
174
|
+
const marker = regionMarker(lines[i]);
|
|
175
|
+
if (marker?.end) {
|
|
176
|
+
const index = marker.name
|
|
177
|
+
? open.lastIndexOf(marker.name)
|
|
178
|
+
: open.length - 1;
|
|
179
|
+
if (index === 0) {
|
|
180
|
+
return i;
|
|
181
|
+
}
|
|
182
|
+
if (index > 0) {
|
|
183
|
+
open.length = index;
|
|
184
|
+
}
|
|
185
|
+
} else if (marker) {
|
|
186
|
+
open.push(marker.name);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return lines.length;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** The lines of the first region called `name`, minus every region marker. */
|
|
193
|
+
const regionLines = (lines, name) => {
|
|
194
|
+
const start = lines.findIndex((line) => {
|
|
195
|
+
const marker = regionMarker(line);
|
|
196
|
+
return marker !== undefined && !marker.end && marker.name === name;
|
|
197
|
+
});
|
|
198
|
+
if (start === -1) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
return lines
|
|
202
|
+
.slice(start + 1, regionEnd(lines, start))
|
|
203
|
+
.filter((line) => regionMarker(line) === undefined);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const LINE_RANGE = /^(?<from>\d*)(?<dash>-)?(?<to>\d*)$/u;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `{ explicit, from, to }` (0-based, `to` exclusive) for "12-30", "12-",
|
|
210
|
+
* "-30", or "12"; `explicit` is false when the range runs to the end.
|
|
211
|
+
*/
|
|
212
|
+
const parseLines = (spec, length) => {
|
|
213
|
+
const match = LINE_RANGE.exec(String(spec).trim());
|
|
214
|
+
if (!match || (match.groups.from === "" && match.groups.to === "")) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const { dash, from, to } = match.groups;
|
|
218
|
+
const first = from === "" ? 1 : Number(from);
|
|
219
|
+
let last = Number(to);
|
|
220
|
+
if (!dash) {
|
|
221
|
+
last = first;
|
|
222
|
+
} else if (to === "") {
|
|
223
|
+
last = length;
|
|
224
|
+
}
|
|
225
|
+
return { explicit: !dash || to !== "", from: first - 1, to: last };
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** mdBook's `path:selector`: a part object, as mdBook would select it. */
|
|
229
|
+
const parseMdbookSpec = (spec) => {
|
|
230
|
+
const [file = "", rawFirst = "", second] = spec.split(":");
|
|
231
|
+
if (!/^\d*$/u.test(rawFirst)) {
|
|
232
|
+
return { anchor: rawFirst, file };
|
|
233
|
+
}
|
|
234
|
+
// mdBook reads line 0 as line 1.
|
|
235
|
+
const first = rawFirst === "" ? "" : String(Math.max(1, Number(rawFirst)));
|
|
236
|
+
if (second === undefined) {
|
|
237
|
+
return first === "" ? { file } : { file, lines: first };
|
|
238
|
+
}
|
|
239
|
+
return { file, lines: `${first}-${/^\d+$/u.test(second) ? second : ""}` };
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** Remove the indentation every non-blank line shares. */
|
|
243
|
+
const dedent = (lines) => {
|
|
244
|
+
const widths = lines
|
|
245
|
+
.filter((line) => line.trim() !== "")
|
|
246
|
+
.map((line) => line.length - line.trimStart().length);
|
|
247
|
+
const width = widths.length > 0 ? Math.min(...widths) : 0;
|
|
248
|
+
return width === 0 ? lines : lines.map((line) => line.slice(width));
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// --- Checking ------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/** The anchor, region, or file a part selects lines from. */
|
|
254
|
+
const sourceOf = (part) => {
|
|
255
|
+
if (part.anchor !== undefined) {
|
|
256
|
+
return `anchor "${part.anchor}" in ${part.file}`;
|
|
257
|
+
}
|
|
258
|
+
if (part.region !== undefined) {
|
|
259
|
+
return `region "${part.region}" in ${part.file}`;
|
|
260
|
+
}
|
|
261
|
+
return part.file;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const describe = (part) =>
|
|
265
|
+
part.lines === undefined
|
|
266
|
+
? sourceOf(part)
|
|
267
|
+
: `lines ${part.lines} of ${sourceOf(part)}`;
|
|
268
|
+
|
|
269
|
+
/** The lines `part` names before any line range: `{ lines }`, or `{ problem }`. */
|
|
270
|
+
const markedLines = (part, source) => {
|
|
271
|
+
if (part.anchor !== undefined) {
|
|
272
|
+
const lines = anchorLines(source, part.anchor);
|
|
273
|
+
return lines
|
|
274
|
+
? { lines }
|
|
275
|
+
: { problem: `no "ANCHOR: ${part.anchor}" line in ${part.file}` };
|
|
276
|
+
}
|
|
277
|
+
if (part.region !== undefined) {
|
|
278
|
+
const lines = regionLines(source, part.region);
|
|
279
|
+
return lines
|
|
280
|
+
? { lines }
|
|
281
|
+
: { problem: `no "#region ${part.region}" marker in ${part.file}` };
|
|
282
|
+
}
|
|
283
|
+
return { lines: source };
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** `lines` cut to `part.lines`: `{ lines }`, or `{ problem }`. */
|
|
287
|
+
const rangeOf = (part, lines) => {
|
|
288
|
+
if (part.lines === undefined) {
|
|
289
|
+
return { lines };
|
|
290
|
+
}
|
|
291
|
+
const range = parseLines(part.lines, lines.length);
|
|
292
|
+
if (!range || range.from < 0 || (range.explicit && range.to <= range.from)) {
|
|
293
|
+
return { problem: `"${part.lines}" isn't a line range (${part.file})` };
|
|
294
|
+
}
|
|
295
|
+
if (range.to > lines.length || range.from >= lines.length) {
|
|
296
|
+
return {
|
|
297
|
+
problem: `lines ${part.lines} run past the end of ${sourceOf(part)} (${lines.length} lines)`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return { lines: lines.slice(range.from, range.to) };
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** Read one file part: `{ lines }`, or `{ problem }`. */
|
|
304
|
+
const selectFile = (part, base, readSource) => {
|
|
305
|
+
const source = readSource(path.resolve(base, part.file));
|
|
306
|
+
if (source === undefined) {
|
|
307
|
+
return { problem: `no file at ${part.file}` };
|
|
308
|
+
}
|
|
309
|
+
const marked = markedLines(part, source);
|
|
310
|
+
const selected = marked.problem ? marked : rangeOf(part, marked.lines);
|
|
311
|
+
if (selected.problem) {
|
|
312
|
+
return selected;
|
|
313
|
+
}
|
|
314
|
+
if (selected.lines.length === 0) {
|
|
315
|
+
return { problem: `${describe(part)} selects no lines` };
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
lines: part.dedent === true ? dedent(selected.lines) : selected.lines,
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/** One manifest part as a part object, or a problem. */
|
|
323
|
+
const normalizePart = (raw) => {
|
|
324
|
+
if (isText(raw)) {
|
|
325
|
+
return { part: parseMdbookSpec(raw) };
|
|
326
|
+
}
|
|
327
|
+
if (!isRecord(raw)) {
|
|
328
|
+
return { problem: "a part must be a string or an object" };
|
|
329
|
+
}
|
|
330
|
+
if (raw.text !== undefined) {
|
|
331
|
+
return Object.keys(raw).length === 1 && isText(raw.text)
|
|
332
|
+
? { part: raw }
|
|
333
|
+
: { problem: '{ "text" } takes one string and nothing else' };
|
|
334
|
+
}
|
|
335
|
+
const unknown = Object.keys(raw).filter((key) => !PART_KEYS.has(key));
|
|
336
|
+
if (unknown.length > 0) {
|
|
337
|
+
return {
|
|
338
|
+
problem: `unknown key ${unknown.map((key) => `"${key}"`).join(", ")}`,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (!isText(raw.file) || raw.file === "") {
|
|
342
|
+
return { problem: 'a part needs a "file" (or "text")' };
|
|
343
|
+
}
|
|
344
|
+
if (raw.anchor !== undefined && raw.region !== undefined) {
|
|
345
|
+
return { problem: 'a part takes "anchor" or "region", not both' };
|
|
346
|
+
}
|
|
347
|
+
const marker = raw.anchor ?? raw.region;
|
|
348
|
+
if (marker !== undefined && (!isText(marker) || marker === "")) {
|
|
349
|
+
return { problem: '"anchor" and "region" take a name' };
|
|
350
|
+
}
|
|
351
|
+
return { part: raw };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/** One part's lines: `{ lines }`, or `{ problem }`. */
|
|
355
|
+
const partLines = (raw, base, readSource) => {
|
|
356
|
+
const { part, problem } = normalizePart(raw);
|
|
357
|
+
if (problem) {
|
|
358
|
+
return { problem };
|
|
359
|
+
}
|
|
360
|
+
if (part.text !== undefined) {
|
|
361
|
+
return { lines: part.text.split(/\r?\n/u) };
|
|
362
|
+
}
|
|
363
|
+
return selectFile(part, base, readSource);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/** Whether `name` stays inside the output folder. */
|
|
367
|
+
const isInside = (name) =>
|
|
368
|
+
name !== "" &&
|
|
369
|
+
!path.isAbsolute(name) &&
|
|
370
|
+
!name.includes("\\") &&
|
|
371
|
+
name
|
|
372
|
+
.split("/")
|
|
373
|
+
.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
374
|
+
|
|
375
|
+
/** A cached reader: a file's lines without the final newline, else undefined. */
|
|
376
|
+
const sourceReader = () => {
|
|
377
|
+
const sources = new Map();
|
|
378
|
+
return (file) => {
|
|
379
|
+
if (!sources.has(file)) {
|
|
380
|
+
const lines =
|
|
381
|
+
existsSync(file) && statSync(file).isFile()
|
|
382
|
+
? readFileSync(file, "utf-8").split(/\r?\n/u)
|
|
383
|
+
: undefined;
|
|
384
|
+
if (lines?.at(-1) === "") {
|
|
385
|
+
lines.pop();
|
|
386
|
+
}
|
|
387
|
+
sources.set(file, lines);
|
|
388
|
+
}
|
|
389
|
+
return sources.get(file);
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
/** Every excerpt's text, and every problem the manifest has. */
|
|
394
|
+
const buildExcerpts = (entries, base) => {
|
|
395
|
+
const readSource = sourceReader();
|
|
396
|
+
const excerpts = new Map();
|
|
397
|
+
const problems = [];
|
|
398
|
+
const names = Object.keys(entries);
|
|
399
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
400
|
+
if (!isInside(name)) {
|
|
401
|
+
problems.push(`"${name}": the name must be a relative path inside out`);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (names.some((other) => other.startsWith(`${name}/`))) {
|
|
405
|
+
problems.push(`"${name}": another excerpt uses it as a folder`);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
409
|
+
problems.push(`"${name}": an empty list selects no lines`);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const lines = [];
|
|
413
|
+
for (const raw of Array.isArray(value) ? value : [value]) {
|
|
414
|
+
const result = partLines(raw, base, readSource);
|
|
415
|
+
if (result.problem) {
|
|
416
|
+
problems.push(`"${name}": ${result.problem}`);
|
|
417
|
+
} else {
|
|
418
|
+
lines.push(...result.lines);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
excerpts.set(name, `${lines.join("\n")}\n`);
|
|
422
|
+
}
|
|
423
|
+
return { excerpts, problems };
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// --- Writing -------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
// The excerpts a run wrote, so a later run deletes only those.
|
|
429
|
+
const RECORD = ".include-excerpts.json";
|
|
430
|
+
|
|
431
|
+
/** The names the last run wrote, as recorded in `out`. */
|
|
432
|
+
const recorded = (out) => {
|
|
433
|
+
try {
|
|
434
|
+
const names = JSON.parse(readFileSync(path.join(out, RECORD), "utf-8"));
|
|
435
|
+
return Array.isArray(names) ? names.filter(isText).filter(isInside) : [];
|
|
436
|
+
} catch {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/** A path's own entry, without following a symbolic link, else nothing. */
|
|
442
|
+
const entryAt = (file) => {
|
|
443
|
+
try {
|
|
444
|
+
return lstatSync(file, { throwIfNoEntry: false });
|
|
445
|
+
} catch {
|
|
446
|
+
// ENOTDIR: a file sits where a folder on the path should be.
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Why `name` can't be written or deleted safely, else undefined: a symbolic
|
|
453
|
+
* link at it or above it inside `out` would send the change elsewhere, and a
|
|
454
|
+
* file where a folder goes (or the reverse) would stop it halfway.
|
|
455
|
+
*/
|
|
456
|
+
const unsafeTarget = (out, name) => {
|
|
457
|
+
const segments = name.split("/");
|
|
458
|
+
for (let i = 1; i <= segments.length; i += 1) {
|
|
459
|
+
const entry = entryAt(path.join(out, ...segments.slice(0, i)));
|
|
460
|
+
const at = `${segments.slice(0, i).join("/")} in out`;
|
|
461
|
+
if (entry?.isSymbolicLink()) {
|
|
462
|
+
return `"${name}": ${at} is a symbolic link`;
|
|
463
|
+
}
|
|
464
|
+
if (entry && i < segments.length && !entry.isDirectory()) {
|
|
465
|
+
return `"${name}": ${at} is a file, not a folder`;
|
|
466
|
+
}
|
|
467
|
+
if (entry && i === segments.length && !entry.isFile()) {
|
|
468
|
+
return `"${name}": ${at} is a folder, not a file`;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/** Remove the empty folders under `dir`, deepest first. */
|
|
474
|
+
const pruneEmpty = (dir) => {
|
|
475
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
476
|
+
if (entry.isDirectory()) {
|
|
477
|
+
const child = path.join(dir, entry.name);
|
|
478
|
+
pruneEmpty(child);
|
|
479
|
+
if (readdirSync(child).length === 0) {
|
|
480
|
+
rmdirSync(child);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/** What writing would change: `{ problems, stale, writes }`. */
|
|
487
|
+
const plan = (excerpts, out) => {
|
|
488
|
+
const stale = recorded(out).filter(
|
|
489
|
+
(name) => !excerpts.has(name) && entryAt(path.join(out, name))
|
|
490
|
+
);
|
|
491
|
+
const problems = [...excerpts.keys(), ...stale]
|
|
492
|
+
.map((name) => unsafeTarget(out, name))
|
|
493
|
+
.filter(Boolean);
|
|
494
|
+
if (problems.length > 0) {
|
|
495
|
+
return { problems, stale: [], writes: [] };
|
|
496
|
+
}
|
|
497
|
+
const writes = [...excerpts.keys()].filter((name) => {
|
|
498
|
+
const file = path.join(out, name);
|
|
499
|
+
return (
|
|
500
|
+
!existsSync(file) || readFileSync(file, "utf-8") !== excerpts.get(name)
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
return { problems, stale, writes };
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const apply = (excerpts, out, changes) => {
|
|
507
|
+
mkdirSync(out, { recursive: true });
|
|
508
|
+
for (const name of changes.writes) {
|
|
509
|
+
const file = path.join(out, name);
|
|
510
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
511
|
+
writeFileSync(file, excerpts.get(name));
|
|
512
|
+
}
|
|
513
|
+
for (const name of changes.stale) {
|
|
514
|
+
rmSync(path.join(out, name));
|
|
515
|
+
}
|
|
516
|
+
writeFileSync(
|
|
517
|
+
path.join(out, RECORD),
|
|
518
|
+
`${JSON.stringify([...excerpts.keys()].toSorted(), null, 2)}\n`
|
|
519
|
+
);
|
|
520
|
+
pruneEmpty(out);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// --- Main ----------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
const fail = (message, code) => {
|
|
526
|
+
process.stderr.write(`${message}\n`);
|
|
527
|
+
process.exit(code);
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const readManifest = (file) => {
|
|
531
|
+
if (!existsSync(file)) {
|
|
532
|
+
fail(`include-excerpts: no manifest at ${file}\n\n${USAGE}`, 2);
|
|
533
|
+
}
|
|
534
|
+
let manifest;
|
|
535
|
+
try {
|
|
536
|
+
manifest = JSON.parse(readFileSync(file, "utf-8"));
|
|
537
|
+
} catch (error) {
|
|
538
|
+
fail(`include-excerpts: ${file} isn't valid JSON: ${error.message}`, 2);
|
|
539
|
+
}
|
|
540
|
+
if (
|
|
541
|
+
!(isRecord(manifest) && isText(manifest.out) && isRecord(manifest.excerpts))
|
|
542
|
+
) {
|
|
543
|
+
fail(
|
|
544
|
+
`include-excerpts: ${file} needs "out" (a folder) and "excerpts" (an object)`,
|
|
545
|
+
2
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return manifest;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
/** `{ check, manifestFile }` from the command line, or exit with the usage. */
|
|
552
|
+
const parseCli = (args) => {
|
|
553
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
554
|
+
process.stdout.write(`${USAGE}\n`);
|
|
555
|
+
process.exit(0);
|
|
556
|
+
}
|
|
557
|
+
const flags = args.filter((arg) => arg.startsWith("-"));
|
|
558
|
+
const files = args.filter((arg) => !arg.startsWith("-"));
|
|
559
|
+
const unexpected = [
|
|
560
|
+
...flags.filter((flag) => flag !== "--check"),
|
|
561
|
+
...files.slice(1),
|
|
562
|
+
];
|
|
563
|
+
if (unexpected.length > 0) {
|
|
564
|
+
fail(`include-excerpts: unexpected ${unexpected.join(" ")}\n\n${USAGE}`, 2);
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
check: flags.includes("--check"),
|
|
568
|
+
manifestFile: path.resolve(files[0] ?? "excerpts.json"),
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
/** A path with every symbolic link resolved, as far as it exists. */
|
|
573
|
+
const resolved = (file) => {
|
|
574
|
+
if (existsSync(file)) {
|
|
575
|
+
return realpathSync(file);
|
|
576
|
+
}
|
|
577
|
+
const parent = path.dirname(file);
|
|
578
|
+
return parent === file
|
|
579
|
+
? file
|
|
580
|
+
: path.join(resolved(parent), path.basename(file));
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/** The output folder, refused unless it's a `_` folder that doesn't hold the manifest. */
|
|
584
|
+
const outFolder = (manifest, manifestFile) => {
|
|
585
|
+
const out = path.resolve(path.dirname(manifestFile), manifest.out);
|
|
586
|
+
const real = resolved(out);
|
|
587
|
+
if (
|
|
588
|
+
!path.basename(out).startsWith("_") ||
|
|
589
|
+
!path.basename(real).startsWith("_") ||
|
|
590
|
+
resolved(manifestFile).startsWith(`${real}${path.sep}`) ||
|
|
591
|
+
(existsSync(out) && !statSync(out).isDirectory())
|
|
592
|
+
) {
|
|
593
|
+
fail(
|
|
594
|
+
'include-excerpts: "out" must be a folder whose name starts with "_" (like docs/_excerpts), not a link to a folder without one, and not the folder holding the manifest: Blume doesn\'t publish it, and the script writes and deletes excerpts there',
|
|
595
|
+
2
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return out;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const reportCheck = (excerpts, changes, label) => {
|
|
602
|
+
const lines = [
|
|
603
|
+
...changes.writes.map((name) => `out of date: ${name}`),
|
|
604
|
+
...changes.stale.map((name) => `not in the manifest: ${name}`),
|
|
605
|
+
];
|
|
606
|
+
if (lines.length > 0) {
|
|
607
|
+
fail(
|
|
608
|
+
[
|
|
609
|
+
...lines,
|
|
610
|
+
`include-excerpts: ${label} is out of date; run without --check.`,
|
|
611
|
+
].join("\n"),
|
|
612
|
+
1
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
process.stdout.write(
|
|
616
|
+
`include-excerpts: ${excerpts.size} excerpt(s) in ${label}, all current\n`
|
|
617
|
+
);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const main = () => {
|
|
621
|
+
const { check, manifestFile } = parseCli(process.argv.slice(2));
|
|
622
|
+
const manifest = readManifest(manifestFile);
|
|
623
|
+
const out = outFolder(manifest, manifestFile);
|
|
624
|
+
const built = buildExcerpts(manifest.excerpts, path.dirname(manifestFile));
|
|
625
|
+
const { excerpts } = built;
|
|
626
|
+
const changes = plan(excerpts, out);
|
|
627
|
+
const problems = [...built.problems, ...changes.problems];
|
|
628
|
+
if (problems.length > 0) {
|
|
629
|
+
const name = path.basename(manifestFile);
|
|
630
|
+
fail(
|
|
631
|
+
[
|
|
632
|
+
...problems.map((problem) => `${name}: ${problem}`),
|
|
633
|
+
`include-excerpts: ${problems.length} problem(s); nothing written.`,
|
|
634
|
+
].join("\n"),
|
|
635
|
+
1
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
const label = path.relative(process.cwd(), out) || ".";
|
|
639
|
+
if (check) {
|
|
640
|
+
reportCheck(excerpts, changes, label);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
apply(excerpts, out, changes);
|
|
644
|
+
process.stdout.write(
|
|
645
|
+
`include-excerpts: ${excerpts.size} excerpt(s) in ${label} (${changes.writes.length} written, ${changes.stale.length} removed)\n`
|
|
646
|
+
);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
main();
|