blume 2.1.2 → 2.2.0

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.
Files changed (377) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +133 -0
  3. package/README.md +4 -4
  4. package/dist/cli/{chunk-74wfhv79.js → chunk-05f21r8h.js} +1 -1
  5. package/dist/cli/{chunk-9qzgg6mj.js → chunk-1cm0h819.js} +3 -3
  6. package/dist/cli/{chunk-nbyvgnnm.js → chunk-1rkxzg3n.js} +104 -47
  7. package/dist/cli/chunk-1rkxzg3n.js.map +36 -0
  8. package/dist/cli/{chunk-desvm72z.js → chunk-2nr29y95.js} +22 -26
  9. package/dist/cli/chunk-2nr29y95.js.map +13 -0
  10. package/dist/cli/{chunk-32khwcb6.js → chunk-49r370mb.js} +42 -69
  11. package/dist/cli/{chunk-32khwcb6.js.map → chunk-49r370mb.js.map} +9 -10
  12. package/dist/cli/{chunk-ms9z66nz.js → chunk-4rftmttv.js} +2 -2
  13. package/dist/cli/{chunk-s0bdnc7v.js → chunk-79bzzkx4.js} +5 -4
  14. package/dist/cli/chunk-79bzzkx4.js.map +10 -0
  15. package/dist/cli/{chunk-hvcxng5f.js → chunk-7tekknev.js} +6 -6
  16. package/dist/cli/{chunk-dv2z0rzf.js → chunk-8b489v1b.js} +38 -13
  17. package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-8b489v1b.js.map} +5 -4
  18. package/dist/cli/{chunk-0pmb4tr4.js → chunk-8f8xw4x2.js} +1195 -222
  19. package/dist/cli/chunk-8f8xw4x2.js.map +44 -0
  20. package/dist/cli/chunk-96snx8yy.js +329 -0
  21. package/dist/cli/chunk-96snx8yy.js.map +13 -0
  22. package/dist/cli/{chunk-ckrcm08h.js → chunk-a3ydrqny.js} +48 -62
  23. package/dist/cli/{chunk-ckrcm08h.js.map → chunk-a3ydrqny.js.map} +4 -5
  24. package/dist/cli/{chunk-bxwhtvw8.js → chunk-ahc5gc9h.js} +5 -5
  25. package/dist/cli/{chunk-dpy8qdjf.js → chunk-as09pafp.js} +73 -182
  26. package/dist/cli/chunk-as09pafp.js.map +18 -0
  27. package/dist/cli/{chunk-tfycddbt.js → chunk-bjk964fp.js} +2 -2
  28. package/dist/cli/{chunk-gfc5fqvf.js → chunk-br0n8n3e.js} +18 -18
  29. package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-br0n8n3e.js.map} +1 -1
  30. package/dist/cli/{chunk-297ajdcv.js → chunk-c69tw29j.js} +6 -6
  31. package/dist/cli/{chunk-297ajdcv.js.map → chunk-c69tw29j.js.map} +3 -3
  32. package/dist/cli/chunk-cn3dy3hz.js +146 -0
  33. package/dist/cli/chunk-cn3dy3hz.js.map +11 -0
  34. package/dist/cli/{chunk-124fvmzq.js → chunk-dm4mg9bj.js} +11064 -8742
  35. package/dist/cli/{chunk-124fvmzq.js.map → chunk-dm4mg9bj.js.map} +78 -64
  36. package/dist/cli/{chunk-dqhg54mr.js → chunk-dmx6cezx.js} +1 -1
  37. package/dist/cli/{chunk-60hz265r.js → chunk-dpcahcrb.js} +24 -11
  38. package/dist/cli/chunk-dpcahcrb.js.map +10 -0
  39. package/dist/cli/{chunk-a57yh0h9.js → chunk-dyk3r7jw.js} +7 -7
  40. package/dist/cli/{chunk-a57yh0h9.js.map → chunk-dyk3r7jw.js.map} +1 -1
  41. package/dist/cli/{chunk-twy7psdz.js → chunk-enk76qd5.js} +14 -10
  42. package/dist/cli/chunk-enk76qd5.js.map +10 -0
  43. package/dist/cli/{chunk-k3gr1nm9.js → chunk-fqy4n3q2.js} +13 -16
  44. package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-fqy4n3q2.js.map} +3 -3
  45. package/dist/cli/{chunk-sc93apfs.js → chunk-frk2wdq1.js} +17 -17
  46. package/dist/cli/{chunk-sc93apfs.js.map → chunk-frk2wdq1.js.map} +2 -2
  47. package/dist/cli/{chunk-kdpzw38a.js → chunk-gprwej7h.js} +95 -7
  48. package/dist/cli/chunk-gprwej7h.js.map +11 -0
  49. package/dist/cli/chunk-kkrteb15.js +157 -0
  50. package/dist/cli/chunk-kkrteb15.js.map +10 -0
  51. package/dist/cli/{chunk-ave1wcbr.js → chunk-m9bp2hc1.js} +1 -1
  52. package/dist/cli/{chunk-58bt3hag.js → chunk-mrvh4c88.js} +4 -4
  53. package/dist/cli/{chunk-5cmts6dv.js → chunk-p8zg3a2h.js} +7 -7
  54. package/dist/cli/{chunk-5cmts6dv.js.map → chunk-p8zg3a2h.js.map} +1 -1
  55. package/dist/cli/{chunk-0w6hs479.js → chunk-rkqae7kh.js} +30 -18
  56. package/dist/cli/chunk-rkqae7kh.js.map +10 -0
  57. package/dist/cli/{chunk-bqxaqn5p.js → chunk-t5xx74rd.js} +8 -8
  58. package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-t5xx74rd.js.map} +1 -1
  59. package/dist/cli/{chunk-1f882900.js → chunk-th0pgaj5.js} +2 -2
  60. package/dist/cli/{chunk-dhwtqnnd.js → chunk-vnbvhgmd.js} +2 -2
  61. package/dist/cli/{chunk-gxtg6ed2.js → chunk-vzy9rh6y.js} +21 -21
  62. package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-vzy9rh6y.js.map} +3 -3
  63. package/dist/cli/{chunk-j1x92mx9.js → chunk-w6e455pz.js} +1 -1
  64. package/dist/cli/chunk-wehv26ry.js +155 -0
  65. package/dist/cli/chunk-wehv26ry.js.map +11 -0
  66. package/dist/cli/{chunk-y4msrwm9.js → chunk-xgq4eytj.js} +15 -15
  67. package/dist/cli/{chunk-36bxs9c7.js → chunk-xt0qvz1t.js} +27 -23
  68. package/dist/cli/chunk-xt0qvz1t.js.map +10 -0
  69. package/dist/cli/index.js +78 -42
  70. package/dist/cli/index.js.map +7 -6
  71. package/dist/types/ai/changelog-components.d.ts +18 -0
  72. package/dist/types/ai/changelog-markdown.d.ts +12 -0
  73. package/dist/types/ai/openapi-components.d.ts +7 -7
  74. package/dist/types/ai/operation-details.d.ts +39 -0
  75. package/dist/types/ai/relative-links.d.ts +3 -1
  76. package/dist/types/ai/serializers.d.ts +6 -4
  77. package/dist/types/analytics/head.d.ts +4 -2
  78. package/dist/types/analytics/script.d.ts +8 -2
  79. package/dist/types/components/content/youtube.d.ts +20 -0
  80. package/dist/types/components/layout/nav-utils.d.ts +8 -0
  81. package/dist/types/components/openapi/example-panels.d.ts +33 -0
  82. package/dist/types/components/openapi/helpers.d.ts +53 -11
  83. package/dist/types/components/openapi/snippets.d.ts +7 -1
  84. package/dist/types/core/alert-diagnostics.d.ts +10 -0
  85. package/dist/types/core/builtin-tags.d.ts +16 -0
  86. package/dist/types/core/changelog-list.d.ts +48 -0
  87. package/dist/types/core/closest-match.d.ts +6 -0
  88. package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
  89. package/dist/types/core/code-fences.d.ts +8 -1
  90. package/dist/types/core/config-input.d.ts +1 -1
  91. package/dist/types/core/content-assets.d.ts +16 -18
  92. package/dist/types/core/diagnostics.d.ts +14 -0
  93. package/dist/types/core/directive-diagnostics.d.ts +22 -8
  94. package/dist/types/core/former-routes.d.ts +14 -0
  95. package/dist/types/core/github.d.ts +5 -4
  96. package/dist/types/core/heading-markers.d.ts +65 -2
  97. package/dist/types/core/hidden-pages.d.ts +3 -0
  98. package/dist/types/core/includes.d.ts +23 -0
  99. package/dist/types/core/last-modified.d.ts +18 -6
  100. package/dist/types/core/links.d.ts +42 -2
  101. package/dist/types/core/load-module.d.ts +19 -4
  102. package/dist/types/core/manifest.d.ts +14 -1
  103. package/dist/types/core/markdown-mirrors.d.ts +22 -0
  104. package/dist/types/core/mdx-elements.d.ts +32 -0
  105. package/dist/types/core/mdx-syntax.d.ts +100 -0
  106. package/dist/types/core/meta-diagnostics.d.ts +7 -0
  107. package/dist/types/core/meta.d.ts +32 -5
  108. package/dist/types/core/nav-diagnostics.d.ts +9 -2
  109. package/dist/types/core/navigation.d.ts +13 -0
  110. package/dist/types/core/ordering-prefix.d.ts +6 -0
  111. package/dist/types/core/pinned-ids.d.ts +23 -0
  112. package/dist/types/core/project-graph.d.ts +9 -1
  113. package/dist/types/core/redirect-patterns.d.ts +18 -0
  114. package/dist/types/core/relative-files.d.ts +31 -0
  115. package/dist/types/core/schema.d.ts +267 -7
  116. package/dist/types/core/sources/normalize.d.ts +101 -9
  117. package/dist/types/core/sources/types.d.ts +16 -0
  118. package/dist/types/core/static-files.d.ts +10 -0
  119. package/dist/types/core/static-host.d.ts +42 -0
  120. package/dist/types/core/svg-images.d.ts +26 -0
  121. package/dist/types/core/syntax-diagnostics.d.ts +14 -0
  122. package/dist/types/core/types.d.ts +42 -1
  123. package/dist/types/deploy/generated-files.d.ts +41 -0
  124. package/dist/types/deploy/redirects.d.ts +21 -7
  125. package/dist/types/markdown/code-title.d.ts +9 -5
  126. package/dist/types/markdown/directives.d.ts +8 -0
  127. package/dist/types/markdown/fence-language.d.ts +51 -0
  128. package/dist/types/markdown/fence-meta.d.ts +5 -0
  129. package/dist/types/markdown/github-alerts.d.ts +35 -0
  130. package/dist/types/openapi/asyncapi.d.ts +6 -5
  131. package/dist/types/openapi/checks.d.ts +10 -5
  132. package/dist/types/openapi/model.d.ts +55 -8
  133. package/dist/types/openapi/parse.d.ts +4 -1
  134. package/dist/types/sources/filesystem.d.ts +14 -4
  135. package/dist/types/sources/mdx-remote.d.ts +6 -3
  136. package/dist/types/sources/registry.d.ts +3 -3
  137. package/dist/types/theme/color.d.ts +33 -0
  138. package/dist/types/theme/palette.d.ts +122 -0
  139. package/docs/01-quickstart.mdx +1 -1
  140. package/docs/02-deployment.mdx +23 -8
  141. package/docs/04-migrating.mdx +16 -4
  142. package/docs/advanced/changelog.mdx +27 -1
  143. package/docs/advanced/skills.mdx +1 -1
  144. package/docs/cli/audit.mdx +1 -1
  145. package/docs/cli/doctor.mdx +2 -2
  146. package/docs/cli/evals.mdx +1 -1
  147. package/docs/cli/index.mdx +4 -2
  148. package/docs/cli/validate.mdx +10 -7
  149. package/docs/configuration/analytics.mdx +2 -0
  150. package/docs/configuration/customization.mdx +3 -1
  151. package/docs/configuration/index.mdx +2 -2
  152. package/docs/configuration/theming.mdx +5 -1
  153. package/docs/content/components.mdx +26 -6
  154. package/docs/content/frontmatter.mdx +1 -1
  155. package/docs/content/includes.mdx +6 -2
  156. package/docs/content/index.mdx +11 -1
  157. package/docs/content/islands.mdx +2 -0
  158. package/docs/content/meta.mdx +9 -3
  159. package/docs/content/navigation.mdx +14 -6
  160. package/docs/content/sources/obsidian.mdx +1 -1
  161. package/docs/content/sources/remote-mdx.mdx +3 -1
  162. package/docs/content/syntax.mdx +76 -8
  163. package/docs/content/variables.mdx +3 -1
  164. package/docs/discoverability/agent-discovery.mdx +1 -1
  165. package/docs/discoverability/json-api.mdx +1 -1
  166. package/docs/discoverability/llms-txt.mdx +1 -1
  167. package/docs/discoverability/markdown.mdx +2 -2
  168. package/docs/discoverability/open-graph.mdx +1 -1
  169. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  170. package/docs/references/graphql.mdx +2 -0
  171. package/docs/references/openapi.mdx +45 -7
  172. package/package.json +10 -9
  173. package/skills/blume/SKILL.md +2 -2
  174. package/skills/blume-migrate/SKILL.md +49 -27
  175. package/skills/blume-migrate/references/docsify.md +262 -0
  176. package/skills/blume-migrate/references/docus.md +298 -0
  177. package/skills/blume-migrate/references/docusaurus.md +3 -3
  178. package/skills/blume-migrate/references/fern.md +327 -0
  179. package/skills/blume-migrate/references/fumadocs.md +1 -1
  180. package/skills/blume-migrate/references/gitbook.md +267 -0
  181. package/skills/blume-migrate/references/github-wiki.md +259 -0
  182. package/skills/blume-migrate/references/jekyll.md +253 -0
  183. package/skills/blume-migrate/references/mdbook.md +256 -0
  184. package/skills/blume-migrate/references/mintlify.md +5 -5
  185. package/skills/blume-migrate/references/mkdocs.md +236 -0
  186. package/skills/blume-migrate/references/monorepo.md +20 -8
  187. package/skills/blume-migrate/references/nextra.md +2 -2
  188. package/skills/blume-migrate/references/readme.md +257 -0
  189. package/skills/blume-migrate/references/redocly.md +198 -0
  190. package/skills/blume-migrate/references/starlight.md +3 -3
  191. package/skills/blume-migrate/references/vitepress.md +204 -0
  192. package/skills/blume-migrate/references/vuepress.md +271 -0
  193. package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
  194. package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
  195. package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
  196. package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
  197. package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
  198. package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
  199. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
  200. package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
  201. package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
  202. package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
  203. package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
  204. package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
  205. package/src/ai/changelog-components.ts +24 -0
  206. package/src/ai/changelog-markdown.ts +45 -41
  207. package/src/ai/component-markdown.ts +22 -9
  208. package/src/ai/llms.ts +36 -5
  209. package/src/ai/markdown.ts +2 -2
  210. package/src/ai/openapi-components.ts +25 -9
  211. package/src/ai/operation-details.ts +208 -0
  212. package/src/ai/relative-links.ts +48 -8
  213. package/src/ai/serializers.ts +7 -6
  214. package/src/ai/site-skill.ts +6 -2
  215. package/src/analytics/head.ts +11 -5
  216. package/src/analytics/script.ts +13 -3
  217. package/src/astro/generate.ts +32 -6
  218. package/src/astro/index.ts +1 -0
  219. package/src/astro/integration.ts +27 -1
  220. package/src/astro/mdx-source-errors.ts +46 -0
  221. package/src/astro/templates.ts +89 -185
  222. package/src/audit/checks/redirects.ts +5 -1
  223. package/src/audit/redirects.ts +36 -19
  224. package/src/audit/report.ts +22 -9
  225. package/src/audit/run.ts +12 -28
  226. package/src/audit/terms.ts +23 -0
  227. package/src/audit/url.ts +2 -2
  228. package/src/cli/build-failure.ts +256 -7
  229. package/src/cli/command-meta.ts +1 -1
  230. package/src/cli/commands/audit.ts +1 -1
  231. package/src/cli/commands/build.ts +12 -3
  232. package/src/cli/commands/check.ts +13 -2
  233. package/src/cli/commands/dev.ts +1 -1
  234. package/src/cli/commands/doctor.ts +4 -2
  235. package/src/cli/commands/init.ts +3 -3
  236. package/src/cli/commands/preview.ts +17 -1
  237. package/src/cli/commands/validate.ts +22 -10
  238. package/src/cli/prepare.ts +1 -1
  239. package/src/cli/preview-pages.ts +161 -0
  240. package/src/cli/unknown-flags.ts +1 -42
  241. package/src/components/code-copy.ts +51 -0
  242. package/src/components/code-language.ts +75 -0
  243. package/src/components/content/AccordionItem.astro +16 -1
  244. package/src/components/content/Callout.astro +11 -1
  245. package/src/components/content/Changelog.astro +189 -0
  246. package/src/components/content/CodeBlock.astro +8 -11
  247. package/src/components/content/Tabs.astro +16 -2
  248. package/src/components/content/YouTube.astro +6 -4
  249. package/src/components/content/code-block.ts +33 -0
  250. package/src/components/content/content-strings.ts +5 -1
  251. package/src/components/content/inline-markdown.ts +35 -8
  252. package/src/components/content/youtube.ts +49 -7
  253. package/src/components/layout/Analytics.astro +5 -1
  254. package/src/components/layout/NavTree.astro +8 -3
  255. package/src/components/layout/RootLayout.astro +18 -31
  256. package/src/components/layout/nav-utils.ts +14 -0
  257. package/src/components/layout/search/pagefind.ts +2 -1
  258. package/src/components/openapi/Operation.astro +4 -0
  259. package/src/components/openapi/PanelTabs.astro +9 -3
  260. package/src/components/openapi/RequestBody.astro +42 -8
  261. package/src/components/openapi/RequestPanel.astro +34 -53
  262. package/src/components/openapi/example-panels.ts +110 -0
  263. package/src/components/openapi/helpers.ts +195 -20
  264. package/src/components/openapi/operation-model.ts +63 -26
  265. package/src/components/openapi/request.ts +13 -0
  266. package/src/components/openapi/snippets.ts +14 -2
  267. package/src/core/alert-diagnostics.ts +89 -0
  268. package/src/core/builtin-tags.ts +76 -0
  269. package/src/core/changelog-list.ts +87 -0
  270. package/src/core/closest-match.ts +41 -0
  271. package/src/core/code-fence-diagnostics.ts +267 -0
  272. package/src/core/code-fences.ts +43 -7
  273. package/src/core/component-diagnostics.ts +65 -12
  274. package/src/core/config-input.ts +1 -1
  275. package/src/core/config.ts +46 -1
  276. package/src/core/content-assets.ts +64 -144
  277. package/src/core/diagnostics.ts +36 -3
  278. package/src/core/directive-diagnostics.ts +265 -47
  279. package/src/core/former-routes.ts +46 -0
  280. package/src/core/git-ignored.ts +53 -0
  281. package/src/core/github.ts +5 -4
  282. package/src/core/heading-markers.ts +156 -2
  283. package/src/core/hidden-pages.ts +64 -0
  284. package/src/core/includes.ts +181 -41
  285. package/src/core/last-modified.ts +47 -10
  286. package/src/core/links.ts +438 -83
  287. package/src/core/load-module.ts +98 -9
  288. package/src/core/manifest.ts +12 -9
  289. package/src/core/markdown-mirrors.ts +52 -0
  290. package/src/core/mdx-elements.ts +153 -0
  291. package/src/core/mdx-syntax.ts +377 -0
  292. package/src/core/meta-diagnostics.ts +134 -0
  293. package/src/core/meta.ts +154 -55
  294. package/src/core/nav-diagnostics.ts +16 -4
  295. package/src/core/navigation.ts +70 -10
  296. package/src/core/ordering-prefix.ts +22 -2
  297. package/src/core/pinned-ids.ts +141 -0
  298. package/src/core/project-graph.ts +221 -29
  299. package/src/core/redirect-patterns.ts +41 -0
  300. package/src/core/relative-files.ts +145 -0
  301. package/src/core/schema.ts +17 -3
  302. package/src/core/sources/mdx-remote.ts +75 -14
  303. package/src/core/sources/normalize.ts +885 -179
  304. package/src/core/sources/notion.ts +5 -4
  305. package/src/core/sources/obsidian.ts +2 -5
  306. package/src/core/sources/types.ts +17 -0
  307. package/src/core/static-files.ts +40 -0
  308. package/src/core/static-host.ts +125 -0
  309. package/src/core/svg-images.ts +98 -0
  310. package/src/core/syntax-diagnostics.ts +491 -0
  311. package/src/core/types.ts +43 -1
  312. package/src/deploy/artifacts.ts +2 -28
  313. package/src/deploy/generated-files.ts +128 -0
  314. package/src/deploy/platforms/vercel.ts +25 -1
  315. package/src/deploy/redirects.ts +43 -16
  316. package/src/deploy/sitemap.ts +2 -1
  317. package/src/markdown/base-links.ts +87 -6
  318. package/src/markdown/code-title.ts +29 -13
  319. package/src/markdown/content-assets.ts +210 -0
  320. package/src/markdown/directives.ts +69 -5
  321. package/src/markdown/fence-language.ts +106 -0
  322. package/src/markdown/fence-meta.ts +42 -0
  323. package/src/markdown/github-alerts.ts +172 -0
  324. package/src/markdown/heading-anchors.ts +265 -56
  325. package/src/markdown/include.ts +30 -3
  326. package/src/markdown/index.ts +145 -40
  327. package/src/markdown/prompt-text.ts +95 -0
  328. package/src/markdown/relative-links.ts +37 -2
  329. package/src/markdown/route-snapshot.ts +22 -12
  330. package/src/markdown/svg-fallback.ts +51 -0
  331. package/src/markdown/title-heading.ts +134 -0
  332. package/src/migrate/migrate.ts +127 -2
  333. package/src/og/card.ts +14 -4
  334. package/src/openapi/asyncapi.ts +14 -6
  335. package/src/openapi/checks.ts +160 -15
  336. package/src/openapi/model.ts +172 -33
  337. package/src/openapi/parse.ts +13 -4
  338. package/src/openapi/render-mdx.ts +55 -9
  339. package/src/openapi/source.ts +87 -6
  340. package/src/registry/eject.ts +0 -1
  341. package/src/search/documents.ts +10 -2
  342. package/src/sources/filesystem.ts +28 -3
  343. package/src/sources/mdx-remote.ts +7 -15
  344. package/src/theme/color.ts +14 -0
  345. package/src/theme/entry.ts +22 -1
  346. package/src/theme/palette.ts +74 -1
  347. package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
  348. package/dist/cli/chunk-0w6hs479.js.map +0 -10
  349. package/dist/cli/chunk-150jk7km.js +0 -52
  350. package/dist/cli/chunk-150jk7km.js.map +0 -10
  351. package/dist/cli/chunk-36bxs9c7.js.map +0 -10
  352. package/dist/cli/chunk-53jqw8z0.js +0 -89
  353. package/dist/cli/chunk-53jqw8z0.js.map +0 -10
  354. package/dist/cli/chunk-60hz265r.js.map +0 -10
  355. package/dist/cli/chunk-desvm72z.js.map +0 -14
  356. package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
  357. package/dist/cli/chunk-es8h0v3j.js +0 -441
  358. package/dist/cli/chunk-es8h0v3j.js.map +0 -11
  359. package/dist/cli/chunk-kdpzw38a.js.map +0 -11
  360. package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
  361. package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
  362. package/dist/cli/chunk-twy7psdz.js.map +0 -10
  363. package/dist/cli/chunk-ydp3065p.js +0 -64
  364. package/dist/cli/chunk-ydp3065p.js.map +0 -10
  365. /package/dist/cli/{chunk-74wfhv79.js.map → chunk-05f21r8h.js.map} +0 -0
  366. /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-1cm0h819.js.map} +0 -0
  367. /package/dist/cli/{chunk-ms9z66nz.js.map → chunk-4rftmttv.js.map} +0 -0
  368. /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-7tekknev.js.map} +0 -0
  369. /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-ahc5gc9h.js.map} +0 -0
  370. /package/dist/cli/{chunk-tfycddbt.js.map → chunk-bjk964fp.js.map} +0 -0
  371. /package/dist/cli/{chunk-dqhg54mr.js.map → chunk-dmx6cezx.js.map} +0 -0
  372. /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-m9bp2hc1.js.map} +0 -0
  373. /package/dist/cli/{chunk-58bt3hag.js.map → chunk-mrvh4c88.js.map} +0 -0
  374. /package/dist/cli/{chunk-1f882900.js.map → chunk-th0pgaj5.js.map} +0 -0
  375. /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-vnbvhgmd.js.map} +0 -0
  376. /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-w6e455pz.js.map} +0 -0
  377. /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-xgq4eytj.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();