@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.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/app/app.config.ts +13 -0
- package/app/app.vue +122 -0
- package/app/assets/css/duxt.css +555 -0
- package/app/assets/css/typeset.css +498 -0
- package/app/components/DuxtBrand.vue +53 -0
- package/app/components/DuxtBreadcrumb.vue +37 -0
- package/app/components/DuxtCodeBlock.vue +143 -0
- package/app/components/DuxtCopyPage.vue +117 -0
- package/app/components/DuxtFooter.vue +86 -0
- package/app/components/DuxtHeader.vue +496 -0
- package/app/components/DuxtJsonEditor.vue +359 -0
- package/app/components/DuxtLandingProse.vue +90 -0
- package/app/components/DuxtLandingShowcase.vue +295 -0
- package/app/components/DuxtLiveWindow.vue +343 -0
- package/app/components/DuxtLocale.vue +86 -0
- package/app/components/DuxtMetaList.vue +76 -0
- package/app/components/DuxtNavigation.vue +94 -0
- package/app/components/DuxtNavigationLink.vue +41 -0
- package/app/components/DuxtOpenApiCallbacks.vue +92 -0
- package/app/components/DuxtOpenApiClient.vue +1339 -0
- package/app/components/DuxtOpenApiMedia.vue +108 -0
- package/app/components/DuxtOpenApiMethod.vue +59 -0
- package/app/components/DuxtOpenApiParameters.vue +114 -0
- package/app/components/DuxtOpenApiResponses.vue +109 -0
- package/app/components/DuxtOpenApiSchema.vue +365 -0
- package/app/components/DuxtOpenApiSecurity.vue +115 -0
- package/app/components/DuxtPageFeedback.vue +56 -0
- package/app/components/DuxtPageInfo.vue +194 -0
- package/app/components/DuxtPageNav.vue +93 -0
- package/app/components/DuxtPreviewSkeleton.vue +262 -0
- package/app/components/DuxtProgress.vue +45 -0
- package/app/components/DuxtSearch.vue +271 -0
- package/app/components/DuxtSections.vue +65 -0
- package/app/components/DuxtShortcuts.vue +46 -0
- package/app/components/DuxtSkipLink.vue +34 -0
- package/app/components/DuxtToc.vue +86 -0
- package/app/components/DuxtTranslationBanner.vue +64 -0
- package/app/components/DuxtVersion.vue +168 -0
- package/app/components/DuxtVersionBanner.vue +109 -0
- package/app/components/OgImage/Duxt.satori.vue +82 -0
- package/app/components/content/Accordion.vue +37 -0
- package/app/components/content/AccordionItem.vue +46 -0
- package/app/components/content/Callout.vue +58 -0
- package/app/components/content/ChangelogGroup.vue +91 -0
- package/app/components/content/ChangelogReleases.vue +520 -0
- package/app/components/content/CodeGroup.vue +183 -0
- package/app/components/content/DevtoolsPanel.vue +107 -0
- package/app/components/content/Field.vue +50 -0
- package/app/components/content/FieldGroup.vue +8 -0
- package/app/components/content/FileTree.vue +92 -0
- package/app/components/content/Mermaid.vue +70 -0
- package/app/components/content/OpenApiOperation.vue +185 -0
- package/app/components/content/OpenApiOperations.vue +72 -0
- package/app/components/content/OpenApiOverview.vue +302 -0
- package/app/components/content/PackageManagers.vue +187 -0
- package/app/components/content/PageCards.vue +48 -0
- package/app/components/content/Partial.vue +65 -0
- package/app/components/content/Preview.vue +173 -0
- package/app/components/content/ProseA.vue +55 -0
- package/app/components/content/ProseH2.vue +53 -0
- package/app/components/content/ProseH3.vue +53 -0
- package/app/components/content/ProseH4.vue +53 -0
- package/app/components/content/ProseImg.vue +87 -0
- package/app/components/content/ProsePre.vue +58 -0
- package/app/components/content/ProseTable.vue +11 -0
- package/app/components/content/Since.vue +21 -0
- package/app/components/content/Steps.vue +20 -0
- package/app/components/content/Tab.vue +23 -0
- package/app/components/content/TabGroup.vue +58 -0
- package/app/components/ui/alert/Alert.vue +21 -0
- package/app/components/ui/alert/AlertDescription.vue +22 -0
- package/app/components/ui/alert/AlertTitle.vue +22 -0
- package/app/components/ui/alert/index.ts +24 -0
- package/app/components/ui/badge/Badge.vue +28 -0
- package/app/components/ui/badge/index.ts +52 -0
- package/app/components/ui/breadcrumb/Breadcrumb.vue +13 -0
- package/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
- package/app/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
- package/app/components/ui/breadcrumb/BreadcrumbLink.vue +24 -0
- package/app/components/ui/breadcrumb/BreadcrumbList.vue +22 -0
- package/app/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
- package/app/components/ui/breadcrumb/index.ts +7 -0
- package/app/components/ui/button/Button.vue +31 -0
- package/app/components/ui/button/index.ts +39 -0
- package/app/components/ui/card/Card.vue +22 -0
- package/app/components/ui/card/CardAction.vue +22 -0
- package/app/components/ui/card/CardContent.vue +14 -0
- package/app/components/ui/card/CardDescription.vue +17 -0
- package/app/components/ui/card/CardFooter.vue +17 -0
- package/app/components/ui/card/CardHeader.vue +22 -0
- package/app/components/ui/card/CardTitle.vue +17 -0
- package/app/components/ui/card/index.ts +7 -0
- package/app/components/ui/collapsible/Collapsible.vue +19 -0
- package/app/components/ui/collapsible/CollapsibleContent.vue +12 -0
- package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
- package/app/components/ui/collapsible/index.ts +3 -0
- package/app/components/ui/command/Command.vue +98 -0
- package/app/components/ui/command/CommandDialog.vue +42 -0
- package/app/components/ui/command/CommandEmpty.vue +31 -0
- package/app/components/ui/command/CommandGroup.vue +52 -0
- package/app/components/ui/command/CommandInput.vue +45 -0
- package/app/components/ui/command/CommandItem.vue +85 -0
- package/app/components/ui/command/CommandList.vue +32 -0
- package/app/components/ui/command/CommandSeparator.vue +23 -0
- package/app/components/ui/command/CommandShortcut.vue +19 -0
- package/app/components/ui/command/index.ts +29 -0
- package/app/components/ui/dialog/Dialog.vue +15 -0
- package/app/components/ui/dialog/DialogClose.vue +12 -0
- package/app/components/ui/dialog/DialogContent.vue +61 -0
- package/app/components/ui/dialog/DialogDescription.vue +25 -0
- package/app/components/ui/dialog/DialogFooter.vue +30 -0
- package/app/components/ui/dialog/DialogHeader.vue +17 -0
- package/app/components/ui/dialog/DialogOverlay.vue +28 -0
- package/app/components/ui/dialog/DialogScrollContent.vue +65 -0
- package/app/components/ui/dialog/DialogTitle.vue +25 -0
- package/app/components/ui/dialog/DialogTrigger.vue +12 -0
- package/app/components/ui/dialog/index.ts +10 -0
- package/app/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +47 -0
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +47 -0
- package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +27 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +48 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +34 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +34 -0
- package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
- package/app/components/ui/dropdown-menu/index.ts +16 -0
- package/app/components/ui/input/Input.vue +35 -0
- package/app/components/ui/input/index.ts +1 -0
- package/app/components/ui/navigation-menu/NavigationMenu.vue +42 -0
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +35 -0
- package/app/components/ui/navigation-menu/NavigationMenuIndicator.vue +32 -0
- package/app/components/ui/navigation-menu/NavigationMenuItem.vue +23 -0
- package/app/components/ui/navigation-menu/NavigationMenuLink.vue +30 -0
- package/app/components/ui/navigation-menu/NavigationMenuList.vue +30 -0
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +31 -0
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +30 -0
- package/app/components/ui/navigation-menu/index.ts +14 -0
- package/app/components/ui/scroll-area/ScrollArea.vue +31 -0
- package/app/components/ui/scroll-area/ScrollBar.vue +38 -0
- package/app/components/ui/scroll-area/index.ts +2 -0
- package/app/components/ui/select/Select.vue +15 -0
- package/app/components/ui/select/SelectContent.vue +59 -0
- package/app/components/ui/select/SelectGroup.vue +19 -0
- package/app/components/ui/select/SelectItem.vue +42 -0
- package/app/components/ui/select/SelectItemText.vue +12 -0
- package/app/components/ui/select/SelectLabel.vue +16 -0
- package/app/components/ui/select/SelectScrollDownButton.vue +28 -0
- package/app/components/ui/select/SelectScrollUpButton.vue +28 -0
- package/app/components/ui/select/SelectSeparator.vue +20 -0
- package/app/components/ui/select/SelectTrigger.vue +32 -0
- package/app/components/ui/select/SelectValue.vue +12 -0
- package/app/components/ui/select/index.ts +11 -0
- package/app/components/ui/separator/Separator.vue +30 -0
- package/app/components/ui/separator/index.ts +1 -0
- package/app/components/ui/sheet/Sheet.vue +15 -0
- package/app/components/ui/sheet/SheetClose.vue +12 -0
- package/app/components/ui/sheet/SheetContent.vue +68 -0
- package/app/components/ui/sheet/SheetDescription.vue +23 -0
- package/app/components/ui/sheet/SheetFooter.vue +15 -0
- package/app/components/ui/sheet/SheetHeader.vue +15 -0
- package/app/components/ui/sheet/SheetOverlay.vue +28 -0
- package/app/components/ui/sheet/SheetTitle.vue +23 -0
- package/app/components/ui/sheet/SheetTrigger.vue +12 -0
- package/app/components/ui/sheet/index.ts +8 -0
- package/app/components/ui/sidebar/Sidebar.vue +110 -0
- package/app/components/ui/sidebar/SidebarContent.vue +23 -0
- package/app/components/ui/sidebar/SidebarFooter.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroup.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroupAction.vue +31 -0
- package/app/components/ui/sidebar/SidebarGroupContent.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
- package/app/components/ui/sidebar/SidebarHeader.vue +18 -0
- package/app/components/ui/sidebar/SidebarInput.vue +19 -0
- package/app/components/ui/sidebar/SidebarInset.vue +23 -0
- package/app/components/ui/sidebar/SidebarMenu.vue +18 -0
- package/app/components/ui/sidebar/SidebarMenuAction.vue +42 -0
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +28 -0
- package/app/components/ui/sidebar/SidebarMenuButton.vue +60 -0
- package/app/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
- package/app/components/ui/sidebar/SidebarMenuItem.vue +18 -0
- package/app/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
- package/app/components/ui/sidebar/SidebarMenuSub.vue +24 -0
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +43 -0
- package/app/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
- package/app/components/ui/sidebar/SidebarProvider.vue +109 -0
- package/app/components/ui/sidebar/SidebarRail.vue +35 -0
- package/app/components/ui/sidebar/SidebarSeparator.vue +19 -0
- package/app/components/ui/sidebar/SidebarTrigger.vue +26 -0
- package/app/components/ui/sidebar/index.ts +62 -0
- package/app/components/ui/sidebar/utils.ts +19 -0
- package/app/components/ui/skeleton/Skeleton.vue +17 -0
- package/app/components/ui/skeleton/index.ts +1 -0
- package/app/components/ui/sonner/Sonner.vue +67 -0
- package/app/components/ui/sonner/index.ts +1 -0
- package/app/components/ui/tabs/Tabs.vue +26 -0
- package/app/components/ui/tabs/TabsContent.vue +23 -0
- package/app/components/ui/tabs/TabsList.vue +28 -0
- package/app/components/ui/tabs/TabsTrigger.vue +30 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/textarea/Textarea.vue +32 -0
- package/app/components/ui/textarea/index.ts +1 -0
- package/app/components/ui/tooltip/Tooltip.vue +15 -0
- package/app/components/ui/tooltip/TooltipContent.vue +49 -0
- package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/app/components/ui/tooltip/TooltipTrigger.vue +12 -0
- package/app/components/ui/tooltip/index.ts +4 -0
- package/app/composables/useActiveHeading.ts +90 -0
- package/app/composables/useDuxtAnimatedHeight.ts +155 -0
- package/app/composables/useDuxtBreadcrumb.ts +60 -0
- package/app/composables/useDuxtChoice.ts +28 -0
- package/app/composables/useDuxtCollection.ts +89 -0
- package/app/composables/useDuxtConfig.ts +47 -0
- package/app/composables/useDuxtNavigation.ts +87 -0
- package/app/composables/useDuxtPageFocus.ts +30 -0
- package/app/composables/useDuxtPath.ts +42 -0
- package/app/composables/useDuxtSearch.ts +208 -0
- package/app/composables/useDuxtSection.ts +44 -0
- package/app/composables/useDuxtSectionRow.ts +49 -0
- package/app/composables/useDuxtShortcuts.ts +58 -0
- package/app/composables/useDuxtSiteUrl.ts +27 -0
- package/app/composables/useDuxtToast.ts +25 -0
- package/app/composables/useDuxtVersion.ts +138 -0
- package/app/composables/useFuzzySearch.ts +83 -0
- package/app/composables/usePackageManager.ts +4 -0
- package/app/composables/useRecentPages.ts +52 -0
- package/app/error.vue +133 -0
- package/app/layouts/changelog.vue +85 -0
- package/app/layouts/default.vue +23 -0
- package/app/layouts/docs.vue +62 -0
- package/app/layouts/reference.vue +85 -0
- package/app/lib/utils.ts +7 -0
- package/app/middleware/duxt-section-layout.global.ts +57 -0
- package/app/pages/[...slug].vue +441 -0
- package/app/pages/index.vue +479 -0
- package/app/types/duxt.d.ts +955 -0
- package/app/utils/aside.ts +16 -0
- package/app/utils/changelog.ts +182 -0
- package/app/utils/code-highlight.ts +116 -0
- package/app/utils/duxt-config.ts +185 -0
- package/app/utils/duxt-text.ts +135 -0
- package/app/utils/file-icons.ts +62 -0
- package/app/utils/generated-sections.ts +190 -0
- package/app/utils/generated-toc.ts +216 -0
- package/app/utils/heading-link.ts +32 -0
- package/app/utils/locale-path.ts +45 -0
- package/app/utils/navigation-tree.ts +206 -0
- package/app/utils/nearest-page.ts +72 -0
- package/app/utils/openapi.ts +1028 -0
- package/app/utils/package-command.ts +162 -0
- package/app/utils/request-samples.ts +635 -0
- package/app/utils/section-scope.ts +97 -0
- package/app/utils/sheet-navigation.ts +100 -0
- package/app/utils/source-file.ts +84 -0
- package/app/utils/version-choices.ts +68 -0
- package/app/utils/version-paths.ts +65 -0
- package/bin/duxt-report.mjs +45 -0
- package/components.json +19 -0
- package/content-cache.ts +193 -0
- package/content.config.ts +38 -0
- package/duxt-app-config.ts +124 -0
- package/highlight-langs.ts +68 -0
- package/i18n/i18n.config.ts +16 -0
- package/i18n/locales/de/duxt/changelog.json +17 -0
- package/i18n/locales/de/duxt/code.json +13 -0
- package/i18n/locales/de/duxt/defaults.json +22 -0
- package/i18n/locales/de/duxt/devtools.json +7 -0
- package/i18n/locales/de/duxt/error.json +10 -0
- package/i18n/locales/de/duxt/footer.json +7 -0
- package/i18n/locales/de/duxt/locale.json +7 -0
- package/i18n/locales/de/duxt/nav.json +17 -0
- package/i18n/locales/de/duxt/openapi.json +89 -0
- package/i18n/locales/de/duxt/page.json +34 -0
- package/i18n/locales/de/duxt/search.json +12 -0
- package/i18n/locales/de/duxt/shortcuts.json +11 -0
- package/i18n/locales/de/duxt/theme.json +7 -0
- package/i18n/locales/de/duxt/toc.json +7 -0
- package/i18n/locales/de/duxt/version.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +17 -0
- package/i18n/locales/en/duxt/code.json +13 -0
- package/i18n/locales/en/duxt/defaults.json +22 -0
- package/i18n/locales/en/duxt/devtools.json +7 -0
- package/i18n/locales/en/duxt/error.json +10 -0
- package/i18n/locales/en/duxt/footer.json +7 -0
- package/i18n/locales/en/duxt/locale.json +7 -0
- package/i18n/locales/en/duxt/nav.json +17 -0
- package/i18n/locales/en/duxt/openapi.json +89 -0
- package/i18n/locales/en/duxt/page.json +34 -0
- package/i18n/locales/en/duxt/search.json +12 -0
- package/i18n/locales/en/duxt/shortcuts.json +11 -0
- package/i18n/locales/en/duxt/theme.json +7 -0
- package/i18n/locales/en/duxt/toc.json +7 -0
- package/i18n/locales/en/duxt/version.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +17 -0
- package/i18n/locales/es/duxt/code.json +13 -0
- package/i18n/locales/es/duxt/defaults.json +22 -0
- package/i18n/locales/es/duxt/devtools.json +7 -0
- package/i18n/locales/es/duxt/error.json +10 -0
- package/i18n/locales/es/duxt/footer.json +7 -0
- package/i18n/locales/es/duxt/locale.json +7 -0
- package/i18n/locales/es/duxt/nav.json +17 -0
- package/i18n/locales/es/duxt/openapi.json +89 -0
- package/i18n/locales/es/duxt/page.json +34 -0
- package/i18n/locales/es/duxt/search.json +12 -0
- package/i18n/locales/es/duxt/shortcuts.json +11 -0
- package/i18n/locales/es/duxt/theme.json +7 -0
- package/i18n/locales/es/duxt/toc.json +7 -0
- package/i18n/locales/es/duxt/version.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +17 -0
- package/i18n/locales/fr/duxt/code.json +13 -0
- package/i18n/locales/fr/duxt/defaults.json +22 -0
- package/i18n/locales/fr/duxt/devtools.json +7 -0
- package/i18n/locales/fr/duxt/error.json +10 -0
- package/i18n/locales/fr/duxt/footer.json +7 -0
- package/i18n/locales/fr/duxt/locale.json +7 -0
- package/i18n/locales/fr/duxt/nav.json +17 -0
- package/i18n/locales/fr/duxt/openapi.json +89 -0
- package/i18n/locales/fr/duxt/page.json +34 -0
- package/i18n/locales/fr/duxt/search.json +12 -0
- package/i18n/locales/fr/duxt/shortcuts.json +11 -0
- package/i18n/locales/fr/duxt/theme.json +7 -0
- package/i18n/locales/fr/duxt/toc.json +7 -0
- package/i18n/locales/fr/duxt/version.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +17 -0
- package/i18n/locales/pt/duxt/code.json +13 -0
- package/i18n/locales/pt/duxt/defaults.json +22 -0
- package/i18n/locales/pt/duxt/devtools.json +7 -0
- package/i18n/locales/pt/duxt/error.json +10 -0
- package/i18n/locales/pt/duxt/footer.json +7 -0
- package/i18n/locales/pt/duxt/locale.json +7 -0
- package/i18n/locales/pt/duxt/nav.json +17 -0
- package/i18n/locales/pt/duxt/openapi.json +89 -0
- package/i18n/locales/pt/duxt/page.json +34 -0
- package/i18n/locales/pt/duxt/search.json +12 -0
- package/i18n/locales/pt/duxt/shortcuts.json +11 -0
- package/i18n/locales/pt/duxt/theme.json +7 -0
- package/i18n/locales/pt/duxt/toc.json +7 -0
- package/i18n/locales/pt/duxt/version.json +25 -0
- package/i18n/locales/pt-BR/duxt/error.json +8 -0
- package/i18n/locales/pt-BR/duxt/nav.json +10 -0
- package/i18n/locales/pt-BR/duxt/page.json +10 -0
- package/i18n/locales/pt-BR/duxt/search.json +11 -0
- package/i18n/locales/pt-BR/duxt/version.json +13 -0
- package/mdc.config.ts +28 -0
- package/modules/config.ts +440 -0
- package/modules/devtools.ts +126 -0
- package/modules/git-meta.ts +349 -0
- package/modules/redirects.ts +152 -0
- package/modules/validate.ts +137 -0
- package/nuxt.config.ts +530 -0
- package/openapi-model.ts +358 -0
- package/openapi-parse.ts +1052 -0
- package/package.json +161 -0
- package/public/devtools/cache.html +318 -0
- package/public/devtools/checks.html +309 -0
- package/public/devtools/config.html +461 -0
- package/public/devtools/i18n.html +309 -0
- package/public/devtools/pages.html +348 -0
- package/public/devtools/paths.html +315 -0
- package/public/devtools/redirects.html +309 -0
- package/public/devtools/search.html +324 -0
- package/public/devtools/sources.html +309 -0
- package/public/devtools/versions.html +318 -0
- package/report.ts +303 -0
- package/repository-root.ts +26 -0
- package/section-reports.ts +55 -0
- package/sections-changelog.ts +644 -0
- package/sections-openapi.ts +450 -0
- package/sections-resolve.ts +711 -0
- package/sections.ts +157 -0
- package/server/devtools/content.ts +120 -0
- package/server/devtools/context.ts +38 -0
- package/server/devtools/entry-path.ts +30 -0
- package/server/devtools/handler.ts +104 -0
- package/server/devtools/preview.ts +461 -0
- package/server/devtools/render/content.ts +233 -0
- package/server/devtools/render/sources.ts +414 -0
- package/server/devtools/render/system.ts +352 -0
- package/server/devtools/shell.ts +611 -0
- package/server/devtools/sources.ts +122 -0
- package/server/devtools/system.ts +178 -0
- package/server/mcp/tools/list-pages.ts +52 -0
- package/server/mcp/tools/read-page.ts +57 -0
- package/server/mcp/tools/search-docs.ts +74 -0
- package/server/middleware/raw-markdown.ts +86 -0
- package/server/routes/llms-full.txt.get.ts +75 -0
- package/server/routes/llms.txt.get.ts +63 -0
- package/server/routes/rss.xml.get.ts +125 -0
- package/server/utils/duxt-server-text.ts +46 -0
- package/sources-git.ts +113 -0
- package/sources-resolve.ts +908 -0
- package/sources.ts +287 -0
- package/validate-report.ts +494 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { DuxtSearchSection } from '@duxt/composables/useFuzzySearch';
|
|
2
|
+
|
|
3
|
+
/** A hit, with the source it came out of. */
|
|
4
|
+
export interface DuxtSearchHit extends DuxtSearchSection {
|
|
5
|
+
/** Undefined on a site with one source: there is nothing to distinguish. */
|
|
6
|
+
source?: { label: string; collection: string };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Search across every source, not just the one being read.
|
|
11
|
+
*
|
|
12
|
+
* A site pulling three repositories had three separate searches, and which one
|
|
13
|
+
* a reader got depended on which page they happened to be standing on. This is
|
|
14
|
+
* the thing the `sources` model makes possible and a single-repository theme
|
|
15
|
+
* cannot do at all.
|
|
16
|
+
*
|
|
17
|
+
* RANKED TOGETHER, NOT GROUPED PER REPOSITORY. The choice matters and it is not
|
|
18
|
+
* obvious. Grouping per repository asks the reader to know which project their
|
|
19
|
+
* answer is in — but if they knew that, the search would be doing less work
|
|
20
|
+
* than the sidebar. So the lists are interleaved by rank and each hit carries a
|
|
21
|
+
* badge saying where it came from: the best answers first, provenance kept.
|
|
22
|
+
* The source being read goes first in each round, because a reader searching
|
|
23
|
+
* inside a project usually means that project.
|
|
24
|
+
*
|
|
25
|
+
* ONE VERSION PER REPOSITORY. Searching every version returns each page as many
|
|
26
|
+
* times as there are versions, which buries the answer under its own history.
|
|
27
|
+
* So each repository contributes the version the reader is in, or its default.
|
|
28
|
+
*/
|
|
29
|
+
export function useDuxtSearch() {
|
|
30
|
+
const duxt = useDuxtConfig();
|
|
31
|
+
const { source } = useDuxtCollection();
|
|
32
|
+
|
|
33
|
+
const sources = computed(() => duxt.resolvedSources ?? []);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The collections to search, one per repository.
|
|
37
|
+
*
|
|
38
|
+
* Fixed at setup, deliberately: each one needs its own `useSearchCollection`,
|
|
39
|
+
* and a composable list that grows with the route is not a thing. The
|
|
40
|
+
* manifest is build-time config and does not change while the app is running,
|
|
41
|
+
* so the set is complete from the start; which of them is FIRST still follows
|
|
42
|
+
* the route.
|
|
43
|
+
*/
|
|
44
|
+
const searchable = sources.value.length
|
|
45
|
+
? [
|
|
46
|
+
...new Map(
|
|
47
|
+
sources.value.map((entry) => [entry.collection, entry])
|
|
48
|
+
).values()
|
|
49
|
+
]
|
|
50
|
+
: [];
|
|
51
|
+
|
|
52
|
+
const searches = searchable.map((entry) => ({
|
|
53
|
+
entry,
|
|
54
|
+
...useSearchCollection(
|
|
55
|
+
entry.collection as unknown as Parameters<typeof useSearchCollection>[0],
|
|
56
|
+
{
|
|
57
|
+
// A table is flattened into one string, so its cells run together —
|
|
58
|
+
// `KeyWhat it controlstitleThe name in the navbar…`. Its content is
|
|
59
|
+
// still reachable through the page that holds it.
|
|
60
|
+
ignoredTags: ['table'],
|
|
61
|
+
immediate: false
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The approximate pass, one index per source.
|
|
68
|
+
*
|
|
69
|
+
* It used to run over the collection being read alone, which made a typo
|
|
70
|
+
* quietly narrow the search from every source back to one — the reader who
|
|
71
|
+
* most needed the other repositories searched was the reader who had
|
|
72
|
+
* mistyped. Each index is still built on first use and only on the path FTS
|
|
73
|
+
* could not answer, so a reader who types accurately downloads none of them.
|
|
74
|
+
*/
|
|
75
|
+
const approximate = searchable.map((entry) => ({
|
|
76
|
+
entry,
|
|
77
|
+
search: useFuzzySearch(entry.collection as DuxtCollectionName, {
|
|
78
|
+
ignoredTags: ['table']
|
|
79
|
+
}).search
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
/** One entry per repository: the version being read, else that repo's default. */
|
|
83
|
+
const active = computed(() => {
|
|
84
|
+
const current = source.value;
|
|
85
|
+
const byRepo = new Map<string, (typeof searchable)[number]>();
|
|
86
|
+
|
|
87
|
+
for (const entry of searchable) {
|
|
88
|
+
const key = entry.repo ?? '';
|
|
89
|
+
const chosen = byRepo.get(key);
|
|
90
|
+
|
|
91
|
+
if (entry.collection === current?.collection) {
|
|
92
|
+
byRepo.set(key, entry);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (chosen?.collection === current?.collection) continue;
|
|
97
|
+
if (!chosen || (entry.isDefault && !chosen.isDefault)) {
|
|
98
|
+
byRepo.set(key, entry);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The source being read leads; the rest keep the config's own order.
|
|
103
|
+
return [...byRepo.values()].sort((a, b) =>
|
|
104
|
+
a.collection === current?.collection
|
|
105
|
+
? -1
|
|
106
|
+
: b.collection === current?.collection
|
|
107
|
+
? 1
|
|
108
|
+
: 0
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/** Only worth a badge when there is more than one thing to tell apart. */
|
|
113
|
+
const labelled = computed(() => active.value.length > 1);
|
|
114
|
+
|
|
115
|
+
const labelOf = (entry: (typeof searchable)[number]) =>
|
|
116
|
+
[entry.repo, entry.version].filter(Boolean).join(' ') ||
|
|
117
|
+
entry.prefix ||
|
|
118
|
+
'/';
|
|
119
|
+
|
|
120
|
+
async function init() {
|
|
121
|
+
await Promise.all(
|
|
122
|
+
searches
|
|
123
|
+
.filter((search) => active.value.includes(search.entry))
|
|
124
|
+
.filter((search) => search.status.value === 'idle')
|
|
125
|
+
.map((search) => search.init())
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Interleave by rank.
|
|
131
|
+
*
|
|
132
|
+
* Without scores to compare across collections — each database ranks inside
|
|
133
|
+
* itself — position is the only comparable thing there is. Round-robin over
|
|
134
|
+
* the lists gives every source its best hit before any source gets its
|
|
135
|
+
* second, which is the closest honest approximation of "ranked together".
|
|
136
|
+
*/
|
|
137
|
+
function interleave(lists: DuxtSearchHit[][], limit: number) {
|
|
138
|
+
const merged: DuxtSearchHit[] = [];
|
|
139
|
+
const depth = Math.max(0, ...lists.map((list) => list.length));
|
|
140
|
+
|
|
141
|
+
for (let rank = 0; rank < depth && merged.length < limit; rank++) {
|
|
142
|
+
for (const list of lists) {
|
|
143
|
+
const hit = list[rank];
|
|
144
|
+
if (hit) merged.push(hit);
|
|
145
|
+
if (merged.length >= limit) break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return merged;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function search(
|
|
153
|
+
term: string,
|
|
154
|
+
limit = 20
|
|
155
|
+
): Promise<{ hits: DuxtSearchHit[]; approximate: boolean }> {
|
|
156
|
+
if (!term.trim()) return { hits: [], approximate: false };
|
|
157
|
+
|
|
158
|
+
const wanted = active.value;
|
|
159
|
+
|
|
160
|
+
const lists = await Promise.all(
|
|
161
|
+
searches
|
|
162
|
+
.filter((entry) => wanted.includes(entry.entry))
|
|
163
|
+
.map(async (entry) => {
|
|
164
|
+
const hits = (await entry.search(term, {
|
|
165
|
+
limit
|
|
166
|
+
})) as DuxtSearchSection[];
|
|
167
|
+
|
|
168
|
+
return hits.map((hit) => ({
|
|
169
|
+
...hit,
|
|
170
|
+
source: labelled.value
|
|
171
|
+
? {
|
|
172
|
+
label: labelOf(entry.entry),
|
|
173
|
+
collection: entry.entry.collection
|
|
174
|
+
}
|
|
175
|
+
: undefined
|
|
176
|
+
}));
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const merged = interleave(lists, limit);
|
|
181
|
+
if (merged.length) return { hits: merged, approximate: false };
|
|
182
|
+
|
|
183
|
+
// FTS matches terms and prefixes, not near-misses, so one wrong letter
|
|
184
|
+
// leaves the reader with an empty box. Fuse gets a second look, over the
|
|
185
|
+
// same sources and merged the same way.
|
|
186
|
+
const fuzzy = await Promise.all(
|
|
187
|
+
approximate
|
|
188
|
+
.filter((entry) => wanted.includes(entry.entry))
|
|
189
|
+
.map(async (entry) => {
|
|
190
|
+
const hits = await entry.search(term, limit);
|
|
191
|
+
|
|
192
|
+
return hits.map((hit) => ({
|
|
193
|
+
...hit,
|
|
194
|
+
source: labelled.value
|
|
195
|
+
? {
|
|
196
|
+
label: labelOf(entry.entry),
|
|
197
|
+
collection: entry.entry.collection
|
|
198
|
+
}
|
|
199
|
+
: undefined
|
|
200
|
+
}));
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return { hits: interleave(fuzzy, limit), approximate: true };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { search, init, labelled };
|
|
208
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ContentNavigationItem } from '@nuxt/content';
|
|
2
|
+
import type { Ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The documentation is split into sections, each owning a branch of the tree —
|
|
6
|
+
* so the sidebar shows one branch, not everything. Which branch follows from
|
|
7
|
+
* the path, falling back to the whole tree so a page outside any section still
|
|
8
|
+
* has a sidebar.
|
|
9
|
+
*/
|
|
10
|
+
export function useDuxtSection(
|
|
11
|
+
navigation: Ref<ContentNavigationItem[] | null | undefined>
|
|
12
|
+
) {
|
|
13
|
+
const path = useDuxtPath();
|
|
14
|
+
const duxt = useDuxtConfig();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* BOTH ROWS, sections first.
|
|
18
|
+
*
|
|
19
|
+
* A generated section can declare that its entry belongs in the top navbar
|
|
20
|
+
* rather than in the section row (`navigation: 'navigation'`), and a site can
|
|
21
|
+
* put an ordinary page there by hand. Neither stops being the branch its
|
|
22
|
+
* pages sit in — but reading only `sections` said it did: the sidebar fell
|
|
23
|
+
* back to the whole tree, and the breadcrumb lost its head, so a release page
|
|
24
|
+
* printed its own version and nothing above it.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* LONGEST MATCH, not the first one written. `/demo` and `/demo/api` both
|
|
28
|
+
* prefix a page under the reference, and taking the first said the reader was
|
|
29
|
+
* in the prose beside it — so the sidebar drew that tree instead of the
|
|
30
|
+
* endpoints. Same rule, and the same bug, as `sourceForPath`.
|
|
31
|
+
*/
|
|
32
|
+
const section = computed(() =>
|
|
33
|
+
currentSection(
|
|
34
|
+
[...(duxt.sections ?? []), ...(duxt.navigation ?? [])],
|
|
35
|
+
path.value
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const items = computed(() =>
|
|
40
|
+
sectionItems(navigation.value ?? [], section.value?.to, path.value)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return { section, items };
|
|
44
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the second navbar row showing?
|
|
3
|
+
*
|
|
4
|
+
* Two things need the answer and must not disagree about it: `DuxtSections`,
|
|
5
|
+
* which draws the row only where one of its sections is current — a page
|
|
6
|
+
* outside every section, or a generated section whose entry the site put in the
|
|
7
|
+
* top row instead, would otherwise get the whole row with nothing marked — and
|
|
8
|
+
* the LAYOUTS, whose sticky columns are offset by exactly the header above
|
|
9
|
+
* them. Answered in one place because a mismatch is silent: the row vanishes
|
|
10
|
+
* and the sidebars keep the gap where it used to be.
|
|
11
|
+
*/
|
|
12
|
+
export function useDuxtSectionRow() {
|
|
13
|
+
const duxt = useDuxtConfig();
|
|
14
|
+
const path = useDuxtPath();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* THIS AREA'S ENTRIES, not every entry the site declared — see
|
|
18
|
+
* `sectionsForPath`. A second source has top-level parts of its own, and the
|
|
19
|
+
* row is "the parts of what you are reading" rather than a site index.
|
|
20
|
+
*/
|
|
21
|
+
const sections = computed(() =>
|
|
22
|
+
sectionsForPath(duxt.sections ?? [], duxt.resolvedSources ?? [], path.value)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const visible = computed(() =>
|
|
26
|
+
sections.value.some(
|
|
27
|
+
(section) => section.to && path.value.startsWith(section.to)
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How far down the page the header reaches — the navbar, plus the section row
|
|
33
|
+
* where there is one — as a length the layouts hand down in
|
|
34
|
+
* `--duxt-header-offset`.
|
|
35
|
+
*
|
|
36
|
+
* NOT `chrome`, which is what this was called. The word is right in the sense
|
|
37
|
+
* an interface means it and wrong in the sense most readers reach for first,
|
|
38
|
+
* and a name that has to be explained is a name chosen badly.
|
|
39
|
+
* `DuxtLiveWindow`'s `chrome` prop keeps it, because there it really is a
|
|
40
|
+
* drawn browser bar.
|
|
41
|
+
*
|
|
42
|
+
* A CSS variable rather than a class per case: the sticky columns live in
|
|
43
|
+
* three layouts and in the page's own contents column, and four copies of a
|
|
44
|
+
* pair of arbitrary values is four places to forget one.
|
|
45
|
+
*/
|
|
46
|
+
const headerOffset = computed(() => (visible.value ? '6.5rem' : '3.75rem'));
|
|
47
|
+
|
|
48
|
+
return { sections, visible, headerOffset };
|
|
49
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place that knows which keys the theme listens to.
|
|
3
|
+
*
|
|
4
|
+
* The overlay on `?` and the handlers themselves have to agree, and the only
|
|
5
|
+
* way they do is by being the same list — a shortcut sheet that lists a key
|
|
6
|
+
* nothing binds is worse than no sheet.
|
|
7
|
+
*
|
|
8
|
+
* A shortcut never fires while the reader is typing. Without that guard, `?`
|
|
9
|
+
* in the search box opens the overlay instead of being searched for, which is
|
|
10
|
+
* the single most common bug in this kind of feature.
|
|
11
|
+
*/
|
|
12
|
+
export interface DuxtShortcut {
|
|
13
|
+
/** The key, as `KeyboardEvent.key` reports it. */
|
|
14
|
+
key: string;
|
|
15
|
+
/** Requires ⌘ on a Mac, Ctrl elsewhere. */
|
|
16
|
+
meta?: boolean;
|
|
17
|
+
/** i18n key for what it does. */
|
|
18
|
+
label: string;
|
|
19
|
+
/** How to draw it. */
|
|
20
|
+
keys: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const duxtShortcuts: DuxtShortcut[] = [
|
|
24
|
+
{ key: 'k', meta: true, label: 'duxt.shortcuts.search', keys: ['⌘', 'K'] },
|
|
25
|
+
{ key: '?', label: 'duxt.shortcuts.help', keys: ['?'] },
|
|
26
|
+
{ key: '[', label: 'duxt.shortcuts.previous', keys: ['['] },
|
|
27
|
+
{ key: ']', label: 'duxt.shortcuts.next', keys: [']'] }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Is the reader typing? Then the key belongs to whatever they are typing in. */
|
|
31
|
+
export function isTyping(target: EventTarget | null): boolean {
|
|
32
|
+
const element = target as HTMLElement | null;
|
|
33
|
+
if (!element) return false;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
element.isContentEditable ||
|
|
37
|
+
['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Bind one key, with the guards every one of them needs. */
|
|
42
|
+
export function onDuxtShortcut(
|
|
43
|
+
match: (event: KeyboardEvent) => boolean,
|
|
44
|
+
run: (event: KeyboardEvent) => void
|
|
45
|
+
) {
|
|
46
|
+
onMounted(() => {
|
|
47
|
+
const handler = (event: KeyboardEvent) => {
|
|
48
|
+
if (isTyping(event.target)) return;
|
|
49
|
+
if (!match(event)) return;
|
|
50
|
+
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
run(event);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
window.addEventListener('keydown', handler);
|
|
56
|
+
onBeforeUnmount(() => window.removeEventListener('keydown', handler));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site's own origin, and paths made absolute against it.
|
|
3
|
+
*
|
|
4
|
+
* duxt cannot guess this — a layer has no idea what domain it will be served
|
|
5
|
+
* from — so it reads `i18n.baseUrl`, the one place a Nuxt site already has to
|
|
6
|
+
* state it for hreflang. Where it is unset, `absolute()` hands back the path
|
|
7
|
+
* unchanged: a relative `canonical` is valid and resolves correctly, whereas a
|
|
8
|
+
* guessed origin in a `canonical` is actively wrong. Same stance `app.vue`
|
|
9
|
+
* takes on the alternate links, for the same reason.
|
|
10
|
+
*
|
|
11
|
+
* Set `NUXT_PUBLIC_I18N_BASE_URL` in the deployment, or `i18n.baseUrl` in the
|
|
12
|
+
* consumer's nuxt.config.
|
|
13
|
+
*/
|
|
14
|
+
export function useDuxtSiteUrl() {
|
|
15
|
+
const runtime = useRuntimeConfig().public as {
|
|
16
|
+
i18n?: { baseUrl?: string };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const origin = computed(
|
|
20
|
+
() => runtime.i18n?.baseUrl?.replace(/\/+$/, '') ?? ''
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const absolute = (path: string) =>
|
|
24
|
+
origin.value ? `${origin.value}${path}` : path;
|
|
25
|
+
|
|
26
|
+
return { origin, absolute };
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { toast } from 'vue-sonner';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Toasts, wrapped so components never import vue-sonner directly.
|
|
5
|
+
*
|
|
6
|
+
* The wrapper is the seam: a consumer that wants a different notification
|
|
7
|
+
* system replaces this one composable instead of every call site, and the
|
|
8
|
+
* layer's own components keep working. Each level maps to Sonner's own type,
|
|
9
|
+
* which picks the icon and its status colour.
|
|
10
|
+
*/
|
|
11
|
+
export function useDuxtToast() {
|
|
12
|
+
return {
|
|
13
|
+
success: (message: string, description?: string) =>
|
|
14
|
+
toast.success(message, { description }),
|
|
15
|
+
info: (message: string, description?: string) =>
|
|
16
|
+
toast.info(message, { description }),
|
|
17
|
+
warning: (message: string, description?: string) =>
|
|
18
|
+
toast.warning(message, { description }),
|
|
19
|
+
error: (message: string, description?: string) =>
|
|
20
|
+
toast.error(message, { description }),
|
|
21
|
+
/** A toast that stays until the promise settles. */
|
|
22
|
+
loading: (message: string) => toast.loading(message),
|
|
23
|
+
dismiss: (id?: number | string) => toast.dismiss(id)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the reader needs to be told about the version they are in.
|
|
3
|
+
*
|
|
4
|
+
* Three questions, one answer, because they are the same question asked by
|
|
5
|
+
* different parts of the page: the banner needs to know whether this version is
|
|
6
|
+
* still the one to read, the head needs to know whether a crawler should index
|
|
7
|
+
* it, and the switcher needs a badge per entry. All three follow from the
|
|
8
|
+
* resolved source manifest — which version serves this path, which one serves
|
|
9
|
+
* it without a prefix, and what the config says each is worth.
|
|
10
|
+
*/
|
|
11
|
+
export function useDuxtVersion() {
|
|
12
|
+
const duxt = useDuxtConfig();
|
|
13
|
+
const path = useDuxtPath();
|
|
14
|
+
|
|
15
|
+
const sources = computed(() => duxt.resolvedSources ?? []);
|
|
16
|
+
|
|
17
|
+
/** The source serving this route. */
|
|
18
|
+
const current = computed(() => sourceForPath(path.value, sources.value));
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The version to send the reader to instead — the default of the SAME
|
|
22
|
+
* repository. Another project's default says nothing about this one.
|
|
23
|
+
*
|
|
24
|
+
* A VERSION-NEUTRAL GENERATED SECTION is its own answer. A changelog is one
|
|
25
|
+
* global history served at a URL with no version in it, so there is no other
|
|
26
|
+
* version of it to prefer: left to the search below it would have matched the
|
|
27
|
+
* documentation's default instead, and the page would then have carried
|
|
28
|
+
* `noindex` and a canonical pointing at a URL that does not exist.
|
|
29
|
+
*/
|
|
30
|
+
const preferred = computed(() =>
|
|
31
|
+
current.value?.generated?.versioning === 'global'
|
|
32
|
+
? current.value
|
|
33
|
+
: sources.value.find(
|
|
34
|
+
(source) =>
|
|
35
|
+
source.isDefault &&
|
|
36
|
+
source.generated?.versioning !== 'global' &&
|
|
37
|
+
source.repo === current.value?.repo &&
|
|
38
|
+
// The default of the same ARTEFACT — see `sameArtefact`. Without
|
|
39
|
+
// it a reference page's canonical pointed at the documentation's
|
|
40
|
+
// default, which is a different document at a different URL.
|
|
41
|
+
sameArtefact(source, current.value)
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** Is this the version a first-time reader should be on? */
|
|
46
|
+
const isPreferred = computed(
|
|
47
|
+
() => !current.value || current.value === preferred.value
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const status = computed(() => current.value?.status ?? 'current');
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The versions the switcher may offer — see `versionChoices`.
|
|
54
|
+
*
|
|
55
|
+
* Here rather than in `DuxtVersion` because the mobile sheet asks the same
|
|
56
|
+
* question to decide WHERE the version goes: a full-width select in its own
|
|
57
|
+
* strip when there is a choice, a badge beside the brand when there is not.
|
|
58
|
+
*/
|
|
59
|
+
const choices = computed(() =>
|
|
60
|
+
versionChoices(sources.value, current.value, duxt.versions)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/** Is there anything to switch to? */
|
|
64
|
+
const switchable = computed(() => choices.value.length > 1);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* WHICH KIND of "not the current version" this is.
|
|
68
|
+
*
|
|
69
|
+
* `isDefault` says only "not the one served by default", and that covers two
|
|
70
|
+
* readers who need opposite things told: someone on a release from two years
|
|
71
|
+
* ago should be told to upgrade, and someone on the branch documenting next
|
|
72
|
+
* month's should be told that what they are reading may still change. Telling
|
|
73
|
+
* the second one to upgrade is exactly backwards, which is what the banner
|
|
74
|
+
* did before this.
|
|
75
|
+
*
|
|
76
|
+
* The config wins where it speaks — `status: 'upcoming'` on a branch called
|
|
77
|
+
* `next` is a fact only the maintainer has. Where it does not, semver decides,
|
|
78
|
+
* and where semver cannot (a branch beside a tag) the version is treated as
|
|
79
|
+
* old: a tag next to a default branch is almost always a past release, and
|
|
80
|
+
* the maintainer can say otherwise in one word.
|
|
81
|
+
*/
|
|
82
|
+
const kind = computed<'current' | 'upcoming' | 'old' | 'deprecated' | 'eol'>(
|
|
83
|
+
() => {
|
|
84
|
+
if (status.value === 'eol') return 'eol';
|
|
85
|
+
if (status.value === 'deprecated') return 'deprecated';
|
|
86
|
+
if (status.value === 'upcoming') return 'upcoming';
|
|
87
|
+
if (isPreferred.value) return 'current';
|
|
88
|
+
|
|
89
|
+
return versionRelation(
|
|
90
|
+
current.value?.version,
|
|
91
|
+
preferred.value?.version
|
|
92
|
+
) === 'newer'
|
|
93
|
+
? 'upcoming'
|
|
94
|
+
: 'old';
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A crawler is as likely to serve v0.7.0 as the current docs, and nothing on
|
|
100
|
+
* the page says which it is. So an older or dead version points at the
|
|
101
|
+
* current one and asks not to be indexed itself — `follow`, because the links
|
|
102
|
+
* out of it are still worth crawling.
|
|
103
|
+
*/
|
|
104
|
+
const shouldIndex = computed(
|
|
105
|
+
() => isPreferred.value && status.value !== 'eol'
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/** The same page in the preferred version, for the banner and the canonical. */
|
|
109
|
+
const preferredPath = computed(() =>
|
|
110
|
+
versionPath(
|
|
111
|
+
path.value,
|
|
112
|
+
current.value?.prefix,
|
|
113
|
+
preferred.value?.prefix || '/'
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const shouldWarn = computed(
|
|
118
|
+
() =>
|
|
119
|
+
Boolean(current.value?.version) &&
|
|
120
|
+
(!isPreferred.value ||
|
|
121
|
+
status.value === 'deprecated' ||
|
|
122
|
+
status.value === 'eol')
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
sources,
|
|
127
|
+
choices,
|
|
128
|
+
switchable,
|
|
129
|
+
current,
|
|
130
|
+
preferred,
|
|
131
|
+
isPreferred,
|
|
132
|
+
status,
|
|
133
|
+
kind,
|
|
134
|
+
shouldIndex,
|
|
135
|
+
shouldWarn,
|
|
136
|
+
preferredPath
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type Fuse from 'fuse.js';
|
|
2
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
3
|
+
|
|
4
|
+
/** One entry of Content's search index: a page, or a heading inside one. */
|
|
5
|
+
export interface DuxtSearchSection {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
titles: string[];
|
|
9
|
+
level: number;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Typo tolerance beside Content's FTS index.
|
|
15
|
+
*
|
|
16
|
+
* FTS5 matches terms and prefixes, never near-misses, so a single wrong letter
|
|
17
|
+
* returns nothing at all. Fuse matches approximately over the same sections the
|
|
18
|
+
* database indexes.
|
|
19
|
+
*
|
|
20
|
+
* Both the library and the sections are loaded on the first query the database
|
|
21
|
+
* cannot answer, not when the dialog opens: a reader who types accurately never
|
|
22
|
+
* downloads either. The sections come through Content's client database, which
|
|
23
|
+
* the FTS index has already loaded by then, so the fallback costs no second
|
|
24
|
+
* payload — only Fuse itself.
|
|
25
|
+
*/
|
|
26
|
+
export function useFuzzySearch(
|
|
27
|
+
/**
|
|
28
|
+
* Which collection to fall back over. A versioned site names its collections
|
|
29
|
+
* after the prefix they serve, so this follows `useDuxtCollection()` once the
|
|
30
|
+
* dialog does — it is passed rather than resolved here to keep the exact and
|
|
31
|
+
* the approximate pass reading the same one.
|
|
32
|
+
*/
|
|
33
|
+
collection: MaybeRefOrGetter<DuxtCollectionName> = 'docs' as DuxtCollectionName,
|
|
34
|
+
options?: { ignoredTags?: string[] }
|
|
35
|
+
) {
|
|
36
|
+
let index: Promise<Fuse<DuxtSearchSection>> | undefined;
|
|
37
|
+
let indexedFor: string | undefined;
|
|
38
|
+
|
|
39
|
+
async function build(name: DuxtCollectionName) {
|
|
40
|
+
const [{ default: Fuse }, sections] = await Promise.all([
|
|
41
|
+
import('fuse.js'),
|
|
42
|
+
queryCollectionSearchSections(name as DuxtCollectionArg, options)
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
return new Fuse(sections as DuxtSearchSection[], {
|
|
46
|
+
// A hit in the heading outranks one buried in a paragraph, and the
|
|
47
|
+
// headings above a section still say more than its body text.
|
|
48
|
+
keys: [
|
|
49
|
+
{ name: 'title', weight: 3 },
|
|
50
|
+
{ name: 'titles', weight: 2 },
|
|
51
|
+
{ name: 'content', weight: 1 }
|
|
52
|
+
],
|
|
53
|
+
|
|
54
|
+
// By default Fuse scores a match by how near the start of the field it
|
|
55
|
+
// sits, which says nothing about a paragraph — only the quality of the
|
|
56
|
+
// match should count here.
|
|
57
|
+
ignoreLocation: true,
|
|
58
|
+
|
|
59
|
+
// Loose enough to survive a transposed letter, tight enough that a short
|
|
60
|
+
// term does not drag in half the documentation.
|
|
61
|
+
threshold: 0.35,
|
|
62
|
+
minMatchCharLength: 3
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function search(term: string, limit = 20) {
|
|
67
|
+
const name = toValue(collection);
|
|
68
|
+
|
|
69
|
+
// Built on first use, and again when the route moves to another collection
|
|
70
|
+
// — otherwise a versioned site would keep answering out of the version the
|
|
71
|
+
// reader happened to arrive from.
|
|
72
|
+
if (!index || name !== indexedFor) {
|
|
73
|
+
indexedFor = name;
|
|
74
|
+
index = build(name);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fuse = await index;
|
|
78
|
+
|
|
79
|
+
return fuse.search(term, { limit }).map((hit) => hit.item);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { search };
|
|
83
|
+
}
|