@kirchdev/duxt 0.1.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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/app/app.config.ts +13 -0
  4. package/app/app.vue +122 -0
  5. package/app/assets/css/duxt.css +555 -0
  6. package/app/assets/css/typeset.css +498 -0
  7. package/app/components/DuxtBrand.vue +53 -0
  8. package/app/components/DuxtBreadcrumb.vue +37 -0
  9. package/app/components/DuxtCodeBlock.vue +143 -0
  10. package/app/components/DuxtCopyPage.vue +117 -0
  11. package/app/components/DuxtFooter.vue +86 -0
  12. package/app/components/DuxtHeader.vue +496 -0
  13. package/app/components/DuxtJsonEditor.vue +359 -0
  14. package/app/components/DuxtLandingProse.vue +90 -0
  15. package/app/components/DuxtLandingShowcase.vue +295 -0
  16. package/app/components/DuxtLiveWindow.vue +343 -0
  17. package/app/components/DuxtLocale.vue +86 -0
  18. package/app/components/DuxtMetaList.vue +76 -0
  19. package/app/components/DuxtNavigation.vue +94 -0
  20. package/app/components/DuxtNavigationLink.vue +41 -0
  21. package/app/components/DuxtOpenApiCallbacks.vue +92 -0
  22. package/app/components/DuxtOpenApiClient.vue +1339 -0
  23. package/app/components/DuxtOpenApiMedia.vue +108 -0
  24. package/app/components/DuxtOpenApiMethod.vue +59 -0
  25. package/app/components/DuxtOpenApiParameters.vue +114 -0
  26. package/app/components/DuxtOpenApiResponses.vue +109 -0
  27. package/app/components/DuxtOpenApiSchema.vue +365 -0
  28. package/app/components/DuxtOpenApiSecurity.vue +115 -0
  29. package/app/components/DuxtPageFeedback.vue +56 -0
  30. package/app/components/DuxtPageInfo.vue +194 -0
  31. package/app/components/DuxtPageNav.vue +93 -0
  32. package/app/components/DuxtPreviewSkeleton.vue +262 -0
  33. package/app/components/DuxtProgress.vue +45 -0
  34. package/app/components/DuxtSearch.vue +271 -0
  35. package/app/components/DuxtSections.vue +65 -0
  36. package/app/components/DuxtShortcuts.vue +46 -0
  37. package/app/components/DuxtSkipLink.vue +34 -0
  38. package/app/components/DuxtToc.vue +86 -0
  39. package/app/components/DuxtTranslationBanner.vue +64 -0
  40. package/app/components/DuxtVersion.vue +168 -0
  41. package/app/components/DuxtVersionBanner.vue +109 -0
  42. package/app/components/OgImage/Duxt.satori.vue +82 -0
  43. package/app/components/content/Accordion.vue +37 -0
  44. package/app/components/content/AccordionItem.vue +46 -0
  45. package/app/components/content/Callout.vue +58 -0
  46. package/app/components/content/ChangelogGroup.vue +91 -0
  47. package/app/components/content/ChangelogReleases.vue +520 -0
  48. package/app/components/content/CodeGroup.vue +183 -0
  49. package/app/components/content/DevtoolsPanel.vue +107 -0
  50. package/app/components/content/Field.vue +50 -0
  51. package/app/components/content/FieldGroup.vue +8 -0
  52. package/app/components/content/FileTree.vue +92 -0
  53. package/app/components/content/Mermaid.vue +70 -0
  54. package/app/components/content/OpenApiOperation.vue +185 -0
  55. package/app/components/content/OpenApiOperations.vue +72 -0
  56. package/app/components/content/OpenApiOverview.vue +302 -0
  57. package/app/components/content/PackageManagers.vue +187 -0
  58. package/app/components/content/PageCards.vue +48 -0
  59. package/app/components/content/Partial.vue +65 -0
  60. package/app/components/content/Preview.vue +173 -0
  61. package/app/components/content/ProseA.vue +55 -0
  62. package/app/components/content/ProseH2.vue +53 -0
  63. package/app/components/content/ProseH3.vue +53 -0
  64. package/app/components/content/ProseH4.vue +53 -0
  65. package/app/components/content/ProseImg.vue +87 -0
  66. package/app/components/content/ProsePre.vue +58 -0
  67. package/app/components/content/ProseTable.vue +11 -0
  68. package/app/components/content/Since.vue +21 -0
  69. package/app/components/content/Steps.vue +20 -0
  70. package/app/components/content/Tab.vue +23 -0
  71. package/app/components/content/TabGroup.vue +58 -0
  72. package/app/components/ui/alert/Alert.vue +21 -0
  73. package/app/components/ui/alert/AlertDescription.vue +22 -0
  74. package/app/components/ui/alert/AlertTitle.vue +22 -0
  75. package/app/components/ui/alert/index.ts +24 -0
  76. package/app/components/ui/badge/Badge.vue +28 -0
  77. package/app/components/ui/badge/index.ts +52 -0
  78. package/app/components/ui/breadcrumb/Breadcrumb.vue +13 -0
  79. package/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
  80. package/app/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
  81. package/app/components/ui/breadcrumb/BreadcrumbLink.vue +24 -0
  82. package/app/components/ui/breadcrumb/BreadcrumbList.vue +22 -0
  83. package/app/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
  84. package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
  85. package/app/components/ui/breadcrumb/index.ts +7 -0
  86. package/app/components/ui/button/Button.vue +31 -0
  87. package/app/components/ui/button/index.ts +39 -0
  88. package/app/components/ui/card/Card.vue +22 -0
  89. package/app/components/ui/card/CardAction.vue +22 -0
  90. package/app/components/ui/card/CardContent.vue +14 -0
  91. package/app/components/ui/card/CardDescription.vue +17 -0
  92. package/app/components/ui/card/CardFooter.vue +17 -0
  93. package/app/components/ui/card/CardHeader.vue +22 -0
  94. package/app/components/ui/card/CardTitle.vue +17 -0
  95. package/app/components/ui/card/index.ts +7 -0
  96. package/app/components/ui/collapsible/Collapsible.vue +19 -0
  97. package/app/components/ui/collapsible/CollapsibleContent.vue +12 -0
  98. package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
  99. package/app/components/ui/collapsible/index.ts +3 -0
  100. package/app/components/ui/command/Command.vue +98 -0
  101. package/app/components/ui/command/CommandDialog.vue +42 -0
  102. package/app/components/ui/command/CommandEmpty.vue +31 -0
  103. package/app/components/ui/command/CommandGroup.vue +52 -0
  104. package/app/components/ui/command/CommandInput.vue +45 -0
  105. package/app/components/ui/command/CommandItem.vue +85 -0
  106. package/app/components/ui/command/CommandList.vue +32 -0
  107. package/app/components/ui/command/CommandSeparator.vue +23 -0
  108. package/app/components/ui/command/CommandShortcut.vue +19 -0
  109. package/app/components/ui/command/index.ts +29 -0
  110. package/app/components/ui/dialog/Dialog.vue +15 -0
  111. package/app/components/ui/dialog/DialogClose.vue +12 -0
  112. package/app/components/ui/dialog/DialogContent.vue +61 -0
  113. package/app/components/ui/dialog/DialogDescription.vue +25 -0
  114. package/app/components/ui/dialog/DialogFooter.vue +30 -0
  115. package/app/components/ui/dialog/DialogHeader.vue +17 -0
  116. package/app/components/ui/dialog/DialogOverlay.vue +28 -0
  117. package/app/components/ui/dialog/DialogScrollContent.vue +65 -0
  118. package/app/components/ui/dialog/DialogTitle.vue +25 -0
  119. package/app/components/ui/dialog/DialogTrigger.vue +12 -0
  120. package/app/components/ui/dialog/index.ts +10 -0
  121. package/app/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
  122. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +47 -0
  123. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +47 -0
  124. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
  125. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
  126. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +27 -0
  127. package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
  128. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +48 -0
  129. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  130. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +19 -0
  131. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  132. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +34 -0
  133. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +34 -0
  134. package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
  135. package/app/components/ui/dropdown-menu/index.ts +16 -0
  136. package/app/components/ui/input/Input.vue +35 -0
  137. package/app/components/ui/input/index.ts +1 -0
  138. package/app/components/ui/navigation-menu/NavigationMenu.vue +42 -0
  139. package/app/components/ui/navigation-menu/NavigationMenuContent.vue +35 -0
  140. package/app/components/ui/navigation-menu/NavigationMenuIndicator.vue +32 -0
  141. package/app/components/ui/navigation-menu/NavigationMenuItem.vue +23 -0
  142. package/app/components/ui/navigation-menu/NavigationMenuLink.vue +30 -0
  143. package/app/components/ui/navigation-menu/NavigationMenuList.vue +30 -0
  144. package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +31 -0
  145. package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +30 -0
  146. package/app/components/ui/navigation-menu/index.ts +14 -0
  147. package/app/components/ui/scroll-area/ScrollArea.vue +31 -0
  148. package/app/components/ui/scroll-area/ScrollBar.vue +38 -0
  149. package/app/components/ui/scroll-area/index.ts +2 -0
  150. package/app/components/ui/select/Select.vue +15 -0
  151. package/app/components/ui/select/SelectContent.vue +59 -0
  152. package/app/components/ui/select/SelectGroup.vue +19 -0
  153. package/app/components/ui/select/SelectItem.vue +42 -0
  154. package/app/components/ui/select/SelectItemText.vue +12 -0
  155. package/app/components/ui/select/SelectLabel.vue +16 -0
  156. package/app/components/ui/select/SelectScrollDownButton.vue +28 -0
  157. package/app/components/ui/select/SelectScrollUpButton.vue +28 -0
  158. package/app/components/ui/select/SelectSeparator.vue +20 -0
  159. package/app/components/ui/select/SelectTrigger.vue +32 -0
  160. package/app/components/ui/select/SelectValue.vue +12 -0
  161. package/app/components/ui/select/index.ts +11 -0
  162. package/app/components/ui/separator/Separator.vue +30 -0
  163. package/app/components/ui/separator/index.ts +1 -0
  164. package/app/components/ui/sheet/Sheet.vue +15 -0
  165. package/app/components/ui/sheet/SheetClose.vue +12 -0
  166. package/app/components/ui/sheet/SheetContent.vue +68 -0
  167. package/app/components/ui/sheet/SheetDescription.vue +23 -0
  168. package/app/components/ui/sheet/SheetFooter.vue +15 -0
  169. package/app/components/ui/sheet/SheetHeader.vue +15 -0
  170. package/app/components/ui/sheet/SheetOverlay.vue +28 -0
  171. package/app/components/ui/sheet/SheetTitle.vue +23 -0
  172. package/app/components/ui/sheet/SheetTrigger.vue +12 -0
  173. package/app/components/ui/sheet/index.ts +8 -0
  174. package/app/components/ui/sidebar/Sidebar.vue +110 -0
  175. package/app/components/ui/sidebar/SidebarContent.vue +23 -0
  176. package/app/components/ui/sidebar/SidebarFooter.vue +18 -0
  177. package/app/components/ui/sidebar/SidebarGroup.vue +18 -0
  178. package/app/components/ui/sidebar/SidebarGroupAction.vue +31 -0
  179. package/app/components/ui/sidebar/SidebarGroupContent.vue +18 -0
  180. package/app/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
  181. package/app/components/ui/sidebar/SidebarHeader.vue +18 -0
  182. package/app/components/ui/sidebar/SidebarInput.vue +19 -0
  183. package/app/components/ui/sidebar/SidebarInset.vue +23 -0
  184. package/app/components/ui/sidebar/SidebarMenu.vue +18 -0
  185. package/app/components/ui/sidebar/SidebarMenuAction.vue +42 -0
  186. package/app/components/ui/sidebar/SidebarMenuBadge.vue +28 -0
  187. package/app/components/ui/sidebar/SidebarMenuButton.vue +60 -0
  188. package/app/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
  189. package/app/components/ui/sidebar/SidebarMenuItem.vue +18 -0
  190. package/app/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
  191. package/app/components/ui/sidebar/SidebarMenuSub.vue +24 -0
  192. package/app/components/ui/sidebar/SidebarMenuSubButton.vue +43 -0
  193. package/app/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
  194. package/app/components/ui/sidebar/SidebarProvider.vue +109 -0
  195. package/app/components/ui/sidebar/SidebarRail.vue +35 -0
  196. package/app/components/ui/sidebar/SidebarSeparator.vue +19 -0
  197. package/app/components/ui/sidebar/SidebarTrigger.vue +26 -0
  198. package/app/components/ui/sidebar/index.ts +62 -0
  199. package/app/components/ui/sidebar/utils.ts +19 -0
  200. package/app/components/ui/skeleton/Skeleton.vue +17 -0
  201. package/app/components/ui/skeleton/index.ts +1 -0
  202. package/app/components/ui/sonner/Sonner.vue +67 -0
  203. package/app/components/ui/sonner/index.ts +1 -0
  204. package/app/components/ui/tabs/Tabs.vue +26 -0
  205. package/app/components/ui/tabs/TabsContent.vue +23 -0
  206. package/app/components/ui/tabs/TabsList.vue +28 -0
  207. package/app/components/ui/tabs/TabsTrigger.vue +30 -0
  208. package/app/components/ui/tabs/index.ts +4 -0
  209. package/app/components/ui/textarea/Textarea.vue +32 -0
  210. package/app/components/ui/textarea/index.ts +1 -0
  211. package/app/components/ui/tooltip/Tooltip.vue +15 -0
  212. package/app/components/ui/tooltip/TooltipContent.vue +49 -0
  213. package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
  214. package/app/components/ui/tooltip/TooltipTrigger.vue +12 -0
  215. package/app/components/ui/tooltip/index.ts +4 -0
  216. package/app/composables/useActiveHeading.ts +90 -0
  217. package/app/composables/useDuxtAnimatedHeight.ts +155 -0
  218. package/app/composables/useDuxtBreadcrumb.ts +60 -0
  219. package/app/composables/useDuxtChoice.ts +28 -0
  220. package/app/composables/useDuxtCollection.ts +89 -0
  221. package/app/composables/useDuxtConfig.ts +47 -0
  222. package/app/composables/useDuxtNavigation.ts +87 -0
  223. package/app/composables/useDuxtPageFocus.ts +30 -0
  224. package/app/composables/useDuxtPath.ts +42 -0
  225. package/app/composables/useDuxtSearch.ts +208 -0
  226. package/app/composables/useDuxtSection.ts +44 -0
  227. package/app/composables/useDuxtSectionRow.ts +49 -0
  228. package/app/composables/useDuxtShortcuts.ts +58 -0
  229. package/app/composables/useDuxtSiteUrl.ts +27 -0
  230. package/app/composables/useDuxtToast.ts +25 -0
  231. package/app/composables/useDuxtVersion.ts +138 -0
  232. package/app/composables/useFuzzySearch.ts +83 -0
  233. package/app/composables/usePackageManager.ts +4 -0
  234. package/app/composables/useRecentPages.ts +52 -0
  235. package/app/error.vue +133 -0
  236. package/app/layouts/changelog.vue +85 -0
  237. package/app/layouts/default.vue +23 -0
  238. package/app/layouts/docs.vue +62 -0
  239. package/app/layouts/reference.vue +85 -0
  240. package/app/lib/utils.ts +7 -0
  241. package/app/middleware/duxt-section-layout.global.ts +57 -0
  242. package/app/pages/[...slug].vue +441 -0
  243. package/app/pages/index.vue +479 -0
  244. package/app/types/duxt.d.ts +955 -0
  245. package/app/utils/aside.ts +16 -0
  246. package/app/utils/changelog.ts +182 -0
  247. package/app/utils/code-highlight.ts +116 -0
  248. package/app/utils/duxt-config.ts +185 -0
  249. package/app/utils/duxt-text.ts +135 -0
  250. package/app/utils/file-icons.ts +62 -0
  251. package/app/utils/generated-sections.ts +190 -0
  252. package/app/utils/generated-toc.ts +216 -0
  253. package/app/utils/heading-link.ts +32 -0
  254. package/app/utils/locale-path.ts +45 -0
  255. package/app/utils/navigation-tree.ts +206 -0
  256. package/app/utils/nearest-page.ts +72 -0
  257. package/app/utils/openapi.ts +1028 -0
  258. package/app/utils/package-command.ts +162 -0
  259. package/app/utils/request-samples.ts +635 -0
  260. package/app/utils/section-scope.ts +97 -0
  261. package/app/utils/sheet-navigation.ts +100 -0
  262. package/app/utils/source-file.ts +84 -0
  263. package/app/utils/version-choices.ts +68 -0
  264. package/app/utils/version-paths.ts +65 -0
  265. package/bin/duxt-report.mjs +45 -0
  266. package/components.json +19 -0
  267. package/content-cache.ts +193 -0
  268. package/content.config.ts +38 -0
  269. package/duxt-app-config.ts +124 -0
  270. package/highlight-langs.ts +68 -0
  271. package/i18n/i18n.config.ts +16 -0
  272. package/i18n/locales/de/duxt/changelog.json +17 -0
  273. package/i18n/locales/de/duxt/code.json +13 -0
  274. package/i18n/locales/de/duxt/defaults.json +22 -0
  275. package/i18n/locales/de/duxt/devtools.json +7 -0
  276. package/i18n/locales/de/duxt/error.json +10 -0
  277. package/i18n/locales/de/duxt/footer.json +7 -0
  278. package/i18n/locales/de/duxt/locale.json +7 -0
  279. package/i18n/locales/de/duxt/nav.json +17 -0
  280. package/i18n/locales/de/duxt/openapi.json +89 -0
  281. package/i18n/locales/de/duxt/page.json +34 -0
  282. package/i18n/locales/de/duxt/search.json +12 -0
  283. package/i18n/locales/de/duxt/shortcuts.json +11 -0
  284. package/i18n/locales/de/duxt/theme.json +7 -0
  285. package/i18n/locales/de/duxt/toc.json +7 -0
  286. package/i18n/locales/de/duxt/version.json +25 -0
  287. package/i18n/locales/en/duxt/changelog.json +17 -0
  288. package/i18n/locales/en/duxt/code.json +13 -0
  289. package/i18n/locales/en/duxt/defaults.json +22 -0
  290. package/i18n/locales/en/duxt/devtools.json +7 -0
  291. package/i18n/locales/en/duxt/error.json +10 -0
  292. package/i18n/locales/en/duxt/footer.json +7 -0
  293. package/i18n/locales/en/duxt/locale.json +7 -0
  294. package/i18n/locales/en/duxt/nav.json +17 -0
  295. package/i18n/locales/en/duxt/openapi.json +89 -0
  296. package/i18n/locales/en/duxt/page.json +34 -0
  297. package/i18n/locales/en/duxt/search.json +12 -0
  298. package/i18n/locales/en/duxt/shortcuts.json +11 -0
  299. package/i18n/locales/en/duxt/theme.json +7 -0
  300. package/i18n/locales/en/duxt/toc.json +7 -0
  301. package/i18n/locales/en/duxt/version.json +25 -0
  302. package/i18n/locales/es/duxt/changelog.json +17 -0
  303. package/i18n/locales/es/duxt/code.json +13 -0
  304. package/i18n/locales/es/duxt/defaults.json +22 -0
  305. package/i18n/locales/es/duxt/devtools.json +7 -0
  306. package/i18n/locales/es/duxt/error.json +10 -0
  307. package/i18n/locales/es/duxt/footer.json +7 -0
  308. package/i18n/locales/es/duxt/locale.json +7 -0
  309. package/i18n/locales/es/duxt/nav.json +17 -0
  310. package/i18n/locales/es/duxt/openapi.json +89 -0
  311. package/i18n/locales/es/duxt/page.json +34 -0
  312. package/i18n/locales/es/duxt/search.json +12 -0
  313. package/i18n/locales/es/duxt/shortcuts.json +11 -0
  314. package/i18n/locales/es/duxt/theme.json +7 -0
  315. package/i18n/locales/es/duxt/toc.json +7 -0
  316. package/i18n/locales/es/duxt/version.json +25 -0
  317. package/i18n/locales/fr/duxt/changelog.json +17 -0
  318. package/i18n/locales/fr/duxt/code.json +13 -0
  319. package/i18n/locales/fr/duxt/defaults.json +22 -0
  320. package/i18n/locales/fr/duxt/devtools.json +7 -0
  321. package/i18n/locales/fr/duxt/error.json +10 -0
  322. package/i18n/locales/fr/duxt/footer.json +7 -0
  323. package/i18n/locales/fr/duxt/locale.json +7 -0
  324. package/i18n/locales/fr/duxt/nav.json +17 -0
  325. package/i18n/locales/fr/duxt/openapi.json +89 -0
  326. package/i18n/locales/fr/duxt/page.json +34 -0
  327. package/i18n/locales/fr/duxt/search.json +12 -0
  328. package/i18n/locales/fr/duxt/shortcuts.json +11 -0
  329. package/i18n/locales/fr/duxt/theme.json +7 -0
  330. package/i18n/locales/fr/duxt/toc.json +7 -0
  331. package/i18n/locales/fr/duxt/version.json +25 -0
  332. package/i18n/locales/pt/duxt/changelog.json +17 -0
  333. package/i18n/locales/pt/duxt/code.json +13 -0
  334. package/i18n/locales/pt/duxt/defaults.json +22 -0
  335. package/i18n/locales/pt/duxt/devtools.json +7 -0
  336. package/i18n/locales/pt/duxt/error.json +10 -0
  337. package/i18n/locales/pt/duxt/footer.json +7 -0
  338. package/i18n/locales/pt/duxt/locale.json +7 -0
  339. package/i18n/locales/pt/duxt/nav.json +17 -0
  340. package/i18n/locales/pt/duxt/openapi.json +89 -0
  341. package/i18n/locales/pt/duxt/page.json +34 -0
  342. package/i18n/locales/pt/duxt/search.json +12 -0
  343. package/i18n/locales/pt/duxt/shortcuts.json +11 -0
  344. package/i18n/locales/pt/duxt/theme.json +7 -0
  345. package/i18n/locales/pt/duxt/toc.json +7 -0
  346. package/i18n/locales/pt/duxt/version.json +25 -0
  347. package/i18n/locales/pt-BR/duxt/error.json +8 -0
  348. package/i18n/locales/pt-BR/duxt/nav.json +10 -0
  349. package/i18n/locales/pt-BR/duxt/page.json +10 -0
  350. package/i18n/locales/pt-BR/duxt/search.json +11 -0
  351. package/i18n/locales/pt-BR/duxt/version.json +13 -0
  352. package/mdc.config.ts +28 -0
  353. package/modules/config.ts +440 -0
  354. package/modules/devtools.ts +126 -0
  355. package/modules/git-meta.ts +349 -0
  356. package/modules/redirects.ts +152 -0
  357. package/modules/validate.ts +137 -0
  358. package/nuxt.config.ts +530 -0
  359. package/openapi-model.ts +358 -0
  360. package/openapi-parse.ts +1052 -0
  361. package/package.json +161 -0
  362. package/public/devtools/cache.html +318 -0
  363. package/public/devtools/checks.html +309 -0
  364. package/public/devtools/config.html +461 -0
  365. package/public/devtools/i18n.html +309 -0
  366. package/public/devtools/pages.html +348 -0
  367. package/public/devtools/paths.html +315 -0
  368. package/public/devtools/redirects.html +309 -0
  369. package/public/devtools/search.html +324 -0
  370. package/public/devtools/sources.html +309 -0
  371. package/public/devtools/versions.html +318 -0
  372. package/report.ts +303 -0
  373. package/repository-root.ts +26 -0
  374. package/section-reports.ts +55 -0
  375. package/sections-changelog.ts +644 -0
  376. package/sections-openapi.ts +450 -0
  377. package/sections-resolve.ts +711 -0
  378. package/sections.ts +157 -0
  379. package/server/devtools/content.ts +120 -0
  380. package/server/devtools/context.ts +38 -0
  381. package/server/devtools/entry-path.ts +30 -0
  382. package/server/devtools/handler.ts +104 -0
  383. package/server/devtools/preview.ts +461 -0
  384. package/server/devtools/render/content.ts +233 -0
  385. package/server/devtools/render/sources.ts +414 -0
  386. package/server/devtools/render/system.ts +352 -0
  387. package/server/devtools/shell.ts +611 -0
  388. package/server/devtools/sources.ts +122 -0
  389. package/server/devtools/system.ts +178 -0
  390. package/server/mcp/tools/list-pages.ts +52 -0
  391. package/server/mcp/tools/read-page.ts +57 -0
  392. package/server/mcp/tools/search-docs.ts +74 -0
  393. package/server/middleware/raw-markdown.ts +86 -0
  394. package/server/routes/llms-full.txt.get.ts +75 -0
  395. package/server/routes/llms.txt.get.ts +63 -0
  396. package/server/routes/rss.xml.get.ts +125 -0
  397. package/server/utils/duxt-server-text.ts +46 -0
  398. package/sources-git.ts +113 -0
  399. package/sources-resolve.ts +908 -0
  400. package/sources.ts +287 -0
  401. package/validate-report.ts +494 -0
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { DuxtResolvedSource } from './sources-resolve';
4
+ import type { DuxtSectionTypes } from './sections-resolve';
5
+ import {
6
+ duxtSectionTypes,
7
+ missingSectionArtefact,
8
+ sectionPages
9
+ } from './sections-resolve';
10
+ import { repositoryRoot } from './repository-root';
11
+
12
+ /**
13
+ * Fill in what each LOCAL artefact had to say, for the manifest a module holds.
14
+ *
15
+ * The reports `sections.ts` produces while it declares the collections are
16
+ * written onto the manifest `content.config.ts` built, and that is not the
17
+ * manifest anything else reads:
18
+ * a config file and a Nuxt module are loaded through two different loaders, so
19
+ * the duxt modules build one of their own — see `duxtSectionTypes` for the same
20
+ * problem stated about the registry. Rather than smuggle the first manifest
21
+ * across, the artefacts are read again here: a file read and a parse, at build
22
+ * time, over a file the build has already opened once.
23
+ *
24
+ * LOCAL ONLY, and the limit is real rather than chosen. A remote artefact lives
25
+ * wherever Content's hash-cached checkout put it, which is a directory known
26
+ * only inside the collection that declared it — recomputing it here is exactly
27
+ * the second implementation `remoteCollection` refuses to write. A remote
28
+ * section therefore reaches the report with no `report` at all, and
29
+ * `sectionFindings` answers from the page count instead, saying less because
30
+ * less is known.
31
+ */
32
+ export function readSectionReports(
33
+ entries: DuxtResolvedSource[],
34
+ types: DuxtSectionTypes = duxtSectionTypes()
35
+ ): DuxtResolvedSource[] {
36
+ for (const entry of entries) {
37
+ const type = entry.generated && types[entry.generated.type];
38
+
39
+ if (!type || entry.generated!.remote) continue;
40
+
41
+ const file = join(repositoryRoot(), entry.path);
42
+
43
+ // Both calls record the report on the entry; the return value is the
44
+ // collection's business, not this one's. A missing local artefact throws
45
+ // here exactly as it throws while the collections are declared — the same
46
+ // error, from whichever of the two runs first.
47
+ if (existsSync(file)) {
48
+ sectionPages(entry, type, readFileSync(file, 'utf8'));
49
+ } else {
50
+ missingSectionArtefact(entry, file);
51
+ }
52
+ }
53
+
54
+ return entries;
55
+ }
@@ -0,0 +1,644 @@
1
+ /**
2
+ * The `changelog` type: a release log, published as pages.
3
+ *
4
+ * The first type in the registry, and the reason the registry is not
5
+ * generalisation without a user: a scaffold with nothing plugged into it is
6
+ * never touched by the build, which is where this repository's real failures
7
+ * have always shown up.
8
+ *
9
+ * What it reads is the file release-please writes — `## [0.2.0](compare) (date)`
10
+ * for a release, `### [0.2.1](compare) (date)` for a patch — and it is
11
+ * deliberately tolerant beyond that: a heading is a release when it begins with
12
+ * something that looks like a version, so a hand-kept `## 1.4.0` or a
13
+ * Keep-a-Changelog file reads too.
14
+ *
15
+ * TWO GRANULARITIES, and `split` is the default. Split turns each release into
16
+ * a page of its own — a deep link, a search hit, a feed item and an `llms.txt`
17
+ * entry per release — under a generated overview; `flat` renders the file as
18
+ * the one page it was written as, for a project that just wants it shown. The
19
+ * cost of two rendering paths was weighed and accepted, and the difference runs
20
+ * further than the page count: a flat changelog is an ORDINARY docs page and
21
+ * keeps the docs chrome, which is why `layout` is answered per declaration.
22
+ *
23
+ * Pure text in, files out. What the split history LOOKS like — the timeline,
24
+ * the group badges, the filters — belongs to `layouts/changelog.vue` and the
25
+ * two components this file writes calls to.
26
+ */
27
+ import { stringify as stringifyYaml } from 'yaml';
28
+ import type {
29
+ DuxtSectionContext,
30
+ DuxtSectionOptions,
31
+ DuxtSectionPage,
32
+ DuxtSectionType
33
+ } from './sections-resolve';
34
+ import { slugify } from './sources-resolve';
35
+
36
+ /**
37
+ * The names this type binds — PUBLIC SURFACE, so renaming one is a `feat!:`.
38
+ *
39
+ * A layout and two MDC components, because that is the whole of what a
40
+ * consumer can override: dropping a `ChangelogGroup.vue` of their own into
41
+ * `app/components/content/` replaces the layer's, exactly as it does for a
42
+ * callout, and a `layouts/changelog.vue` replaces the chrome around it.
43
+ */
44
+ export const DUXT_CHANGELOG_LAYOUT = 'changelog';
45
+ export const DUXT_CHANGELOG_RELEASES = 'changelog-releases';
46
+ export const DUXT_CHANGELOG_GROUP = 'changelog-group';
47
+
48
+ /** A heading, at any level. */
49
+ const HEADING = /^(#{1,6})[ \t]+(.+?)[ \t]*$/;
50
+
51
+ /** A fence opening or closing a code block, so a `#` inside one is text. */
52
+ const FENCE = /^\s*(```|~~~)/;
53
+
54
+ /** An inline link, reduced to the text it shows. */
55
+ const LINK = /\[([^\]]*)\]\([^)]*\)/g;
56
+
57
+ /**
58
+ * A link the release heading OPENS on — where release-please puts the compare
59
+ * URL of the release against the one before it.
60
+ *
61
+ * Anchored at the front, and only that link: a heading matches `RELEASE` once
62
+ * its links are reduced to their text, so a link standing first IS the version
63
+ * and nothing else can be. A link further along is prose about the release
64
+ * rather than a diff of it.
65
+ *
66
+ * `https?` ONLY. A relative href in a changelog resolves against the site that
67
+ * renders it rather than the repository the file came from, so it points at a
68
+ * page that does not exist — and a `javascript:` one is a script this layer
69
+ * would be putting on the page of a site that only asked for someone else's
70
+ * changelog.
71
+ */
72
+ const RELEASE_LINK = /^\[[^\]]*\]\((https?:\/\/[^)\s]+)(?:[ \t]+"[^"]*")?\)/;
73
+
74
+ /** What is left of a release heading once its link is gone: version, date. */
75
+ const RELEASE = /^(v?\d[\w.+-]*)(?:[ \t]+\((\d{4}-\d{2}-\d{2})\))?$/;
76
+
77
+ /** A list item at the top level of a group — one entry of the release. */
78
+ const ENTRY = /^(?:[-*+]|\d+[.)])[ \t]+\S/;
79
+
80
+ /** How the file is turned into pages. */
81
+ const GRANULARITIES = ['split', 'flat'] as const;
82
+
83
+ /**
84
+ * The options this type reads, and the whole of what it answers to.
85
+ *
86
+ * Named rather than ignored, for the same reason an unknown VALUE is: a
87
+ * misspelled `granularty: 'flat'` is one character from the key that works, the
88
+ * site silently gets the default instead of what it configured, and nothing
89
+ * downstream ever mentions it again. `options` is opaque to the scaffold on
90
+ * purpose — `DuxtGeneratedSection.options` carries the bag rather than reading
91
+ * it — so the type that reads a key is the only place left that can say a key
92
+ * is not one it knows.
93
+ */
94
+ const OPTIONS = ['granularity'] as const;
95
+
96
+ type Granularity = (typeof GRANULARITIES)[number];
97
+
98
+ interface Release {
99
+ /** The version exactly as the changelog wrote it. */
100
+ version: string;
101
+ /** The release date, when the heading carried one. */
102
+ date?: string;
103
+ /** The diff against the release before it, when the heading linked one. */
104
+ compare?: string;
105
+ /** The lines under the heading, up to the next release. */
106
+ body: string[];
107
+ }
108
+
109
+ /** One `###` block of a release, named by its own heading. */
110
+ interface Group {
111
+ /** The heading, VERBATIM — see `groupsOf`. */
112
+ name: string;
113
+ /** How many entries it lists, for the badge on the overview. */
114
+ count: number;
115
+ /** The lines under it. */
116
+ lines: string[];
117
+ }
118
+
119
+ export const changelogSectionType: DuxtSectionType = {
120
+ parse: parseChangelog,
121
+ /**
122
+ * ONE GLOBAL HISTORY, read from the default version.
123
+ *
124
+ * A changelog is not a per-version document that happens to mention other
125
+ * versions — it is the list OF the versions, and building one copy per
126
+ * version would publish the same file under three URLs, each of them missing
127
+ * the releases that came after it.
128
+ */
129
+ versioning: 'global',
130
+ /**
131
+ * The original, in every language.
132
+ *
133
+ * A release log is written once, by the release tool, in whatever language
134
+ * the project commits in. A localised site therefore serves the original and
135
+ * says so with the translation banner it already draws for an untranslated
136
+ * page — which is the honest answer, and needs no new component.
137
+ */
138
+ localisation: 'original',
139
+ /**
140
+ * A layout of its own for the split history, and none for the flat file.
141
+ *
142
+ * The split history is a timeline: the releases in the sidebar, a page per
143
+ * release, no table of contents over four bullet points. The flat file is a
144
+ * long ordinary page, and the docs chrome is exactly what it wants — a
145
+ * contents column listing the releases most of all. Same type, two products,
146
+ * so the question is answered from the declaration's own options.
147
+ */
148
+ layout: (options) =>
149
+ granularityOf(options) === 'split' ? DUXT_CHANGELOG_LAYOUT : undefined,
150
+ icon: 'lucide:tag'
151
+ };
152
+
153
+ /**
154
+ * The granularity this declaration asked for, and the declaration checked.
155
+ *
156
+ * Named rather than ignored, for the reason an unknown type is: a misspelled
157
+ * `granularity: 'splitt'` that silently means `split` is a site quietly not
158
+ * getting what it configured, and this build is the last place that can say so.
159
+ * A misspelled KEY is that same failure one character away, and is named in the
160
+ * same words — see `OPTIONS`.
161
+ */
162
+ function granularityOf(options: DuxtSectionOptions): Granularity {
163
+ // Here rather than beside the caller because this is the ONE place the type
164
+ // reads its options at all: both the layout question and the parser come
165
+ // through it, so a section that is never built still names the typo.
166
+ const unknown = Object.keys(options).find(
167
+ (key) => !OPTIONS.includes(key as (typeof OPTIONS)[number])
168
+ );
169
+
170
+ if (unknown) {
171
+ throw new Error(
172
+ `duxt: a changelog section names the option "${unknown}", which is not ` +
173
+ `one of ${OPTIONS.map((name) => `"${name}"`).join(' or ')}.`
174
+ );
175
+ }
176
+
177
+ const value = options.granularity ?? 'split';
178
+
179
+ if (!GRANULARITIES.includes(value as Granularity)) {
180
+ throw new Error(
181
+ `duxt: a changelog section asks for the granularity ` +
182
+ `"${String(value)}", which is not one of ` +
183
+ `${GRANULARITIES.map((name) => `"${name}"`).join(' or ')}.`
184
+ );
185
+ }
186
+
187
+ return value as Granularity;
188
+ }
189
+
190
+ function parseChangelog(
191
+ artefact: string,
192
+ context: DuxtSectionContext
193
+ ): DuxtSectionPage[] {
194
+ const lines = artefact.split(/\r?\n/);
195
+
196
+ if (granularityOf(context.options) === 'flat') return [flat(lines, context)];
197
+
198
+ const headings = headingsOf(lines);
199
+
200
+ const starts = headings.filter((heading) => release(heading.text));
201
+
202
+ reportNearMisses(headings, starts, context);
203
+ const releases: Release[] = starts.map((heading, index) => {
204
+ const parsed = release(heading.text)!;
205
+ const end = starts[index + 1]?.line ?? lines.length;
206
+
207
+ return {
208
+ version: parsed.version,
209
+ date: parsed.date,
210
+ compare: parsed.compare,
211
+ body: lines.slice(heading.line + 1, end)
212
+ };
213
+ });
214
+
215
+ const preamble = lines.slice(0, starts[0]?.line ?? lines.length);
216
+
217
+ // Zero-padded to the number of releases, so ten of them still sort the way
218
+ // they read. Content strips the prefix from the URL, which is what makes an
219
+ // ordering scheme affordable at all — see `DuxtSectionPage.file`.
220
+ const width = String(releases.length).length;
221
+
222
+ return [
223
+ index(preamble, releases, context),
224
+ ...releases.map((entry, position) =>
225
+ page(entry, String(position + 1).padStart(width, '0'))
226
+ )
227
+ ];
228
+ }
229
+
230
+ /**
231
+ * The file as it stands, as one page.
232
+ *
233
+ * Nothing is rewritten but the title: the file's own `# Changelog` goes,
234
+ * because the docs page draws its heading from `title` and a second `<h1>` in
235
+ * the body is both a duplicate and an axe finding. Every release heading stays
236
+ * exactly where the release tool put it — which is what "unchanged" has to
237
+ * mean, or the mode is not the escape hatch it exists to be.
238
+ */
239
+ function flat(lines: string[], context: DuxtSectionContext): DuxtSectionPage {
240
+ return {
241
+ file: 'index.md',
242
+ body: [
243
+ frontmatter({ title: context.label }),
244
+ '',
245
+ ...trim(withoutTitle(lines)),
246
+ ''
247
+ ].join('\n')
248
+ };
249
+ }
250
+
251
+ /** Every heading outside a fenced code block, with the line it sits on. */
252
+ function headingsOf(
253
+ lines: string[]
254
+ ): { line: number; level: number; text: string }[] {
255
+ const headings: { line: number; level: number; text: string }[] = [];
256
+
257
+ scan(lines, (line, number, fenced) => {
258
+ if (fenced) return;
259
+
260
+ const match = HEADING.exec(line);
261
+ if (match) {
262
+ headings.push({
263
+ line: number,
264
+ level: match[1]!.length,
265
+ text: match[2]!
266
+ });
267
+ }
268
+ });
269
+
270
+ return headings;
271
+ }
272
+
273
+ /**
274
+ * Every line, with whether it sits inside a fenced block.
275
+ *
276
+ * One walk, three readers: the heading list, the entry count and the promotion
277
+ * pass all have to agree about what is code and what is content, and three
278
+ * copies of the fence rule is three places for them to stop agreeing. A fence
279
+ * closes only on its own kind, so a ``` inside a ~~~ block is content rather
280
+ * than the end of it.
281
+ */
282
+ function scan(
283
+ lines: string[],
284
+ visit: (line: string, number: number, fenced: boolean) => void
285
+ ): void {
286
+ let fence: string | undefined;
287
+
288
+ lines.forEach((line, number) => {
289
+ const opened = FENCE.exec(line);
290
+
291
+ if (opened) {
292
+ if (!fence) fence = opened[1];
293
+ else if (fence === opened[1]) fence = undefined;
294
+
295
+ visit(line, number, true);
296
+ return;
297
+ }
298
+
299
+ visit(line, number, Boolean(fence));
300
+ });
301
+ }
302
+
303
+ /** Is this heading a release, and which one? */
304
+ /**
305
+ * A heading that reads like a release but was not taken for one.
306
+ *
307
+ * The parser is deliberately tolerant, and tolerance has a silent edge: a
308
+ * heading `RELEASE` does not match is not an error, it is PROSE — its lines are
309
+ * folded into the release above it, or into the preamble, and the release it
310
+ * was meant to be never becomes a page. Nothing said so. This says so.
311
+ *
312
+ * The test is a dotted number at the front, so `## 1.4 (Feb 2024)` and the
313
+ * Keep a Changelog form `## [1.2.3] - 2024-01-01` — which carries no link and
314
+ * therefore survives `LINK` with its brackets on — are reported, while a group
315
+ * heading like `### 3 breaking changes` is not. It is a warning rather than a
316
+ * throw for the reason every other content finding is one: the site renders,
317
+ * and a changelog this layer did not write is not the build's to reject.
318
+ */
319
+ const NEAR_RELEASE = /^\[?v?\d+\.\d/;
320
+
321
+ function reportNearMisses(
322
+ headings: { line: number; text: string }[],
323
+ starts: { line: number }[],
324
+ context: DuxtSectionContext
325
+ ): void {
326
+ if (!context.warn) return;
327
+
328
+ const taken = new Set(starts.map((heading) => heading.line));
329
+
330
+ for (const heading of headings) {
331
+ if (taken.has(heading.line)) continue;
332
+
333
+ const text = heading.text.replace(LINK, '$1').trim();
334
+ if (!NEAR_RELEASE.test(text)) continue;
335
+
336
+ context.warn(
337
+ `the heading "${heading.text}" on line ${heading.line + 1} reads like a ` +
338
+ 'release but does not parse as one, so it became part of the release ' +
339
+ 'above it instead of a page of its own. A release heading is a version, ' +
340
+ 'optionally followed by a date in parentheses: `## 1.4.0 (2024-02-01)`.'
341
+ );
342
+ }
343
+ }
344
+
345
+ function release(
346
+ text: string
347
+ ): { version: string; date?: string; compare?: string } | undefined {
348
+ const trimmed = text.trim();
349
+
350
+ const match = RELEASE.exec(trimmed.replace(LINK, '$1').trim());
351
+ if (!match) return undefined;
352
+
353
+ return {
354
+ version: match[1]!,
355
+ date: match[2],
356
+ compare: RELEASE_LINK.exec(trimmed)?.[1]
357
+ };
358
+ }
359
+
360
+ /**
361
+ * A release body, split into the groups its own headings name.
362
+ *
363
+ * THE GROUPS ARE THE FILE'S HEADINGS, TAKEN VERBATIM. No fixed taxonomy and no
364
+ * mapping: release-please writes "Features" and "Bug Fixes", changesets and
365
+ * Keep a Changelog write their own, and every one of them writes them in the
366
+ * language the project is kept in. A hard-wired list would fail silently on the
367
+ * first heading it did not know — which is the failure mode this repository
368
+ * least wants, and the reason the filters on the overview are built from
369
+ * whatever came out of the file.
370
+ *
371
+ * The LEVEL comes off the body rather than being assumed: release-please writes
372
+ * a patch release at `###` and its groups at `###` as well, so "the shallowest
373
+ * heading in this release" is the only rule that reads both.
374
+ */
375
+ function groupsOf(lines: string[]): { intro: string[]; groups: Group[] } {
376
+ const headings = headingsOf(lines);
377
+ if (!headings.length) return { intro: lines, groups: [] };
378
+
379
+ const level = Math.min(...headings.map((heading) => heading.level));
380
+ const starts = headings.filter((heading) => heading.level === level);
381
+
382
+ return {
383
+ intro: trim(lines.slice(0, starts[0]!.line)),
384
+ groups: starts.map((heading, index) => {
385
+ const end = starts[index + 1]?.line ?? lines.length;
386
+ const body = trim(lines.slice(heading.line + 1, end));
387
+
388
+ return {
389
+ name: heading.text.trim(),
390
+ count: entries(body),
391
+ lines: body
392
+ };
393
+ })
394
+ };
395
+ }
396
+
397
+ /** How many entries a group lists — its top-level list items. */
398
+ function entries(lines: string[]): number {
399
+ let count = 0;
400
+
401
+ scan(lines, (line, _number, fenced) => {
402
+ if (!fenced && ENTRY.test(line)) count += 1;
403
+ });
404
+
405
+ return count;
406
+ }
407
+
408
+ /**
409
+ * The section's own page: the preamble, then the releases as a timeline.
410
+ *
411
+ * The release LIST travels as props rather than as a Markdown list, the way
412
+ * `sections-openapi.ts` hands its tags over: what the component needs is the
413
+ * date and the groups beside each version, and a bullet list carrying that
414
+ * would be a data structure spelled as prose. The entries themselves stay on
415
+ * the release pages — repeating them here would put the whole changelog twice
416
+ * into the search index, `llms-full.txt` and the feed.
417
+ */
418
+ function index(
419
+ preamble: string[],
420
+ releases: Release[],
421
+ context: DuxtSectionContext
422
+ ): DuxtSectionPage {
423
+ const body = trim(withoutTitle(preamble));
424
+
425
+ const props = {
426
+ releases: releases.map((entry) => ({
427
+ version: entry.version,
428
+ date: entry.date,
429
+ to: `${context.prefix}/${segment(entry.version)}`,
430
+ groups: groupsOf(trim(entry.body)).groups.map((group) => ({
431
+ name: group.name,
432
+ count: group.count
433
+ }))
434
+ }))
435
+ };
436
+
437
+ return {
438
+ file: 'index.md',
439
+ body: [
440
+ frontmatter({ title: context.label }),
441
+ '',
442
+ // NO `<h1>` OF ITS OWN. The page draws the docs header — breadcrumb,
443
+ // title, description, the copy control beside it — for exactly the
444
+ // generated pages whose body opens on no heading, and a release history
445
+ // is one: its pages have a title and a trail like any other page, and the
446
+ // only reason they ever drew their own was that the layout drew none.
447
+ ...(body.length ? [...body, ''] : []),
448
+ ...(releases.length
449
+ ? [component(DUXT_CHANGELOG_RELEASES, props), '']
450
+ : [])
451
+ ].join('\n')
452
+ };
453
+ }
454
+
455
+ /**
456
+ * One release, as a page: its groups, and in the frontmatter what the release
457
+ * IS.
458
+ *
459
+ * `date` and `compare` are FRONTMATTER rather than a row the body opens on, and
460
+ * that is the difference between a release page and a dashboard: a release note
461
+ * is Markdown and should read as the rest of the site's Markdown does. Both
462
+ * facts are provenance — when this was cut, and what went into it — so they
463
+ * belong where the site already answers that, in the column beside the contents
464
+ * where "Edit this page" and the last commit sit. `DuxtPageInfo` draws them.
465
+ */
466
+ function page(entry: Release, order: string): DuxtSectionPage {
467
+ const { intro, groups } = groupsOf(trim(entry.body));
468
+
469
+ return {
470
+ file: `${order}.${segment(entry.version)}.md`,
471
+ body: [
472
+ frontmatter({
473
+ title: entry.version,
474
+ date: entry.date,
475
+ compare: entry.compare
476
+ }),
477
+ '',
478
+ // The version is the page's title, and the page draws it — see `index`.
479
+ ...(intro.length ? [...promote(intro), ''] : []),
480
+ ...groups.flatMap((group) => [
481
+ // The entries stay MARKDOWN, in the component's slot: they are the
482
+ // release, and what a reader searches for, what `llms-full.txt` carries
483
+ // and what the copy button hands a model is prose either way. Only the
484
+ // name and the count — which the badge and the filters need as data —
485
+ // travel as props.
486
+ component(
487
+ DUXT_CHANGELOG_GROUP,
488
+ { name: group.name, count: group.count },
489
+ promote(group.lines).join('\n')
490
+ ),
491
+ ''
492
+ ])
493
+ ].join('\n')
494
+ };
495
+ }
496
+
497
+ /**
498
+ * The URL segment one release is served at.
499
+ *
500
+ * A `v` is added to a bare number, and that is not decoration: Content reads a
501
+ * file name made only of digits and dots as a version and stops refining it —
502
+ * which would leave the `NN.` ordering prefix in the URL, so `01.0.2.0.md`
503
+ * would serve `/releases/01.0.2.0`. With the `v` the prefix is stripped as it
504
+ * is everywhere else in this repository, and the segment matches the tag the
505
+ * release was cut as (`include-v-in-tag`).
506
+ */
507
+ function segment(version: string): string {
508
+ return slugify(/^\d/.test(version) ? `v${version}` : version);
509
+ }
510
+
511
+ /**
512
+ * Every heading below the second level, one level up.
513
+ *
514
+ * A release body starts at `###` — release-please puts "Features" and "Bug
515
+ * Fixes" there, under the `##` the release itself occupies. On a page of its
516
+ * own that release is the `<h1>`, and the groups it was split into are the
517
+ * `<h2>`s their component draws — so what was written under a group has to come
518
+ * up one level too, or it skips one: a broken outline for a screen reader, and
519
+ * an axe `heading-order` failure on the built page.
520
+ */
521
+ function promote(lines: string[]): string[] {
522
+ const promoted: string[] = [];
523
+
524
+ scan(lines, (line, _number, fenced) => {
525
+ const match = fenced ? undefined : HEADING.exec(line);
526
+
527
+ promoted.push(
528
+ match && match[1]!.length >= 3
529
+ ? `${'#'.repeat(match[1]!.length - 1)} ${match[2]}`
530
+ : line
531
+ );
532
+ });
533
+
534
+ return promoted;
535
+ }
536
+
537
+ /**
538
+ * The lines without the file's own title.
539
+ *
540
+ * A page draws its `<h1>` from `title`, so the `# Changelog` at the top of the
541
+ * file is a second one — in the docs shell and in a layout of its own alike.
542
+ *
543
+ * THE FIRST ONE ONLY, and only ahead of the first release. Dropping every `#`
544
+ * reads correctly on the file release-please writes, where there is exactly
545
+ * one — and is silent content loss on a hand-kept changelog that puts its
546
+ * releases at the top level: every release heading would go, and `flat`, the
547
+ * mode that exists to render the file untouched, would print one unbroken run
548
+ * of bullets with no release boundaries in it. `split` reads that same file
549
+ * release by release, which is what makes the general rule an asymmetry rather
550
+ * than a policy.
551
+ */
552
+ function withoutTitle(lines: string[]): string[] {
553
+ const title = titleOf(lines);
554
+ if (title === undefined) return lines;
555
+
556
+ return [...lines.slice(0, title), ...lines.slice(title + 1)];
557
+ }
558
+
559
+ /**
560
+ * The line the file's own title sits on, where it has one.
561
+ *
562
+ * A heading is the title when it is the first `#` in the file and no release
563
+ * came before it — a file that opens on a release has no title to take, and
564
+ * `index()` reads the same rule scoped to the preamble it is handed, where
565
+ * "before the first release" is the whole of what it holds. A heading inside a
566
+ * fenced block is text, which `headingsOf` already settles.
567
+ */
568
+ function titleOf(lines: string[]): number | undefined {
569
+ for (const heading of headingsOf(lines)) {
570
+ if (release(heading.text)) return undefined;
571
+ if (heading.level === 1) return heading.line;
572
+ }
573
+
574
+ return undefined;
575
+ }
576
+
577
+ /** The lines with the blank ones at either end dropped. */
578
+ function trim(lines: string[]): string[] {
579
+ let start = 0;
580
+ let end = lines.length;
581
+
582
+ while (start < end && !lines[start]!.trim()) start += 1;
583
+ while (end > start && !lines[end - 1]!.trim()) end -= 1;
584
+
585
+ return lines.slice(start, end);
586
+ }
587
+
588
+ /**
589
+ * An MDC block component with YAML props and Markdown inside it.
590
+ *
591
+ * The fence LENGTH is computed rather than fixed, exactly as in
592
+ * `sections-openapi.ts`: a release note containing a `::callout` would
593
+ * otherwise close the component early and spill its props into the page.
594
+ *
595
+ * EVERY STRING IS QUOTED, and that is the same defence the frontmatter helper
596
+ * below makes for the same reason one level up. A plain `2026-02-01` is a
597
+ * timestamp under the schema the props are read back with, so an unquoted date
598
+ * reaches the component as a `Date` and `<time :datetime>` prints the reader's
599
+ * own timezone rather than the day the release was cut.
600
+ */
601
+ function component(
602
+ name: string,
603
+ props: Record<string, unknown>,
604
+ slot?: string
605
+ ): string {
606
+ const body = slot?.trim() ?? '';
607
+
608
+ const longest = Math.max(
609
+ 2,
610
+ ...body.split('\n').map((line) => /^\s*(:+)/.exec(line)?.[1]?.length ?? 0)
611
+ );
612
+
613
+ const fence = ':'.repeat(Math.max(3, longest + 1));
614
+
615
+ return [
616
+ `${fence}${name}`,
617
+ '---',
618
+ stringifyYaml(props, {
619
+ defaultStringType: 'QUOTE_DOUBLE',
620
+ defaultKeyType: 'PLAIN',
621
+ lineWidth: 0
622
+ }).trimEnd(),
623
+ '---',
624
+ ...(body ? [body] : []),
625
+ fence
626
+ ].join('\n');
627
+ }
628
+
629
+ /**
630
+ * A frontmatter block YAML can read back.
631
+ *
632
+ * Every value is written as a JSON string, which is also a YAML double-quoted
633
+ * scalar — so a release title carrying a colon cannot end the mapping early,
634
+ * which is the exact failure `tests/frontmatter-yaml.test.ts` exists over.
635
+ */
636
+ function frontmatter(fields: Record<string, string | undefined>): string {
637
+ return [
638
+ '---',
639
+ ...Object.entries(fields)
640
+ .filter(([, value]) => value !== undefined)
641
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`),
642
+ '---'
643
+ ].join('\n');
644
+ }