@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,461 @@
1
+ import type { DuxtSource } from '../../sources-resolve';
2
+ import { duxtManifest } from '../../sections-resolve';
3
+ import type { PageRecord } from '../../validate-report';
4
+ import { page, PANELS } from './shell';
5
+ import type { Doc, DocGroup, IndexedSection } from './render/content';
6
+ import { renderChecks, renderPages, renderSearch } from './render/content';
7
+ import { renderPaths, renderSources, renderVersions } from './render/sources';
8
+ import type { RedirectRule } from './render/system';
9
+ import {
10
+ renderCache,
11
+ renderConfig,
12
+ renderI18n,
13
+ renderRedirects
14
+ } from './render/system';
15
+
16
+ /**
17
+ * Every panel, rendered from a site that does not exist.
18
+ *
19
+ * The documentation has to show what a panel looks like, and the two obvious
20
+ * ways are both bad: a screenshot goes stale the day a column is added, and
21
+ * pointing the reader at the real route only works inside a dev server — the
22
+ * tab is not registered in a build at all, so a published page would frame a
23
+ * 404.
24
+ *
25
+ * So the panels are rendered here through the SAME functions the tab uses,
26
+ * over a fixture site. What the reader sees in the documentation is the real
27
+ * renderer's output; when a panel gains a column, the pictures in the docs
28
+ * gain it in the same commit, because there are no pictures.
29
+ *
30
+ * The fixture is one imagined site, not ten unrelated tables: `acme/sdk` in two
31
+ * versions and two languages, `acme/cli` in one — which is the smallest shape
32
+ * that makes every panel say something. A single-source site would render a
33
+ * Versions tab with nothing in it.
34
+ */
35
+
36
+ const SOURCES: DuxtSource[] = [
37
+ {
38
+ path: 'docs',
39
+ repo: 'acme/sdk',
40
+ refs: [
41
+ { branch: 'main', label: 'v2' },
42
+ { tag: 'v1.9.4', label: 'v1.9', status: 'deprecated' }
43
+ ],
44
+ locales: ['en', 'de'],
45
+ history: true,
46
+ // A generated section, because a fixture without one shows a Sources table
47
+ // that cannot be told apart from the one this layer had before sections
48
+ // existed — and a Checks panel that never demonstrates the findings an
49
+ // artefact produces.
50
+ generated: [{ type: 'changelog', path: 'CHANGELOG.md', label: 'Releases' }]
51
+ },
52
+ { path: 'docs', repo: 'acme/cli' }
53
+ ];
54
+
55
+ const RESOLVED = duxtManifest(SOURCES, { defaultLocale: 'en' });
56
+
57
+ const ROOT = '/srv/acme-docs';
58
+
59
+ /** The layer's own config, as this fixture site would have written it. */
60
+ const CONSUMER = {
61
+ title: 'Acme SDK',
62
+ version: 'v2.4.0',
63
+ sources: SOURCES,
64
+ resolvedSources: RESOLVED,
65
+ landing: {
66
+ headline: {
67
+ en: 'Ship the SDK, not the glue',
68
+ de: 'Weniger Kleber, mehr SDK'
69
+ },
70
+ command: 'npm i @acme/sdk'
71
+ },
72
+ footer: { note: '© Acme Inc.' }
73
+ } as unknown as Record<string, unknown>;
74
+
75
+ /* -------------------------------------------------------------------------- */
76
+ /* The pages the fixture site serves */
77
+ /* -------------------------------------------------------------------------- */
78
+
79
+ interface PageSeed {
80
+ path: string;
81
+ title?: string;
82
+ german?: string;
83
+ description?: boolean;
84
+ icon?: string;
85
+ file: string;
86
+ updated?: string;
87
+ contributors?: number;
88
+ }
89
+
90
+ const SDK_PAGES: PageSeed[] = [
91
+ {
92
+ path: '/',
93
+ title: 'Acme SDK',
94
+ german: 'Acme SDK',
95
+ description: true,
96
+ icon: 'lucide:book-open-text',
97
+ file: 'index.md',
98
+ updated: '2026-02-11',
99
+ contributors: 6
100
+ },
101
+ {
102
+ path: '/getting-started',
103
+ title: 'Getting started',
104
+ german: 'Erste Schritte',
105
+ description: true,
106
+ icon: 'lucide:rocket',
107
+ file: '1.getting-started/index.md',
108
+ updated: '2026-02-09',
109
+ contributors: 4
110
+ },
111
+ {
112
+ path: '/getting-started/install',
113
+ title: 'Install',
114
+ german: 'Installation',
115
+ description: true,
116
+ file: '1.getting-started/1.install.md',
117
+ updated: '2026-01-28',
118
+ contributors: 3
119
+ },
120
+ {
121
+ path: '/guides/deploying',
122
+ title: 'Deploying',
123
+ german: 'Deployen',
124
+ description: true,
125
+ icon: 'lucide:ship',
126
+ file: '2.guides/1.deploying.md',
127
+ updated: '2026-02-10',
128
+ contributors: 2
129
+ },
130
+ {
131
+ path: '/guides/caching',
132
+ title: 'Caching',
133
+ description: true,
134
+ file: '2.guides/2.caching.md',
135
+ updated: '2026-02-12',
136
+ contributors: 1
137
+ },
138
+ {
139
+ path: '/reference/client',
140
+ title: 'Client',
141
+ german: 'Client',
142
+ description: true,
143
+ icon: 'lucide:plug',
144
+ file: '3.reference/1.client.md',
145
+ updated: '2026-02-04',
146
+ contributors: 5
147
+ },
148
+ {
149
+ // The row every Pages panel is opened for: a page that made it into the
150
+ // build with no frontmatter to speak of.
151
+ path: '/reference/errors',
152
+ file: '3.reference/2.errors.md',
153
+ updated: '2025-12-19',
154
+ contributors: 1
155
+ },
156
+ {
157
+ path: '/changelog',
158
+ title: 'Changelog',
159
+ description: true,
160
+ icon: 'lucide:history',
161
+ file: '4.changelog.md',
162
+ updated: '2026-02-12',
163
+ contributors: 8
164
+ }
165
+ ];
166
+
167
+ const CLI_PAGES: PageSeed[] = [
168
+ {
169
+ path: '/',
170
+ title: 'Acme CLI',
171
+ description: true,
172
+ icon: 'lucide:terminal',
173
+ file: 'index.md'
174
+ },
175
+ {
176
+ path: '/getting-started',
177
+ title: 'Getting started',
178
+ description: true,
179
+ file: '1.getting-started.md'
180
+ },
181
+ {
182
+ path: '/commands',
183
+ title: 'Commands',
184
+ description: true,
185
+ icon: 'lucide:list',
186
+ file: '2.commands/index.md'
187
+ },
188
+ {
189
+ path: '/commands/run',
190
+ title: 'acme run',
191
+ description: true,
192
+ file: '2.commands/1.run.md'
193
+ }
194
+ ];
195
+
196
+ /**
197
+ * Which logical page each collection is missing.
198
+ *
199
+ * The gaps are the point. A translation lagging two pages behind and a version
200
+ * that predates a guide are exactly what the Versions matrix exists to show,
201
+ * and a fixture where every column is a full house would show nothing at all.
202
+ */
203
+ const MISSING: Record<string, string[]> = {
204
+ docs_de_sdk: ['/guides/caching', '/changelog'],
205
+ docs_sdk_v1_9: ['/guides/caching'],
206
+ docs_de_sdk_v1_9: ['/guides/caching', '/changelog', '/reference/errors']
207
+ };
208
+
209
+ /**
210
+ * The pages a changelog becomes: an overview, then one release each.
211
+ *
212
+ * Declared beside the docs seeds rather than inside them because they are not
213
+ * a docs tree — no `description`, no history of their own, and a name this
214
+ * layer invented rather than one anybody wrote.
215
+ */
216
+ const RELEASES: { path: string; title: string; file: string }[] = [
217
+ { path: '', title: 'Releases', file: 'index.md' },
218
+ { path: '/0.2.0', title: '0.2.0', file: '1.0.2.0.md' },
219
+ { path: '/0.1.1', title: '0.1.1', file: '2.0.1.1.md' },
220
+ { path: '/0.1.0', title: '0.1.0', file: '3.0.1.0.md' }
221
+ ];
222
+
223
+ // What reading that CHANGELOG said about it. A near miss rather than a clean
224
+ // read, for the same reason the docs tree has a stale link in it: the panel is
225
+ // documented by what it looks like when something is wrong.
226
+ for (const source of RESOLVED) {
227
+ if (!source.generated) continue;
228
+
229
+ source.generated.report = {
230
+ pages: RELEASES.length,
231
+ warnings: [
232
+ 'the heading "1.3 (Feb 2026)" on line 41 reads like a release but does ' +
233
+ 'not parse as one, so it became part of the release above it instead ' +
234
+ 'of a page of its own. A release heading is a version, optionally ' +
235
+ 'followed by a date in parentheses: `## 1.4.0 (2024-02-01)`.'
236
+ ]
237
+ };
238
+ }
239
+
240
+ function docsOf(source: DuxtResolvedSource): Doc[] {
241
+ if (source.generated) {
242
+ return RELEASES.map((release) => ({
243
+ path: `${source.prefix}${release.path}`,
244
+ title: release.title,
245
+ id: `${source.collection}/${release.file}`
246
+ }));
247
+ }
248
+
249
+ const seeds = source.repo === 'cli' ? CLI_PAGES : SDK_PAGES;
250
+ const skip = new Set(MISSING[source.collection] ?? []);
251
+ const german = source.locale === 'de';
252
+
253
+ return seeds
254
+ .filter((seed) => !skip.has(seed.path))
255
+ .map((seed) => ({
256
+ path: `${source.prefix}${seed.path === '/' ? '' : seed.path}` || '/',
257
+ title: german ? seed.german : seed.title,
258
+ description: seed.description
259
+ ? 'One line, for the card and the OG image.'
260
+ : undefined,
261
+ icon: seed.icon,
262
+ id: `${source.collection}/${german ? 'de/' : ''}${seed.file}`,
263
+ lastUpdated:
264
+ source.history && seed.updated
265
+ ? `${seed.updated}T09:12:00Z`
266
+ : undefined,
267
+ contributors: Array.from(
268
+ { length: seed.contributors ?? 0 },
269
+ (_, index) => ({
270
+ name: `Contributor ${index + 1}`
271
+ })
272
+ )
273
+ }));
274
+ }
275
+
276
+ const GROUPS: DocGroup[] = RESOLVED.map((source) => ({
277
+ collection: source.collection,
278
+ docs: docsOf(source)
279
+ }));
280
+
281
+ const BY_COLLECTION = new Map(
282
+ GROUPS.map((group) => [group.collection, group.docs.map((doc) => doc.path)])
283
+ );
284
+
285
+ /**
286
+ * The same pages as the validator sees them.
287
+ *
288
+ * Two deliberate faults, because a Checks panel with nothing in it teaches
289
+ * nobody what a finding looks like: one link to a page that was renamed, and
290
+ * one page with no frontmatter.
291
+ */
292
+ const RECORDS: PageRecord[] = GROUPS.flatMap((group) =>
293
+ group.docs.map((doc) => ({
294
+ collection: group.collection,
295
+ path: doc.path,
296
+ file: doc.id ?? doc.path,
297
+ title: doc.title,
298
+ description: doc.description,
299
+ anchors: new Set(['install', 'usage']),
300
+ links:
301
+ doc.path.endsWith('/getting-started') && group.collection === 'docs_sdk'
302
+ ? [{ href: '/guides/cacheing' }, { href: '/guides/deploying#install' }]
303
+ : []
304
+ }))
305
+ );
306
+
307
+ const SECTIONS: IndexedSection[] = [
308
+ {
309
+ collection: 'docs_sdk',
310
+ id: 'docs_sdk/1.getting-started/1.install.md#install',
311
+ title: 'Install',
312
+ titles: ['Getting started'],
313
+ content:
314
+ 'Add the package with your package manager of choice. The SDK ships types, so nothing else is needed to get autocompletion in an editor.'
315
+ },
316
+ {
317
+ collection: 'docs_sdk',
318
+ id: 'docs_sdk/2.guides/1.deploying.md#build-output',
319
+ title: 'Build output',
320
+ titles: ['Deploying'],
321
+ content:
322
+ 'The build writes a single server bundle and a public directory. Both are what a host needs; nothing else in the repository is read at runtime.'
323
+ },
324
+ {
325
+ collection: 'docs_sdk',
326
+ id: 'docs_sdk/2.guides/2.caching.md#invalidation',
327
+ title: 'Invalidation',
328
+ titles: ['Guides', 'Caching'],
329
+ content:
330
+ 'A cached response is keyed by the request path and the account tier, so a change of plan is visible without a purge.'
331
+ },
332
+ {
333
+ collection: 'docs_de_sdk',
334
+ id: 'docs_de_sdk/de/1.getting-started/1.install.md#installation',
335
+ title: 'Installation',
336
+ titles: ['Erste Schritte'],
337
+ content:
338
+ 'Das Paket wird mit dem Paketmanager der Wahl hinzugefügt. Typen liegen bei, mehr braucht ein Editor nicht.'
339
+ },
340
+ {
341
+ collection: 'docs_cli',
342
+ id: 'docs_cli/2.commands/1.run.md#flags',
343
+ title: 'Flags',
344
+ titles: ['Commands', 'acme run'],
345
+ content:
346
+ 'Every flag has a long form. The short forms exist for the three that are typed most often and are listed beside them.'
347
+ }
348
+ ];
349
+
350
+ const LOCALE_KEYS = [
351
+ 'duxt.nav.open',
352
+ 'duxt.nav.close',
353
+ 'duxt.search.placeholder',
354
+ 'duxt.search.empty',
355
+ 'duxt.toc.title',
356
+ 'duxt.page.edit',
357
+ 'duxt.page.updated',
358
+ 'duxt.version.switch'
359
+ ];
360
+
361
+ const CACHE = [
362
+ {
363
+ name: 'github.com-acme-sdk-main',
364
+ size: 4.6 * 1024 * 1024,
365
+ modified: new Date('2026-02-12T08:41:00Z')
366
+ },
367
+ {
368
+ name: 'github.com-acme-sdk-v1.9.4',
369
+ size: 3.9 * 1024 * 1024,
370
+ modified: new Date('2026-01-30T17:03:00Z')
371
+ },
372
+ {
373
+ name: 'github.com-acme-cli-main',
374
+ size: 820 * 1024,
375
+ modified: new Date('2026-02-11T12:20:00Z')
376
+ }
377
+ ];
378
+
379
+ const REDIRECTS: RedirectRule[] = [
380
+ {
381
+ from: '/sdk/guides/deploy',
382
+ to: '/sdk/guides/deploying',
383
+ status: 301,
384
+ mine: true
385
+ },
386
+ {
387
+ from: '/de/sdk/guides/deploy',
388
+ to: '/de/sdk/guides/deploying',
389
+ status: 301,
390
+ mine: true
391
+ },
392
+ {
393
+ from: '/sdk/v1-9/guides/deploy',
394
+ to: '/sdk/v1-9/guides/deploying',
395
+ status: 301,
396
+ mine: true
397
+ },
398
+ { from: '/sitemap.xml', to: '/sitemap_index.xml', status: 307, mine: false }
399
+ ];
400
+
401
+ /* -------------------------------------------------------------------------- */
402
+ /* The panels */
403
+ /* -------------------------------------------------------------------------- */
404
+
405
+ /** The body of one panel, rendered from the fixture site. */
406
+ export function previewBody(slug: string): string {
407
+ switch (slug) {
408
+ case 'paths':
409
+ return renderPaths({
410
+ // A page that v2 has and v1.9 does not: the fallback's own case, and
411
+ // the reason this panel exists.
412
+ input: '/de/sdk/v1-9/guides/caching',
413
+ locales: ['en', 'de'],
414
+ fallbackLocale: 'en',
415
+ sources: RESOLVED,
416
+ found: null,
417
+ byCollection: BY_COLLECTION
418
+ });
419
+ case 'pages':
420
+ return renderPages(GROUPS, ROOT);
421
+ case 'versions':
422
+ return renderVersions(RESOLVED, BY_COLLECTION);
423
+ case 'checks':
424
+ return renderChecks(RESOLVED, RECORDS, ROOT);
425
+ case 'search':
426
+ return renderSearch('install', SECTIONS);
427
+ case 'config':
428
+ return renderConfig(CONSUMER);
429
+ case 'i18n':
430
+ return renderI18n({
431
+ files: [
432
+ { dir: 'en', keys: LOCALE_KEYS },
433
+ { dir: 'de', keys: LOCALE_KEYS },
434
+ { dir: 'fr', keys: LOCALE_KEYS.slice(0, -2) },
435
+ { dir: 'pt-BR', keys: [...LOCALE_KEYS, 'duxt.page.translate'] }
436
+ ],
437
+ locales: ['en', 'de', 'fr', 'pt-BR'],
438
+ defaultLocale: 'en'
439
+ });
440
+ case 'cache':
441
+ return renderCache({ dataDir: `${ROOT}/.data/content`, entries: CACHE });
442
+ case 'redirects':
443
+ return renderRedirects(REDIRECTS);
444
+ default:
445
+ return renderSources(RESOLVED, `${ROOT}/app/app.config.ts`);
446
+ }
447
+ }
448
+
449
+ /** The file a preview is written to, and the name a docs page asks for. */
450
+ export const previewFile = (slug: string) => `${slug || 'sources'}.html`;
451
+
452
+ /** Every panel the documentation can embed. */
453
+ export const PREVIEW_TABS = PANELS.map((panel) => ({
454
+ ...panel,
455
+ file: previewFile(panel.slug)
456
+ }));
457
+
458
+ /** One panel as a whole document, without the tab row — see `page`. */
459
+ export function previewPage(slug: string): string {
460
+ return page(slug, previewBody(slug), true);
461
+ }
@@ -0,0 +1,233 @@
1
+ import type { PageRecord } from '../../../validate-report';
2
+ import { report } from '../../../validate-report';
3
+ import {
4
+ code,
5
+ dim,
6
+ editor,
7
+ escape,
8
+ filter,
9
+ row,
10
+ stat,
11
+ stats,
12
+ table,
13
+ tag
14
+ } from '../shell';
15
+
16
+ /**
17
+ * The three panels that read the content itself, rather than the config.
18
+ *
19
+ * All three describe what the RUNNING site holds: the queries in
20
+ * `../content.ts` ask Content's own database, the same one the pages render
21
+ * from, and hand the result here. A panel that parsed its own copy of the
22
+ * Markdown would answer about a site that does not exist.
23
+ */
24
+
25
+ export interface Doc {
26
+ path: string;
27
+ title?: string;
28
+ description?: string;
29
+ icon?: string;
30
+ id?: string;
31
+ lastUpdated?: string;
32
+ contributors?: { name: string }[];
33
+ body?: unknown;
34
+ }
35
+
36
+ export interface DocGroup {
37
+ collection: string;
38
+ docs: Doc[];
39
+ }
40
+
41
+ /** Every page, with what the frontmatter and the git history gave it. */
42
+ export function renderPages(groups: DocGroup[], rootDir = ''): string {
43
+ const total = groups.reduce((sum, group) => sum + group.docs.length, 0);
44
+ const untitled = groups.reduce(
45
+ (sum, group) => sum + group.docs.filter((doc) => !doc.title).length,
46
+ 0
47
+ );
48
+ const dated = groups.reduce(
49
+ (sum, group) => sum + group.docs.filter((doc) => doc.lastUpdated).length,
50
+ 0
51
+ );
52
+
53
+ const summary = stats([
54
+ stat(total, 'pages'),
55
+ stat(groups.length, 'collections'),
56
+ stat(dated, 'with history', dated ? '' : 'warn'),
57
+ stat(untitled, 'without a title', untitled ? 'warn' : 'ok')
58
+ ]);
59
+
60
+ // One block per collection, the first opened: a site with five versions has
61
+ // five near-identical tables, and all five expanded is a page nobody reads.
62
+ const sections = groups.map(({ collection, docs }, index) => {
63
+ const rows = docs.map((doc) =>
64
+ row([
65
+ code(doc.path),
66
+ escape(doc.title) || tag('missing', 'warn'),
67
+ doc.description ? dim('yes') : tag('missing', 'warn'),
68
+ doc.icon ? code(doc.icon) : dim('—'),
69
+ doc.lastUpdated
70
+ ? escape(doc.lastUpdated.slice(0, 10))
71
+ : dim('no history'),
72
+ doc.contributors?.length
73
+ ? escape(String(doc.contributors.length))
74
+ : dim('—'),
75
+ editor(doc.id, doc.id?.split('/').slice(1).join('/'), rootDir)
76
+ ])
77
+ );
78
+
79
+ return `<details${index === 0 ? ' open' : ''}>
80
+ <summary>${escape(collection)} <span class="dim">${docs.length} pages</span></summary>
81
+ ${docs.length > 12 ? filter(`Filter ${collection}`) : ''}
82
+ ${table(
83
+ [
84
+ 'Path',
85
+ 'Title',
86
+ 'Description',
87
+ 'Icon',
88
+ 'Last updated',
89
+ 'Contributors',
90
+ 'File'
91
+ ],
92
+ rows,
93
+ 'Content produced no pages here. Every URL under this prefix is a 404 — check the source’s `path` and `refs`; the Checks panel reports it as an error.'
94
+ )}
95
+ </details>`;
96
+ });
97
+
98
+ return `${summary}${sections.join('')}`;
99
+ }
100
+
101
+ /**
102
+ * The build validator, run against the served site.
103
+ *
104
+ * Identical checks to `pnpm build`, from `validate-report.ts` — the point is
105
+ * not a second opinion but a place where the findings stay put. A warning
106
+ * printed during a build has scrolled away by the time anyone reads it, and
107
+ * "last updated" on a broken link is exactly when you want the file name.
108
+ */
109
+ export function renderChecks(
110
+ sources: DuxtResolvedSource[],
111
+ pages: PageRecord[],
112
+ rootDir = ''
113
+ ): string {
114
+ const { errors, warnings, notes } = report(sources, pages);
115
+
116
+ const list = (findings: string[], kind: string) =>
117
+ table(
118
+ ['', kind === 'error' ? 'Error' : 'Warning'],
119
+ findings.map((finding) =>
120
+ row([tag(kind, kind), fileLinked(finding, rootDir)])
121
+ )
122
+ );
123
+
124
+ const summary = stats([
125
+ stat(pages.length, 'pages checked'),
126
+ stat(sources.length, 'collections'),
127
+ stat(errors.length, 'errors', errors.length ? 'error' : 'ok'),
128
+ stat(warnings.length, 'warnings', warnings.length ? 'warn' : 'ok')
129
+ ]);
130
+
131
+ // Notes are not findings and must not read like them: a language's coverage
132
+ // is a state of the site, and putting it in the warnings table would make
133
+ // every translated site look as though something were wrong with it.
134
+ //
135
+ // A note indented by two spaces belongs UNDER the line above it — the report
136
+ // is one summary per language followed by the files behind the figure, and
137
+ // HTML collapses that indent to a single space if it is left as text.
138
+ const coverage = notes.length
139
+ ? `<h2>Translations</h2><ul class="steps">${notes
140
+ .map((note) =>
141
+ note.startsWith(' ')
142
+ ? `<li class="dim" style="margin-left:1.5rem">${fileLinked(note.trim(), rootDir)}</li>`
143
+ : `<li><strong>${fileLinked(note, rootDir)}</strong></li>`
144
+ )
145
+ .join('')}</ul>`
146
+ : '';
147
+
148
+ return `${summary}${errors.length ? `<h2>Errors</h2>${list(errors, 'error')}` : ''}${
149
+ warnings.length ? `<h2>Warnings</h2>${list(warnings, 'warn')}` : ''
150
+ }${coverage}`;
151
+ }
152
+
153
+ /**
154
+ * Findings name their file in quotes; turn the first one into an editor link.
155
+ *
156
+ * The message stays exactly the string the build prints — the two must not
157
+ * drift — so the link is added by reading it rather than by threading a second
158
+ * field through the report.
159
+ */
160
+ function fileLinked(finding: string, rootDir: string): string {
161
+ const match = /"([^"]+\.md)"/.exec(finding);
162
+ if (!match) return escape(finding);
163
+
164
+ const [quoted, file] = match;
165
+
166
+ return (
167
+ escape(finding.slice(0, match.index)) +
168
+ editor(file, quoted, rootDir) +
169
+ escape(finding.slice(match.index + quoted.length))
170
+ );
171
+ }
172
+
173
+ export interface IndexedSection {
174
+ collection: string;
175
+ id: string;
176
+ title?: string;
177
+ titles?: string[];
178
+ content?: string;
179
+ }
180
+
181
+ /**
182
+ * What the search index holds.
183
+ *
184
+ * The search had three separate bugs — a filter fighting a filter, results that
185
+ * were heading fragments, table cells run together without separators — and all
186
+ * three were invisible because nobody could see the sections the index is built
187
+ * from. This is that list, with the same query the client runs.
188
+ */
189
+ export function renderSearch(term: string, sections: IndexedSection[]): string {
190
+ const form = `<form method="get">
191
+ <input type="search" name="q" value="${escape(term)}" placeholder="A word as a reader would type it" autofocus>
192
+ <button type="submit">Search</button>
193
+ </form>`;
194
+
195
+ const needle = term.trim().toLowerCase();
196
+
197
+ const matching = needle
198
+ ? sections.filter(
199
+ (section) =>
200
+ section.title?.toLowerCase().includes(needle) ||
201
+ section.content?.toLowerCase().includes(needle)
202
+ )
203
+ : sections;
204
+
205
+ // The id is the longest string in the table and the least worth reading in
206
+ // full, so it is the one that gets clipped — otherwise it takes the width the
207
+ // indexed text needs, and the column that answers the question is a sliver.
208
+ const rows = matching.slice(0, 200).map(
209
+ (section) => `<tr>
210
+ <td>${code(section.collection)}</td>
211
+ <td><code class="trunc" title="${escape(section.id)}">${escape(section.id)}</code></td>
212
+ <td>${escape(section.title)}</td>
213
+ <td>${escape((section.titles ?? []).join(' › ')) || dim('—')}</td>
214
+ <td class="wide">${escape((section.content ?? '').slice(0, 180))}${(section.content ?? '').length > 180 ? dim(' …') : ''}</td>
215
+ </tr>`
216
+ );
217
+
218
+ const summary = stats([
219
+ stat(sections.length, 'sections indexed'),
220
+ needle
221
+ ? stat(matching.length, 'matching', matching.length ? '' : 'warn')
222
+ : undefined,
223
+ matching.length > 200 ? stat(200, 'shown', 'warn') : undefined
224
+ ]);
225
+
226
+ return `${form}${summary}<div class="note">One row per heading — which is what the dialog groups back into pages. The text column is exactly what FTS5 matches against.</div>${table(
227
+ ['Collection', 'Id', 'Heading', 'Under', 'Indexed text'],
228
+ rows,
229
+ needle
230
+ ? `Nothing in the index matches “${escape(term)}”. FTS5 matches terms and prefixes, so a typo returns nothing.`
231
+ : 'The index is empty.'
232
+ )}`;
233
+ }