blume 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
  3. package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
  4. package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
  5. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
  6. package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
  7. package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
  8. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
  9. package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
  10. package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
  11. package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
  12. package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
  13. package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
  14. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  15. package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
  16. package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
  17. package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
  18. package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
  19. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  20. package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
  21. package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
  22. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
  23. package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
  24. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  25. package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
  26. package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
  27. package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
  28. package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
  29. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  30. package/dist/cli/chunk-e04dxsz1.js +39 -0
  31. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  32. package/dist/cli/{chunk-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
  33. package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
  34. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  35. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  36. package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
  37. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  38. package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
  39. package/dist/cli/chunk-j85vccga.js.map +36 -0
  40. package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
  41. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
  42. package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
  43. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
  44. package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
  45. package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
  46. package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
  47. package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
  48. package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
  49. package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
  50. package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
  51. package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
  52. package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
  53. package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
  54. package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
  55. package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
  56. package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
  57. package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
  58. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  59. package/dist/cli/index.js +20 -18
  60. package/dist/cli/index.js.map +3 -3
  61. package/dist/types/ai/link-headers.d.ts +8 -1
  62. package/dist/types/ai/openapi-components.d.ts +5 -2
  63. package/dist/types/ai/relative-links.d.ts +9 -7
  64. package/dist/types/ai/skills.d.ts +4 -1
  65. package/dist/types/ai/tar.d.ts +1 -3
  66. package/dist/types/analytics/index.d.ts +2 -0
  67. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  68. package/dist/types/analytics/schema.d.ts +14 -0
  69. package/dist/types/astro/integration.d.ts +3 -2
  70. package/dist/types/core/base-path.d.ts +21 -7
  71. package/dist/types/core/config-input.d.ts +9 -7
  72. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  73. package/dist/types/core/heading-markers.d.ts +5 -7
  74. package/dist/types/core/i18n.d.ts +2 -0
  75. package/dist/types/core/last-modified.d.ts +10 -0
  76. package/dist/types/core/meta.d.ts +8 -0
  77. package/dist/types/core/schema.d.ts +7 -0
  78. package/dist/types/core/sources/lower.d.ts +29 -16
  79. package/dist/types/core/sources/normalize.d.ts +13 -1
  80. package/dist/types/core/sources/watch.d.ts +12 -5
  81. package/dist/types/core/standard-schema.d.ts +5 -0
  82. package/dist/types/deploy/artifacts.d.ts +6 -4
  83. package/dist/types/deploy/headers.d.ts +5 -0
  84. package/dist/types/deploy/platforms/types.d.ts +8 -0
  85. package/dist/types/deploy/redirects.d.ts +24 -13
  86. package/dist/types/markdown/directives.d.ts +62 -0
  87. package/dist/types/markdown/features.d.ts +21 -0
  88. package/dist/types/markdown/mdast.d.ts +63 -0
  89. package/dist/types/openapi/asyncapi.d.ts +4 -2
  90. package/docs/02-deployment.mdx +4 -2
  91. package/docs/08-faq.mdx +1 -1
  92. package/docs/advanced/custom-pages.mdx +3 -3
  93. package/docs/cli/audit.mdx +17 -1
  94. package/docs/cli/evals.mdx +3 -3
  95. package/docs/cli/translate.mdx +3 -3
  96. package/docs/cli/version.mdx +1 -1
  97. package/docs/configuration/analytics.mdx +20 -1
  98. package/docs/configuration/customization.mdx +2 -0
  99. package/docs/configuration/search.mdx +1 -1
  100. package/docs/content/components.mdx +1 -1
  101. package/docs/content/frontmatter.mdx +1 -1
  102. package/docs/content/i18n.mdx +2 -0
  103. package/docs/content/index.mdx +4 -2
  104. package/docs/content/islands.mdx +1 -1
  105. package/docs/content/meta.mdx +3 -1
  106. package/docs/content/sources.mdx +1 -5
  107. package/docs/content/syntax.mdx +14 -0
  108. package/docs/content/versioning.mdx +1 -1
  109. package/docs/discoverability/agent-discovery.mdx +1 -1
  110. package/docs/discoverability/index.mdx +2 -1
  111. package/docs/discoverability/markdown.mdx +2 -2
  112. package/docs/discoverability/open-graph.mdx +5 -3
  113. package/docs/discoverability/rss.mdx +1 -1
  114. package/docs/references/asyncapi.mdx +1 -1
  115. package/docs/references/graphql.mdx +1 -1
  116. package/docs/references/openapi.mdx +5 -3
  117. package/package.json +1 -1
  118. package/skills/blume-migrate/references/fumadocs.md +1 -1
  119. package/skills/blume-migrate/references/nextra.md +1 -1
  120. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
  121. package/src/ai/agent-readability.ts +2 -2
  122. package/src/ai/ai-catalog.ts +6 -2
  123. package/src/ai/api/handlers.ts +2 -2
  124. package/src/ai/api/spec.ts +2 -2
  125. package/src/ai/api-catalog.ts +6 -2
  126. package/src/ai/changelog-markdown.ts +2 -2
  127. package/src/ai/link-headers.ts +14 -5
  128. package/src/ai/llms.ts +18 -7
  129. package/src/ai/mcp/discovery.ts +2 -2
  130. package/src/ai/mcp/query.ts +2 -2
  131. package/src/ai/mcp/server.ts +2 -2
  132. package/src/ai/openapi-components.ts +22 -6
  133. package/src/ai/relative-links.ts +42 -26
  134. package/src/ai/serializers.ts +2 -1
  135. package/src/ai/skills.ts +14 -1
  136. package/src/ai/tar.ts +139 -12
  137. package/src/analytics/head.ts +4 -0
  138. package/src/analytics/index.ts +5 -0
  139. package/src/analytics/one-dollar-stats.ts +86 -0
  140. package/src/analytics/schema.ts +2 -0
  141. package/src/astro/generate.ts +1 -1
  142. package/src/astro/include-hmr.ts +31 -12
  143. package/src/astro/include-refresh.ts +19 -8
  144. package/src/astro/integration.ts +108 -14
  145. package/src/astro/runtime-modules.ts +28 -13
  146. package/src/astro/templates.ts +81 -37
  147. package/src/audit/checks/links.ts +8 -1
  148. package/src/audit/graph.ts +3 -1
  149. package/src/audit/run.ts +12 -9
  150. package/src/audit/snapshot.ts +5 -0
  151. package/src/cli/args.ts +32 -0
  152. package/src/cli/commands/audit.ts +4 -1
  153. package/src/cli/commands/doctor.ts +3 -1
  154. package/src/cli/commands/eval.ts +19 -11
  155. package/src/cli/commands/translate.ts +9 -12
  156. package/src/cli/commands/validate.ts +3 -1
  157. package/src/cli/dev-lock.ts +157 -37
  158. package/src/cli/report-format.ts +11 -6
  159. package/src/components/colors.ts +19 -0
  160. package/src/components/content/Badge.astro +5 -3
  161. package/src/components/content/Component.astro +2 -2
  162. package/src/components/content/Tab.astro +0 -1
  163. package/src/components/content/Tabs.astro +4 -0
  164. package/src/components/content/badge-color.ts +5 -2
  165. package/src/components/content/base-href.ts +13 -36
  166. package/src/components/islands/assistant.tsx +21 -4
  167. package/src/components/islands/base-path.ts +47 -10
  168. package/src/components/islands/hooks.ts +35 -19
  169. package/src/components/islands/webmcp.ts +4 -2
  170. package/src/components/layout/Breadcrumbs.astro +5 -2
  171. package/src/components/layout/DiscoveryLinks.astro +9 -5
  172. package/src/components/layout/Header.astro +2 -2
  173. package/src/components/layout/LanguageSwitcher.astro +2 -2
  174. package/src/components/layout/NavSelector.astro +2 -2
  175. package/src/components/layout/NavTabMenu.astro +3 -3
  176. package/src/components/layout/NavTree.astro +16 -6
  177. package/src/components/layout/PageActions.astro +4 -3
  178. package/src/components/layout/PageLayout.astro +7 -6
  179. package/src/components/layout/Pagination.astro +3 -3
  180. package/src/components/layout/RootLayout.astro +5 -5
  181. package/src/components/layout/Search.astro +33 -13
  182. package/src/components/layout/VersionBanner.astro +2 -2
  183. package/src/components/layout/analytics-client.ts +12 -0
  184. package/src/components/layout/toc-active.ts +41 -0
  185. package/src/components/layout/toc-element.ts +8 -14
  186. package/src/components/openapi/ApiTagOperations.astro +2 -2
  187. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  188. package/src/components/openapi/GraphqlChip.astro +2 -2
  189. package/src/components/openapi/Operation.astro +2 -0
  190. package/src/components/openapi/Playground.astro +4 -4
  191. package/src/components/openapi/RequestPanel.astro +5 -2
  192. package/src/components/openapi/SchemaTable.astro +7 -0
  193. package/src/components/openapi/async.ts +38 -6
  194. package/src/components/openapi/helpers.ts +19 -9
  195. package/src/components/openapi/message-composer.ts +6 -1
  196. package/src/components/openapi/message.ts +17 -2
  197. package/src/components/openapi/operation-model.ts +81 -11
  198. package/src/components/openapi/panel.ts +4 -2
  199. package/src/components/openapi/param-style.ts +181 -0
  200. package/src/components/openapi/playground-client.ts +73 -16
  201. package/src/components/openapi/request.ts +190 -26
  202. package/src/components/openapi/schema-tree.ts +30 -24
  203. package/src/components/openapi/snippets.ts +85 -6
  204. package/src/components/openapi/ws-client.ts +18 -2
  205. package/src/core/base-path.ts +62 -16
  206. package/src/core/config-input.ts +9 -7
  207. package/src/core/diagnostics.ts +2 -0
  208. package/src/core/directive-diagnostics.ts +99 -0
  209. package/src/core/frontmatter.ts +21 -18
  210. package/src/core/heading-markers.ts +5 -18
  211. package/src/core/i18n.ts +31 -3
  212. package/src/core/last-modified.ts +25 -3
  213. package/src/core/locale-links.ts +5 -1
  214. package/src/core/meta.ts +33 -19
  215. package/src/core/navigation.ts +28 -8
  216. package/src/core/project-graph.ts +27 -5
  217. package/src/core/schema.ts +24 -4
  218. package/src/core/sources/contentful-rich-text.ts +27 -20
  219. package/src/core/sources/filesystem.ts +20 -2
  220. package/src/core/sources/github-releases.ts +25 -6
  221. package/src/core/sources/lexical.ts +23 -18
  222. package/src/core/sources/lower.ts +201 -34
  223. package/src/core/sources/mdx-remote.ts +51 -16
  224. package/src/core/sources/normalize.ts +325 -90
  225. package/src/core/sources/notion.ts +52 -25
  226. package/src/core/sources/obsidian.ts +23 -6
  227. package/src/core/sources/portable-text.ts +38 -21
  228. package/src/core/sources/strapi-blocks.ts +20 -16
  229. package/src/core/sources/watch.ts +20 -7
  230. package/src/core/standard-schema.ts +10 -6
  231. package/src/core/version-cut.ts +52 -15
  232. package/src/deploy/artifacts.ts +27 -6
  233. package/src/deploy/cloudflare-negotiation.ts +4 -12
  234. package/src/deploy/headers.ts +8 -4
  235. package/src/deploy/node-headers.ts +1 -1
  236. package/src/deploy/platforms/cloudflare.ts +12 -3
  237. package/src/deploy/platforms/netlify.ts +1 -0
  238. package/src/deploy/platforms/node.ts +1 -0
  239. package/src/deploy/platforms/static.ts +1 -0
  240. package/src/deploy/platforms/types.ts +8 -0
  241. package/src/deploy/platforms/vercel.ts +1 -0
  242. package/src/deploy/redirects.ts +54 -18
  243. package/src/deploy/robots.ts +2 -2
  244. package/src/deploy/rss.ts +4 -3
  245. package/src/deploy/sitemap.ts +7 -5
  246. package/src/markdown/base-links.ts +34 -36
  247. package/src/markdown/directives.ts +242 -36
  248. package/src/markdown/features.ts +17 -0
  249. package/src/markdown/index.ts +7 -6
  250. package/src/markdown/mdast.ts +5 -2
  251. package/src/markdown/relative-links.ts +12 -4
  252. package/src/og/card.ts +129 -5
  253. package/src/og/derive.ts +41 -31
  254. package/src/og/index.ts +1 -0
  255. package/src/openapi/asyncapi.ts +4 -2
  256. package/src/openapi/model.ts +22 -14
  257. package/src/openapi/proxy.ts +63 -10
  258. package/src/openapi/render-mdx.ts +41 -2
  259. package/src/registry/eject.ts +181 -24
  260. package/src/search/adapters/version-scope.ts +30 -0
  261. package/src/search/documents.ts +67 -26
  262. package/src/search/popular.ts +2 -1
  263. package/src/seo/jsonld.ts +7 -3
  264. package/src/translate/meta.ts +68 -24
  265. package/src/translate/run.ts +3 -3
  266. package/src/translate/validate.ts +4 -1
  267. package/src/translate/work-list.ts +56 -17
  268. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  269. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  270. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  271. package/dist/cli/chunk-esh98wmb.js +0 -23
  272. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  273. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  274. package/dist/cli/chunk-j85scx15.js.map +0 -10
  275. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  276. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  277. /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
  278. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
  279. /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
  280. /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
  281. /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
  282. /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
  283. /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
  284. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
  285. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
  286. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
  287. /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
  288. /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
  289. /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
  290. /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
  291. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
  292. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
  293. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { JsonObject } from "./json.ts";
11
11
  import { asString, getPath, objectsIn } from "./json.ts";
12
- import type { InlineMarks } from "./lower.ts";
12
+ import type { InlineMarks, InlinePart } from "./lower.ts";
13
13
  import {
14
14
  absoluteUrl,
15
15
  blockquote,
@@ -17,10 +17,10 @@ import {
17
17
  headingPrefix,
18
18
  image,
19
19
  joinBlocks,
20
+ linkParts,
20
21
  listItem,
21
22
  markdownDocument,
22
23
  renderInline,
23
- renderLink,
24
24
  unsupported,
25
25
  writesMdx,
26
26
  } from "./lower.ts";
@@ -139,45 +139,50 @@ const embeddedEntry = (
139
139
  );
140
140
  };
141
141
 
142
- const renderInlines = (
142
+ const inlineParts = (
143
143
  nodes: JsonObject[],
144
144
  options: ContentfulRichTextOptions
145
- ): string =>
146
- nodes
147
- // oxlint-disable-next-line no-use-before-define -- mutual recursion: a link's label is itself inline content
148
- .map((node) => renderInlineNode(node, options))
149
- .join("");
145
+ ): InlinePart[] =>
146
+ // oxlint-disable-next-line no-use-before-define -- mutual recursion: a link's label is itself inline content
147
+ nodes.flatMap((node) => inlineNodeParts(node, options));
150
148
 
151
- const renderInlineNode = (
149
+ const inlineNodeParts = (
152
150
  node: JsonObject,
153
151
  options: ContentfulRichTextOptions
154
- ): string => {
152
+ ): InlinePart[] => {
155
153
  switch (asString(node.nodeType)) {
156
154
  case "text": {
157
- return renderInline(asString(node.value) ?? "", marksOf(node));
155
+ return [{ marks: marksOf(node), text: asString(node.value) ?? "" }];
158
156
  }
159
157
  case "hyperlink": {
160
- return renderLink(
161
- renderInlines(children(node), options),
158
+ return linkParts(
159
+ inlineParts(children(node), options),
162
160
  asString(getPath(node, "data.uri"))
163
161
  );
164
162
  }
165
163
  case "asset-hyperlink": {
166
- return renderLink(
167
- renderInlines(children(node), options),
164
+ return linkParts(
165
+ inlineParts(children(node), options),
168
166
  linkedAsset(node, options)?.url
169
167
  );
170
168
  }
171
169
  case "embedded-entry-inline": {
172
- return embeddedEntry(node, options);
170
+ return [{ markdown: embeddedEntry(node, options) }];
173
171
  }
174
172
  default: {
175
173
  // entry-hyperlink (no route to point at) and anything unknown: the label.
176
- return renderInlines(children(node), options);
174
+ return inlineParts(children(node), options);
177
175
  }
178
176
  }
179
177
  };
180
178
 
179
+ /** A block's inline children as Markdown, guarded as one block. */
180
+ const blockText = (
181
+ node: JsonObject,
182
+ options: ContentfulRichTextOptions
183
+ ): string =>
184
+ guardBlockStart(renderInline(inlineParts(children(node), options)));
185
+
181
186
  const renderList = (
182
187
  items: JsonObject[],
183
188
  marker: (index: number) => string,
@@ -198,12 +203,14 @@ const renderList = (
198
203
  )
199
204
  .join("\n");
200
205
 
206
+ // Each of a cell's paragraphs is a block as much as a page's is, so it goes
207
+ // through the same guard: two struck runs in one still meet as `~~~~`.
201
208
  const cellText = (
202
209
  cell: JsonObject,
203
210
  options: ContentfulRichTextOptions
204
211
  ): string =>
205
212
  children(cell)
206
- .map((paragraph) => renderInlines(children(paragraph), options))
213
+ .map((paragraph) => blockText(paragraph, options))
207
214
  .join(" ")
208
215
  .replaceAll(CELL_BREAK, " ")
209
216
  .replaceAll(CELL_PIPE, String.raw`\|`);
@@ -235,11 +242,11 @@ const renderBlock = (
235
242
  const type = asString(node.nodeType) ?? "";
236
243
  const heading = HEADING.exec(type)?.groups?.level;
237
244
  if (heading) {
238
- return `${headingPrefix(Number(heading))}${guardBlockStart(renderInlines(children(node), options))}`;
245
+ return `${headingPrefix(Number(heading))}${blockText(node, options)}`;
239
246
  }
240
247
  switch (type) {
241
248
  case "paragraph": {
242
- return guardBlockStart(renderInlines(children(node), options));
249
+ return blockText(node, options);
243
250
  }
244
251
  case "blockquote": {
245
252
  return blockquote(
@@ -6,6 +6,8 @@ import { glob } from "tinyglobby";
6
6
 
7
7
  import { BlumeError } from "../diagnostics.ts";
8
8
  import matter from "../frontmatter.ts";
9
+ import type { Diagnostic } from "../types.ts";
10
+ import { unloadablePathDiagnostic } from "./normalize.ts";
9
11
  import type { ContentSource, SourceEntry, SourceLoadResult } from "./types.ts";
10
12
  import {
11
13
  baselineScanIgnore,
@@ -54,8 +56,24 @@ export const filesystemSource = (
54
56
  });
55
57
  files.sort();
56
58
 
59
+ // A file Astro's content loader can't read would publish a route that
60
+ // only ever renders "Page not found"; it is reported and left out.
61
+ const diagnostics: Diagnostic[] = [];
62
+ const loadable: string[] = [];
63
+ for (const file of files) {
64
+ const unloadable = unloadablePathDiagnostic(
65
+ relative(contentRoot, file),
66
+ file
67
+ );
68
+ if (unloadable) {
69
+ diagnostics.push(unloadable);
70
+ } else {
71
+ loadable.push(file);
72
+ }
73
+ }
74
+
57
75
  const entries = await Promise.all(
58
- files.map(async (file): Promise<SourceEntry> => {
76
+ loadable.map(async (file): Promise<SourceEntry> => {
59
77
  const source = await readFile(file, "utf-8");
60
78
  const ext = extname(file).toLowerCase();
61
79
  const format = ext === ".mdx" ? "mdx" : "md";
@@ -73,7 +91,7 @@ export const filesystemSource = (
73
91
  })
74
92
  );
75
93
 
76
- return { diagnostics: [], entries };
94
+ return { diagnostics, entries };
77
95
  };
78
96
 
79
97
  const validate = (): void => {
@@ -16,6 +16,7 @@ import {
16
16
  snapshotCache,
17
17
  } from "./cache.ts";
18
18
  import { destination, escapeMarkdownText } from "./lower.ts";
19
+ import { REMOTE_TIMEOUT_MS } from "./remote.ts";
19
20
  import type {
20
21
  ContentSource,
21
22
  SourceContext,
@@ -358,15 +359,33 @@ export const githubReleasesSource = (
358
359
  (options.drafts || !release.draft) &&
359
360
  (options.prereleases || !release.prerelease);
360
361
 
362
+ // Each page gets {@link REMOTE_TIMEOUT_MS}, body included: an API that
363
+ // accepts the connection and never answers would otherwise hold the scan,
364
+ // and so the build or every dev rescan, indefinitely. The timeout's error
365
+ // names the URL and the limit, so the empty-changelog warning says what
366
+ // stalled instead of only that an operation was aborted.
361
367
  const fetchPage = async (page: number): Promise<GithubRelease[]> => {
362
368
  const url = `${base}/repos/${options.owner}/${options.repo}/releases?per_page=${PER_PAGE}&page=${page}`;
363
- const res = await doFetch(url, { headers: githubHeaders() });
364
- if (!res.ok) {
365
- throw new Error(`${url} -> ${res.status}`);
369
+ try {
370
+ const res = await doFetch(url, {
371
+ headers: githubHeaders(),
372
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS),
373
+ });
374
+ if (!res.ok) {
375
+ throw new Error(`${url} -> ${res.status}`);
376
+ }
377
+ // SAFETY: GitHub's releases endpoint returns a JSON array of release
378
+ // objects; `GithubRelease` models only the fields the adapter reads.
379
+ return (await res.json()) as GithubRelease[];
380
+ } catch (error) {
381
+ if (error instanceof DOMException && error.name === "TimeoutError") {
382
+ throw new Error(
383
+ `${url} did not respond within ${REMOTE_TIMEOUT_MS / 1000}s`,
384
+ { cause: error }
385
+ );
386
+ }
387
+ throw error;
366
388
  }
367
- // SAFETY: GitHub's releases endpoint returns a JSON array of release
368
- // objects; `GithubRelease` models only the fields the adapter reads.
369
- return (await res.json()) as GithubRelease[];
370
389
  };
371
390
 
372
391
  const fetchReleases = async (): Promise<GithubRelease[]> => {
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { JsonObject } from "./json.ts";
9
9
  import { asNumber, asObject, asString, getPath, objectsIn } from "./json.ts";
10
- import type { InlineMarks } from "./lower.ts";
10
+ import type { InlineMarks, InlinePart } from "./lower.ts";
11
11
  import {
12
12
  absoluteUrl,
13
13
  blockquote,
@@ -15,10 +15,10 @@ import {
15
15
  headingPrefix,
16
16
  image,
17
17
  indent,
18
+ linkParts,
18
19
  listItem,
19
20
  markdownDocument,
20
21
  renderInline,
21
- renderLink,
22
22
  unsupported,
23
23
  writesMdx,
24
24
  } from "./lower.ts";
@@ -67,48 +67,53 @@ const customBlock = (node: JsonObject, options: LexicalOptions): string => {
67
67
  );
68
68
  };
69
69
 
70
- const renderInlines = (nodes: JsonObject[], options: LexicalOptions): string =>
71
- nodes
72
- // oxlint-disable-next-line no-use-before-define -- mutual recursion: a link's label is itself inline content
73
- .map((node) => renderInlineNode(node, options))
74
- .join("");
70
+ const inlineParts = (
71
+ nodes: JsonObject[],
72
+ options: LexicalOptions
73
+ ): InlinePart[] =>
74
+ // oxlint-disable-next-line no-use-before-define -- mutual recursion: a link's label is itself inline content
75
+ nodes.flatMap((node) => inlineNodeParts(node, options));
75
76
 
76
- const renderInlineNode = (
77
+ const inlineNodeParts = (
77
78
  node: JsonObject,
78
79
  options: LexicalOptions
79
- ): string => {
80
+ ): InlinePart[] => {
80
81
  switch (typeOf(node)) {
81
82
  case "text": {
82
- return renderInline(asString(node.text) ?? "", marksOf(node));
83
+ return [{ marks: marksOf(node), text: asString(node.text) ?? "" }];
83
84
  }
84
85
  case "linebreak": {
85
- return "\n";
86
+ return [{ markdown: "\n" }];
86
87
  }
87
88
  case "tab": {
88
- return "\t";
89
+ return [{ markdown: "\t" }];
89
90
  }
90
91
  case "link":
91
92
  case "autolink": {
92
93
  // `fields.url` on Payload 3; `url` on older Lexical payloads. An
93
94
  // internal link (`linkType: "internal"`) carries a document, not a
94
95
  // URL, so its label stays.
95
- return renderLink(
96
- renderInlines(children(node), options),
96
+ return linkParts(
97
+ inlineParts(children(node), options),
97
98
  asString(getPath(node, "fields.url")) ?? asString(node.url)
98
99
  );
99
100
  }
100
101
  case "inlineBlock": {
101
- return customBlock(node, options);
102
+ return [{ markdown: customBlock(node, options) }];
102
103
  }
103
104
  default: {
104
- return renderInlines(children(node), options);
105
+ return inlineParts(children(node), options);
105
106
  }
106
107
  }
107
108
  };
108
109
 
110
+ /** Inline nodes as Markdown, guarded as one block. */
111
+ const inlineText = (nodes: JsonObject[], options: LexicalOptions): string =>
112
+ guardBlockStart(renderInline(inlineParts(nodes, options)));
113
+
109
114
  /** A text block's inline children, joined and guarded as one block. */
110
115
  const blockText = (node: JsonObject, options: LexicalOptions): string =>
111
- guardBlockStart(renderInlines(children(node), options));
116
+ inlineText(children(node), options);
112
117
 
113
118
  const renderUpload = (node: JsonObject, options: LexicalOptions): string => {
114
119
  const value = asObject(node.value);
@@ -145,7 +150,7 @@ const renderList = (node: JsonObject, options: LexicalOptions): string => {
145
150
  if (listType === "check") {
146
151
  check = item.checked === true ? "[x] " : "[ ] ";
147
152
  }
148
- const line = `${check}${guardBlockStart(renderInlines(inline, options))}`;
153
+ const line = `${check}${inlineText(inline, options)}`;
149
154
  items.push(listItem(lastMarker, sub ? `${line}\n${sub}` : line));
150
155
  index += 1;
151
156
  }
@@ -9,13 +9,29 @@ import { isSafeHref } from "../safe-href.ts";
9
9
 
10
10
  // Markdown/raw-HTML structure characters. Rich-text leaves are *plain text* —
11
11
  // formatting arrives as marks, never as syntax in the text — so a literal
12
- // `*`, `_`, `[`, backtick, `~`, or `<` typed in the CMS must render as
13
- // itself. Unescaped, it opened emphasis or a code span mid-paragraph, and `<`
14
- // let CMS prose inject raw HTML into the rendered page. `{` and `}` open an
15
- // expression once an entry is written as MDX (see {@link writesMdx}). CommonMark
12
+ // `*`, `_`, `[`, backtick, `~`, `^`, or `<` typed in the CMS must render as
13
+ // itself. Unescaped, it opened emphasis, a code span, or a superscript
14
+ // (`2^10 and 2^20`) mid-paragraph, and `<` let CMS prose inject raw HTML into
15
+ // the rendered page. `{` and `}` open an expression once an entry is written
16
+ // as MDX (see {@link writesMdx}), and `$$` opens math there. CommonMark
16
17
  // backslash-escapes every ASCII punctuation character, so `\*` is always the
17
18
  // literal asterisk, in `.md` and `.mdx` alike.
18
- const MARKDOWN_SPECIALS = /[\\`*_{}[\]~<]/gu;
19
+ const MARKDOWN_SPECIALS = /[\\`*_{}[\]~<$^]/gu;
20
+
21
+ // MDX reads a colon ahead of a letter or digit as a text directive (`10:30`,
22
+ // `og:image`, `pets:read`), so that colon is escaped too. One inside a bare
23
+ // URL (`http://localhost:3000`) stays as written: an escape there would end
24
+ // the autolink early.
25
+ const DIRECTIVE_COLON = /(?<!(?:[a-z][\w+.-]*:\/\/|www\.)\S*):(?=[a-z\d])/giu;
26
+
27
+ // GFM autolinks a bare `http://`, `https://`, or `www.` URL in prose, taking
28
+ // its characters verbatim up to the next space or `<`: an escape inside one
29
+ // reaches the href as a backslash (`a\_b`), so prose leaves a bare URL's
30
+ // characters as written. That includes a trailing `*`, `_`, or `~`, which
31
+ // GFM leaves out of the link: escaped, its backslash would still land inside.
32
+ // A link label is the exception — GFM links nothing inside one, so there
33
+ // every special is escaped (see `linkParts`).
34
+ const BARE_URL = /\b(?:https?:\/\/|www\.)[\w-][^\s<]*/giu;
19
35
 
20
36
  // An entity or numeric character reference (`&copy;`, `&#38;`) is decoded
21
37
  // even in plain text, so one typed in the CMS would render as the character
@@ -61,15 +77,37 @@ const escapeEsmStart = (text: string): string =>
61
77
  (keyword: string) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`
62
78
  );
63
79
 
64
- export const escapeMarkdownText = (text: string): string =>
80
+ const escapeSpecials = (text: string): string =>
81
+ text.replaceAll(MARKDOWN_SPECIALS, String.raw`\$&`);
82
+
83
+ /** The escapes that apply everywhere, once the specials are escaped. */
84
+ const escapeRest = (text: string): string =>
65
85
  escapeEsmStart(
66
86
  escapeBlockStart(
67
87
  text
68
- .replaceAll(MARKDOWN_SPECIALS, String.raw`\$&`)
88
+ .replaceAll(DIRECTIVE_COLON, String.raw`\:`)
69
89
  .replaceAll(CHARACTER_REFERENCE, String.raw`\&`)
70
90
  )
71
91
  );
72
92
 
93
+ /**
94
+ * CMS text as Markdown that renders as written, wherever it lands — a link
95
+ * label included, where GFM autolinks nothing, so every special is escaped.
96
+ */
97
+ export const escapeMarkdownText = (text: string): string =>
98
+ escapeRest(escapeSpecials(text));
99
+
100
+ /** CMS prose as {@link escapeMarkdownText}, except inside a bare URL. */
101
+ const escapeProse = (text: string): string => {
102
+ let out = "";
103
+ let last = 0;
104
+ for (const match of text.matchAll(BARE_URL)) {
105
+ out += escapeSpecials(text.slice(last, match.index)) + match[0];
106
+ last = match.index + match[0].length;
107
+ }
108
+ return escapeRest(out + escapeSpecials(text.slice(last)));
109
+ };
110
+
73
111
  /** The marks a lowerer can put on an inline run. */
74
112
  export interface InlineMarks {
75
113
  bold?: boolean;
@@ -78,6 +116,20 @@ export interface InlineMarks {
78
116
  strike?: boolean;
79
117
  }
80
118
 
119
+ /** A run of CMS text and the marks on it. */
120
+ export interface InlineRun {
121
+ marks: InlineMarks;
122
+ text: string;
123
+ }
124
+
125
+ /** Inline Markdown a lowerer rendered itself: a link, an embed, a break. */
126
+ export interface InlineMarkdown {
127
+ markdown: string;
128
+ }
129
+
130
+ /** One piece of a block's inline content, in order. */
131
+ export type InlinePart = InlineMarkdown | InlineRun;
132
+
81
133
  const BACKTICK_RUN = /`+/gu;
82
134
 
83
135
  /** The longest run of backticks in `text`, 0 when there is none. */
@@ -100,47 +152,153 @@ export const codeSpan = (code: string): string => {
100
152
  return `${fence}${pad}${code}${pad}${fence}`;
101
153
  };
102
154
 
155
+ // Two struck runs side by side meet as `~~~~`: a closer and an opener that
156
+ // GFM reads as one four-tilde run, so both print as literal tildes. A literal
157
+ // `~` is escaped outside a bare URL, so an unescaped `~~~~` outside a code
158
+ // span, a link destination, or a bare URL is exactly that seam, and dropping
159
+ // it merges the runs into one strikethrough. The other alternatives only step
160
+ // over escapes, code spans, destinations, and bare URLs so the seam is never
161
+ // matched inside them.
162
+ const SEAM_STEPS =
163
+ /\\[\s\S]|(?<ticks>`+)[\s\S]*?(?<!`)\k<ticks>(?!`)|\]\((?:<(?:\\.|[^\\<>\n])*>|(?:\\.|[^\\\s)])+)\)/u;
164
+ const STRIKE_SEAM = new RegExp(
165
+ `${SEAM_STEPS.source}|${BARE_URL.source}|~~~~`,
166
+ "giu"
167
+ );
168
+
169
+ // A heading that ends in a space and a run of `#` reads that run as the
170
+ // optional closing sequence and drops it (`## Issue #` renders "Issue").
171
+ const CLOSING_HASHES = /(?<=[ \t])#+(?=[ \t]*$)/u;
172
+
103
173
  /**
104
174
  * A block's inline text, guarded where only the whole block can tell. Runs
105
175
  * are escaped one at a time, but "import" ending one run and a space or code
106
176
  * span opening the next form an MDX `import` statement only once they are
107
- * joined, and whitespace the first runs put in front of a block turns it into
108
- * an indented code block. Apply it to the joined text of a paragraph,
109
- * heading, quote, or list item — never to code.
177
+ * joined, whitespace the first runs put in front of a block turns it into
178
+ * an indented code block, two struck runs fuse into literal tildes, and a
179
+ * trailing ` #` is a heading's closing sequence. Apply it to the joined text
180
+ * of a paragraph, heading, quote, or list item — never to code.
110
181
  */
111
182
  export const guardBlockStart = (text: string): string =>
112
- escapeEsmStart(text.replaceAll(BLOCK_INDENT, ""));
183
+ escapeEsmStart(
184
+ text
185
+ .replaceAll(BLOCK_INDENT, "")
186
+ .replaceAll(STRIKE_SEAM, (match: string) =>
187
+ match === "~~~~" ? "" : match
188
+ )
189
+ .replace(CLOSING_HASHES, String.raw`\$&`)
190
+ );
113
191
 
114
192
  const EDGE_SPACE = /^(?<lead>\s*)(?<body>[\s\S]*?)(?<trail>\s*)$/u;
115
193
 
116
194
  /**
117
- * Wrap a text run in the Markdown for its marks. Code stays verbatim inside
118
- * the backticks (an escape would render as a backslash); everything else is
119
- * escaped first. Whitespace at either edge moves outside the delimiters,
120
- * since `** bold **` is not strong emphasis but `**bold**` is — editors
121
- * routinely bold a word together with the space after it.
195
+ * `text` with `render` applied between its edge whitespace, or as it is when
196
+ * it is only whitespace.
122
197
  */
123
- export const renderInline = (text: string, marks: InlineMarks): string => {
198
+ const betweenEdges = (
199
+ text: string,
200
+ render: (body: string) => string
201
+ ): string => {
124
202
  const match = EDGE_SPACE.exec(text);
125
- const lead = match?.groups?.lead ?? "";
126
203
  const body = match?.groups?.body ?? "";
127
- const trail = match?.groups?.trail ?? "";
128
204
  if (body === "") {
129
205
  return text;
130
206
  }
131
- let out = marks.code ? codeSpan(body) : escapeMarkdownText(body);
132
- if (marks.bold) {
133
- out = `**${out}**`;
134
- }
135
- if (marks.italic) {
136
- out = `*${out}*`;
207
+ return `${match?.groups?.lead ?? ""}${render(body)}${match?.groups?.trail ?? ""}`;
208
+ };
209
+
210
+ const MARK_NAMES = ["bold", "code", "italic", "strike"] as const;
211
+
212
+ const sameMarks = (
213
+ a: InlineMarks,
214
+ b: InlineMarks,
215
+ names: readonly (keyof InlineMarks)[] = MARK_NAMES
216
+ ): boolean => names.every((mark) => Boolean(a[mark]) === Boolean(b[mark]));
217
+
218
+ /** A stretch of neighboring runs that share marks, and those marks. */
219
+ interface RunGroup {
220
+ marks: InlineMarks;
221
+ runs: InlineRun[];
222
+ }
223
+
224
+ /** Runs split into stretches of neighbors that share the `names` marks. */
225
+ const groupRuns = (
226
+ runs: InlineRun[],
227
+ names: readonly (keyof InlineMarks)[] = MARK_NAMES
228
+ ): RunGroup[] => {
229
+ const groups: RunGroup[] = [];
230
+ for (const run of runs) {
231
+ const group = groups.at(-1);
232
+ if (group && sameMarks(group.marks, run.marks, names)) {
233
+ group.runs.push(run);
234
+ } else {
235
+ groups.push({ marks: run.marks, runs: [run] });
236
+ }
137
237
  }
138
- if (marks.strike) {
139
- out = `~~${out}~~`;
238
+ return groups;
239
+ };
240
+
241
+ /**
242
+ * Consecutive text runs as Markdown. Neighbors with the same bold, italic,
243
+ * and strikethrough share one set of delimiters — rendered one run at a
244
+ * time, two bold runs met as `****` and two italic ones as `**`, which print
245
+ * as literal asterisks. Code stays verbatim inside the backticks (an escape
246
+ * would render as a backslash), and neighboring code joins into one span;
247
+ * everything else is escaped. Whitespace at either edge moves outside the
248
+ * delimiters, since `** bold **` is not strong emphasis but `**bold**` is —
249
+ * editors routinely bold a word together with the space after it.
250
+ */
251
+ const renderRuns = (
252
+ runs: InlineRun[],
253
+ escape: (text: string) => string
254
+ ): string =>
255
+ groupRuns(runs, ["bold", "italic", "strike"])
256
+ .map(({ marks, runs: shared }) => {
257
+ const inner = groupRuns(shared)
258
+ .map((same) =>
259
+ betweenEdges(same.runs.map((run) => run.text).join(""), (body) =>
260
+ same.marks.code ? codeSpan(body) : escape(body)
261
+ )
262
+ )
263
+ .join("");
264
+ return betweenEdges(inner, (body) => {
265
+ let out = body;
266
+ if (marks.bold) {
267
+ out = `**${out}**`;
268
+ }
269
+ if (marks.italic) {
270
+ out = `*${out}*`;
271
+ }
272
+ if (marks.strike) {
273
+ out = `~~${out}~~`;
274
+ }
275
+ return out;
276
+ });
277
+ })
278
+ .join("");
279
+
280
+ /** Inline parts as Markdown, each stretch of runs through {@link renderRuns}. */
281
+ const renderParts = (
282
+ parts: InlinePart[],
283
+ escape: (text: string) => string
284
+ ): string => {
285
+ let out = "";
286
+ let runs: InlineRun[] = [];
287
+ for (const part of parts) {
288
+ if ("markdown" in part) {
289
+ out += renderRuns(runs, escape) + part.markdown;
290
+ runs = [];
291
+ } else {
292
+ runs.push(part);
293
+ }
140
294
  }
141
- return `${lead}${out}${trail}`;
295
+ return out + renderRuns(runs, escape);
142
296
  };
143
297
 
298
+ /** A block's inline content as Markdown. */
299
+ export const renderInline = (parts: InlinePart[]): string =>
300
+ renderParts(parts, escapeProse);
301
+
144
302
  // A destination with whitespace, a control character, or parentheses ends
145
303
  // early in `[label](…)`; CommonMark's pointy-bracket form carries it intact.
146
304
  // oxlint-disable-next-line no-control-regex -- control characters are exactly what the bare form refuses.
@@ -167,13 +325,22 @@ export const destination = (url: string): string => {
167
325
  };
168
326
 
169
327
  /**
170
- * A Markdown link, or the label alone when the target is missing — or unsafe:
171
- * CMS content is the editor's, not the site author's, so a destination that
172
- * isn't a web, mail, or relative address (`javascript:`, `data:`) never
173
- * becomes a clickable link on the docs site (see `safe-links.ts`).
328
+ * A Markdown link over inline parts, or the parts alone when the target is
329
+ * missing — or unsafe: CMS content is the editor's, not the site author's, so
330
+ * a destination that isn't a web, mail, or relative address (`javascript:`,
331
+ * `data:`) never becomes a clickable link on the docs site (see
332
+ * `safe-links.ts`). The parts stay parts then, so their runs merge with their
333
+ * neighbors'. A label escapes a bare URL too: GFM autolinks nothing inside a
334
+ * link, so its specials would read as syntax.
174
335
  */
175
- export const renderLink = (label: string, href?: string): string =>
176
- href && isSafeHref(href) ? `[${label}](${destination(href)})` : label;
336
+ export const linkParts = (parts: InlinePart[], href?: string): InlinePart[] =>
337
+ href && isSafeHref(href)
338
+ ? [
339
+ {
340
+ markdown: `[${renderParts(parts, escapeMarkdownText)}](${destination(href)})`,
341
+ },
342
+ ]
343
+ : parts;
177
344
 
178
345
  /** The ATX prefix for a heading level, clamped to Markdown's six. */
179
346
  export const headingPrefix = (level: number): string =>