@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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon for a filename or a language id.
|
|
3
|
+
*
|
|
4
|
+
* The vscode-icons collection is the one that actually has an icon per
|
|
5
|
+
* ecosystem; lucide covers the fallbacks. Used by both the code block header
|
|
6
|
+
* and the file tree, so a `.ts` file looks the same in either.
|
|
7
|
+
*/
|
|
8
|
+
const byExtension: Record<string, string> = {
|
|
9
|
+
bash: 'vscode-icons:file-type-shell',
|
|
10
|
+
css: 'vscode-icons:file-type-css',
|
|
11
|
+
env: 'vscode-icons:file-type-dotenv',
|
|
12
|
+
go: 'vscode-icons:file-type-go',
|
|
13
|
+
html: 'vscode-icons:file-type-html',
|
|
14
|
+
jpg: 'vscode-icons:file-type-image',
|
|
15
|
+
javascript: 'vscode-icons:file-type-js',
|
|
16
|
+
js: 'vscode-icons:file-type-js',
|
|
17
|
+
json: 'vscode-icons:file-type-json',
|
|
18
|
+
jsonc: 'vscode-icons:file-type-json',
|
|
19
|
+
md: 'vscode-icons:file-type-markdown',
|
|
20
|
+
mdc: 'vscode-icons:file-type-markdown',
|
|
21
|
+
mjs: 'vscode-icons:file-type-js',
|
|
22
|
+
php: 'vscode-icons:file-type-php',
|
|
23
|
+
png: 'vscode-icons:file-type-image',
|
|
24
|
+
python: 'vscode-icons:file-type-python',
|
|
25
|
+
py: 'vscode-icons:file-type-python',
|
|
26
|
+
rs: 'vscode-icons:file-type-rust',
|
|
27
|
+
sh: 'vscode-icons:file-type-shell',
|
|
28
|
+
svg: 'vscode-icons:file-type-svg',
|
|
29
|
+
ts: 'vscode-icons:file-type-typescript',
|
|
30
|
+
typescript: 'vscode-icons:file-type-typescript',
|
|
31
|
+
toml: 'vscode-icons:file-type-toml',
|
|
32
|
+
vue: 'vscode-icons:file-type-vue',
|
|
33
|
+
yaml: 'vscode-icons:file-type-yaml',
|
|
34
|
+
yml: 'vscode-icons:file-type-yaml'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Whole filenames worth their own icon, checked before the extension. */
|
|
38
|
+
const byName: Record<string, string> = {
|
|
39
|
+
'package.json': 'vscode-icons:file-type-npm',
|
|
40
|
+
'pnpm-lock.yaml': 'vscode-icons:file-type-pnpm',
|
|
41
|
+
'pnpm-workspace.yaml': 'vscode-icons:file-type-pnpm',
|
|
42
|
+
'nuxt.config.ts': 'vscode-icons:file-type-nuxt',
|
|
43
|
+
'content.config.ts': 'vscode-icons:file-type-nuxt',
|
|
44
|
+
'app.config.ts': 'vscode-icons:file-type-nuxt',
|
|
45
|
+
'tsconfig.json': 'vscode-icons:file-type-tsconfig',
|
|
46
|
+
dockerfile: 'vscode-icons:file-type-docker',
|
|
47
|
+
'.gitignore': 'vscode-icons:file-type-git'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function fileIcon(input?: string, fallback = 'lucide:file') {
|
|
51
|
+
if (!input) return fallback;
|
|
52
|
+
|
|
53
|
+
const name = input.split('/').pop()?.toLowerCase() ?? '';
|
|
54
|
+
if (byName[name]) return byName[name];
|
|
55
|
+
|
|
56
|
+
const extension = name.includes('.') ? name.split('.').pop()! : name;
|
|
57
|
+
return byExtension[extension] ?? fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function folderIcon(open = false) {
|
|
61
|
+
return open ? 'lucide:folder-open' : 'lucide:folder';
|
|
62
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the app does with a generated section.
|
|
3
|
+
*
|
|
4
|
+
* A section is an ordinary collection at a prefix of its own, so almost nothing
|
|
5
|
+
* in the theme has to know it exists — the collection lookup, the search, the
|
|
6
|
+
* breadcrumb and the navigation tree all work off the manifest and were not
|
|
7
|
+
* touched. Two things do not follow from the collection, and they are here:
|
|
8
|
+
* WHERE its navbar entry goes, and WHICH LAYOUT its pages render in.
|
|
9
|
+
*
|
|
10
|
+
* Pure, and beside the resolver's other path arithmetic for the same reason:
|
|
11
|
+
* every version bug this layer has had came from prefix work done inline in a
|
|
12
|
+
* component, where it could only be checked by clicking.
|
|
13
|
+
*/
|
|
14
|
+
// Named rather than auto-imported: a test runs this file outside Nuxt, where
|
|
15
|
+
// nothing fills the auto-imports in.
|
|
16
|
+
import { sourceForPath } from './version-paths';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The config with each generated section's navbar entry in place.
|
|
20
|
+
*
|
|
21
|
+
* Appended here rather than written into `appConfig` by the build, and that is
|
|
22
|
+
* not a detail: the generated template merges `appConfig` BEHIND every
|
|
23
|
+
* `app.config.ts`, and `mergeDuxtConfig` replaces an array rather than
|
|
24
|
+
* appending to it — so a consumer naming `sections` at all would have replaced
|
|
25
|
+
* the entry the build had just added, and the section would be unreachable on
|
|
26
|
+
* exactly the sites that configure the most.
|
|
27
|
+
*
|
|
28
|
+
* A section already listed by hand is left alone, so a consumer who wants it
|
|
29
|
+
* somewhere else in the row simply writes it there. "Listed by hand" is asked
|
|
30
|
+
* of the SECTION rather than of one URL: the consumer wrote the entry at the
|
|
31
|
+
* position they were reading, and a `per-version` section is at a different URL
|
|
32
|
+
* on every other version — so the per-URL question answered no there and
|
|
33
|
+
* appended a second entry with the same label.
|
|
34
|
+
*
|
|
35
|
+
* ONE DECLARATION IS ONE ENTRY, whatever it resolved to. A section is a
|
|
36
|
+
* collection per version and per locale, so `per-version` and `per-locale`
|
|
37
|
+
* reach here as several manifest entries for the same declared section — and
|
|
38
|
+
* the row is "the top-level parts of the documentation", not a list of the
|
|
39
|
+
* versions of one. So the entry is resolved against the section the reader is
|
|
40
|
+
* currently in, exactly as `DuxtVersion` and `useDuxtVersion` resolve theirs,
|
|
41
|
+
* with a de-dupe by `to` underneath it: two entries pointing at one URL are one
|
|
42
|
+
* link twice, and under `DuxtHeader`'s label key, one Vue key twice.
|
|
43
|
+
*
|
|
44
|
+
* Which entries are one declaration is READ OFF THE MANIFEST, never guessed —
|
|
45
|
+
* see `DuxtGeneratedMeta.declaration` for what guessing it cost.
|
|
46
|
+
*/
|
|
47
|
+
export function withGeneratedSections(
|
|
48
|
+
config: DuxtConfig,
|
|
49
|
+
path = '/'
|
|
50
|
+
): DuxtConfig {
|
|
51
|
+
const sources = config.resolvedSources ?? [];
|
|
52
|
+
const declarations = declarationsIn(sources);
|
|
53
|
+
|
|
54
|
+
if (!declarations.length) return config;
|
|
55
|
+
|
|
56
|
+
const base = readerBase(path, sources);
|
|
57
|
+
|
|
58
|
+
const entriesFor = (
|
|
59
|
+
placement: DuxtSectionPlacementInput,
|
|
60
|
+
existing: DuxtLink[]
|
|
61
|
+
) => {
|
|
62
|
+
// Seeded from what the consumer wrote and UPDATED as entries are added, so
|
|
63
|
+
// the de-dupe holds among the generated entries as well as against the
|
|
64
|
+
// hand-written ones. Building it once before the map was the bug: it
|
|
65
|
+
// answered for the consumer's list only.
|
|
66
|
+
const taken = new Set(existing.map((entry) => entry.to).filter(Boolean));
|
|
67
|
+
const entries: DuxtLink[] = [];
|
|
68
|
+
|
|
69
|
+
for (const declaration of declarations) {
|
|
70
|
+
const meta = declaration[0]!.generated!;
|
|
71
|
+
if (meta.navigation !== placement) continue;
|
|
72
|
+
|
|
73
|
+
// ANY position of this declaration counts as already in the row, not just
|
|
74
|
+
// the one this reader would be given: a section listed by hand is listed,
|
|
75
|
+
// and the entry `to` resolves to is always one of these prefixes, so this
|
|
76
|
+
// is the de-dupe by URL as well.
|
|
77
|
+
if (declaration.some((entry) => taken.has(entry.prefix))) continue;
|
|
78
|
+
|
|
79
|
+
const to = entryPath(declaration, base);
|
|
80
|
+
|
|
81
|
+
taken.add(to);
|
|
82
|
+
entries.push({ label: meta.label, to, icon: meta.icon });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return entries;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const navigation = entriesFor('navigation', config.navigation ?? []);
|
|
89
|
+
const sections = entriesFor('sections', config.sections ?? []);
|
|
90
|
+
|
|
91
|
+
if (!navigation.length && !sections.length) return config;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...config,
|
|
95
|
+
// Only the row that gains something is rewritten: an empty array where the
|
|
96
|
+
// config had `undefined` is a different value, and the header reads both.
|
|
97
|
+
...(navigation.length
|
|
98
|
+
? { navigation: [...(config.navigation ?? []), ...navigation] }
|
|
99
|
+
: {}),
|
|
100
|
+
...(sections.length
|
|
101
|
+
? { sections: [...(config.sections ?? []), ...sections] }
|
|
102
|
+
: {})
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The manifest's generated entries, grouped into the declarations that made
|
|
108
|
+
* them, in the order the site declared them.
|
|
109
|
+
*
|
|
110
|
+
* A `Map` rather than a sort: the manifest already lists a declaration's
|
|
111
|
+
* entries together, and the grouping has to survive it not doing so.
|
|
112
|
+
*/
|
|
113
|
+
function declarationsIn(sources: DuxtResolvedSource[]): DuxtResolvedSource[][] {
|
|
114
|
+
const groups = new Map<number, DuxtResolvedSource[]>();
|
|
115
|
+
|
|
116
|
+
for (const source of sources) {
|
|
117
|
+
if (!source.generated) continue;
|
|
118
|
+
|
|
119
|
+
const group = groups.get(source.generated.declaration);
|
|
120
|
+
if (group) group.push(source);
|
|
121
|
+
else groups.set(source.generated.declaration, [source]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return [...groups.values()];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The prefix the reader's DOCUMENTATION sits at — their repository and their
|
|
129
|
+
* version, with no section segment on the end.
|
|
130
|
+
*
|
|
131
|
+
* Inside a generated section the reader's version can only be read off that
|
|
132
|
+
* section's own prefix, so its slug is taken back off. `/v1.x/releases` is a
|
|
133
|
+
* reader on `v1.x`, and the row has to keep saying so.
|
|
134
|
+
*/
|
|
135
|
+
function readerBase(path: string, sources: DuxtResolvedSource[]): string {
|
|
136
|
+
const current = sourceForPath(path, sources);
|
|
137
|
+
if (!current) return '';
|
|
138
|
+
|
|
139
|
+
const own = current.generated ? `/${current.generated.slug}` : '';
|
|
140
|
+
|
|
141
|
+
return own && current.prefix.endsWith(own)
|
|
142
|
+
? current.prefix.slice(0, -own.length)
|
|
143
|
+
: current.prefix;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Where one declared section's navbar entry points, from where the reader is.
|
|
148
|
+
*
|
|
149
|
+
* The entries handed in ARE the declaration — its versions and its languages,
|
|
150
|
+
* as the manifest recorded them. The reader's own base picks one of them.
|
|
151
|
+
*/
|
|
152
|
+
function entryPath(declaration: DuxtResolvedSource[], base: string): string {
|
|
153
|
+
const here = `${base}/${declaration[0]!.generated!.slug}`;
|
|
154
|
+
|
|
155
|
+
if (declaration.some((other) => other.prefix === here)) return here;
|
|
156
|
+
|
|
157
|
+
// Nothing at the reader's version: a `global` section has one entry and no
|
|
158
|
+
// version at all, and a version may simply have declared no artefact. The
|
|
159
|
+
// entry nearest the root is the default version's — the resolver adds a
|
|
160
|
+
// version segment for every version EXCEPT the one served without a prefix.
|
|
161
|
+
return declaration.reduce((nearest, other) =>
|
|
162
|
+
depth(other.prefix) < depth(nearest.prefix) ? other : nearest
|
|
163
|
+
).prefix;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** How many segments deep a prefix is; `''` is the root and therefore zero. */
|
|
167
|
+
const depth = (prefix: string) => (prefix ? prefix.split('/').length - 1 : 0);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The layout a route asks for, when it is inside a generated section.
|
|
171
|
+
*
|
|
172
|
+
* THE SHARED SLOT. Each type names its own layout and every type renders
|
|
173
|
+
* through this one lookup, which is what stops two types inventing two
|
|
174
|
+
* unrelated ways to escape the docs chrome. A type that names none renders in
|
|
175
|
+
* the ordinary chrome, and the lookup answers `undefined`.
|
|
176
|
+
*
|
|
177
|
+
* Filtered to the sections that name a layout before the prefix match, so a
|
|
178
|
+
* site whose types all render in the docs chrome does no work at all.
|
|
179
|
+
*/
|
|
180
|
+
export function generatedLayout(
|
|
181
|
+
path: string,
|
|
182
|
+
sources: DuxtResolvedSource[]
|
|
183
|
+
): string | undefined {
|
|
184
|
+
const withLayout = sources.filter((source) => source.generated?.layout);
|
|
185
|
+
if (!withLayout.length) return undefined;
|
|
186
|
+
|
|
187
|
+
// Every entry here carries a prefix, so `sourceForPath` returns a genuine
|
|
188
|
+
// match or nothing — it has no prefixless source to fall back to.
|
|
189
|
+
return sourceForPath(path, withLayout)?.generated?.layout;
|
|
190
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contents column for a page whose headings a COMPONENT draws.
|
|
3
|
+
*
|
|
4
|
+
* Content builds `body.toc` from the Markdown headings it parsed, which is the
|
|
5
|
+
* whole outline of an ordinary page and none of the outline of a generated one:
|
|
6
|
+
* a release page's headings are the group names `ChangelogGroup` renders from a
|
|
7
|
+
* prop. Content sees a component call and nothing else, so `body.toc.links`
|
|
8
|
+
* comes back empty and the page had no contents column at all.
|
|
9
|
+
*
|
|
10
|
+
* So the outline is read back off the AST the page ships anyway — the same
|
|
11
|
+
* nodes `ContentRenderer` is about to draw, before it draws them. That keeps it
|
|
12
|
+
* SSR-safe and pure: no registry a child fills during render (which renders
|
|
13
|
+
* empty on the server and mismatches on hydration), and no second query.
|
|
14
|
+
*
|
|
15
|
+
* Anchors come from `changelog.ts`, which is also what the two components use,
|
|
16
|
+
* so a link here and the id it points at cannot drift apart.
|
|
17
|
+
*/
|
|
18
|
+
import { changelogAnchor, changelogLabel } from './changelog';
|
|
19
|
+
|
|
20
|
+
export interface DuxtGeneratedTocLink {
|
|
21
|
+
id: string;
|
|
22
|
+
text: string;
|
|
23
|
+
depth: number;
|
|
24
|
+
children?: DuxtGeneratedTocLink[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HEADING = /^h([23])$/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A prop, whichever spelling it arrived in.
|
|
31
|
+
*
|
|
32
|
+
* Content writes a non-string prop under a `:`-prefixed key holding JSON — the
|
|
33
|
+
* same encoding MDC uses in a document — so `count` is `":count"` and
|
|
34
|
+
* `releases` is a JSON string. Read both, because which one a prop takes
|
|
35
|
+
* depends on the value the parser gave it and not on the component.
|
|
36
|
+
*/
|
|
37
|
+
function prop(props: Record<string, unknown>, name: string): unknown {
|
|
38
|
+
if (name in props) return props[name];
|
|
39
|
+
|
|
40
|
+
const raw = props[`:${name}`];
|
|
41
|
+
if (typeof raw !== 'string') return raw;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The words a node shows, with its markup dropped. */
|
|
51
|
+
function text(node: unknown): string {
|
|
52
|
+
if (typeof node === 'string') return node;
|
|
53
|
+
if (!Array.isArray(node)) return '';
|
|
54
|
+
|
|
55
|
+
return node.slice(2).map(text).join('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Every entry the page's own AST names, in document order and flat.
|
|
60
|
+
*
|
|
61
|
+
* Depth follows what the page RENDERS rather than what the node is: a group is
|
|
62
|
+
* an `<h2>` on the release page, and a heading written under that group was
|
|
63
|
+
* promoted to `##` by the parser — so inside a group it is one level further
|
|
64
|
+
* in, or the column would claim both are siblings.
|
|
65
|
+
*/
|
|
66
|
+
function walk(nodes: unknown[], inside: boolean): DuxtGeneratedTocLink[] {
|
|
67
|
+
const links: DuxtGeneratedTocLink[] = [];
|
|
68
|
+
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
if (!Array.isArray(node)) continue;
|
|
71
|
+
|
|
72
|
+
const [tag, ...rest] = node as unknown[];
|
|
73
|
+
if (typeof tag !== 'string') continue;
|
|
74
|
+
|
|
75
|
+
const props = (
|
|
76
|
+
rest[0] && typeof rest[0] === 'object' ? rest[0] : {}
|
|
77
|
+
) as Record<string, unknown>;
|
|
78
|
+
const children = rest.slice(1);
|
|
79
|
+
|
|
80
|
+
if (tag === 'changelog-group') {
|
|
81
|
+
const name = String(prop(props, 'name') ?? '');
|
|
82
|
+
const id = changelogAnchor(name);
|
|
83
|
+
|
|
84
|
+
// The same words the heading itself draws — the anchor is still built
|
|
85
|
+
// from the file's own name, so the two cannot point apart.
|
|
86
|
+
if (id) links.push({ id, text: changelogLabel(name), depth: 2 });
|
|
87
|
+
links.push(...walk(children, true));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The release OVERVIEW contributes nothing. Its versions are already the
|
|
92
|
+
// whole visible page — a column repeating the six lines the reader is
|
|
93
|
+
// looking at is a second copy of the list, not a way around it. What is
|
|
94
|
+
// left there is the aside's own links and the page's provenance, which is
|
|
95
|
+
// why the column is drawn on a page with no outline at all.
|
|
96
|
+
if (tag === 'changelog-releases') continue;
|
|
97
|
+
|
|
98
|
+
const heading = HEADING.exec(tag);
|
|
99
|
+
if (heading) {
|
|
100
|
+
const id = props.id;
|
|
101
|
+
if (typeof id === 'string' && id) {
|
|
102
|
+
links.push({
|
|
103
|
+
id,
|
|
104
|
+
text: text(node).trim(),
|
|
105
|
+
depth: inside ? 3 : Number(heading[1])
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
links.push(...walk(children, inside));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return links;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The flat list nested the way `DuxtToc` draws it: one level of children. */
|
|
118
|
+
export function generatedToc(body: unknown): DuxtGeneratedTocLink[] {
|
|
119
|
+
const value = (body as { value?: unknown })?.value;
|
|
120
|
+
if (!Array.isArray(value)) return [];
|
|
121
|
+
|
|
122
|
+
const links: DuxtGeneratedTocLink[] = [];
|
|
123
|
+
|
|
124
|
+
for (const link of walk(value, false)) {
|
|
125
|
+
const parent = links.at(-1);
|
|
126
|
+
|
|
127
|
+
if (link.depth > 2 && parent) (parent.children ??= []).push(link);
|
|
128
|
+
else links.push({ ...link, depth: 2 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return links;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Does this page open on a heading of its own?
|
|
136
|
+
*
|
|
137
|
+
* The one question that decides whether a generated page gets the DOCS HEADER —
|
|
138
|
+
* breadcrumb, title, description, the copy control beside it. A type whose
|
|
139
|
+
* parser writes an `<h1>` has said it draws its own (an endpoint wants its
|
|
140
|
+
* title beside a method chip, which no shell could know); one that writes none
|
|
141
|
+
* has a title and a trail like every other page and should look like one.
|
|
142
|
+
*
|
|
143
|
+
* Read off the artefact rather than declared as a flag on the type: the two
|
|
144
|
+
* cannot then disagree, and the failure a flag would have — an `<h1>` from the
|
|
145
|
+
* parser under an `<h1>` from the shell — is not expressible.
|
|
146
|
+
*/
|
|
147
|
+
export function generatedTitle(body: unknown): boolean {
|
|
148
|
+
const value = (body as { value?: unknown })?.value;
|
|
149
|
+
if (!Array.isArray(value)) return false;
|
|
150
|
+
|
|
151
|
+
return value.some((node) => Array.isArray(node) && node[0] === 'h1');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The prose a generated page OPENS with, flattened to its words.
|
|
156
|
+
*
|
|
157
|
+
* The shell draws a page's description under its title, and for a generated
|
|
158
|
+
* page that was suppressed outright: Content derives a description from the
|
|
159
|
+
* body where the frontmatter names none, and the body then printed the same
|
|
160
|
+
* sentence again three lines below.
|
|
161
|
+
*
|
|
162
|
+
* Derived and written are indistinguishable once they are in `description`, so
|
|
163
|
+
* this answers the question that actually matters — does the body already open
|
|
164
|
+
* with it? A type that writes a real description (an API document's `summary`,
|
|
165
|
+
* which appears nowhere in its prose) gets the line every written page has; one
|
|
166
|
+
* whose description was lifted out of its own first paragraph does not.
|
|
167
|
+
*
|
|
168
|
+
* Walks INTO the first node, because a generated body opens on a component
|
|
169
|
+
* call: the prose is that component's children, not a sibling of it.
|
|
170
|
+
*/
|
|
171
|
+
export function generatedLead(body: unknown): string {
|
|
172
|
+
const value = (body as { value?: unknown })?.value;
|
|
173
|
+
if (!Array.isArray(value)) return '';
|
|
174
|
+
|
|
175
|
+
return collapse(lead(value));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Does the body already open with this description?
|
|
180
|
+
*
|
|
181
|
+
* `startsWith` rather than equality, because a page's description is often the
|
|
182
|
+
* FIRST LINE of prose the page then prints in full — the same sentence at two
|
|
183
|
+
* lengths, and printing both puts one above the rule and one under it.
|
|
184
|
+
*
|
|
185
|
+
* Both sides are collapsed first. Frontmatter folds a wrapped line into a
|
|
186
|
+
* space; the AST keeps the newline the author typed, so the two spellings of
|
|
187
|
+
* one sentence differ by exactly the whitespace nobody can see.
|
|
188
|
+
*/
|
|
189
|
+
export function generatedLeadsWith(
|
|
190
|
+
description: string,
|
|
191
|
+
body: unknown
|
|
192
|
+
): boolean {
|
|
193
|
+
const opening = generatedLead(body);
|
|
194
|
+
|
|
195
|
+
return Boolean(opening) && opening.startsWith(collapse(description));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One sentence, however it was wrapped. */
|
|
199
|
+
const collapse = (value: string) => value.replace(/\s+/g, ' ').trim();
|
|
200
|
+
|
|
201
|
+
/** The first paragraph's words, however deep the components go. */
|
|
202
|
+
function lead(nodes: unknown[]): string {
|
|
203
|
+
for (const node of nodes) {
|
|
204
|
+
if (!Array.isArray(node)) continue;
|
|
205
|
+
|
|
206
|
+
const [tag, ...rest] = node as unknown[];
|
|
207
|
+
if (typeof tag !== 'string') continue;
|
|
208
|
+
|
|
209
|
+
if (tag === 'p') return text(node).trim();
|
|
210
|
+
|
|
211
|
+
const found = lead(rest.slice(1));
|
|
212
|
+
if (found) return found;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this heading's own content already contain a link?
|
|
3
|
+
*
|
|
4
|
+
* A Prose heading wraps the WHOLE heading in an anchor, because the words are
|
|
5
|
+
* what a reader aims at when they want to link to a section. That cannot be
|
|
6
|
+
* done when the heading is itself a link — `## [1.4.0](…/compare/…)`, which is
|
|
7
|
+
* what release-please writes on every release, rendered as one page by the
|
|
8
|
+
* changelog's `flat` granularity. An anchor inside an anchor is invalid HTML:
|
|
9
|
+
* the parser closes the outer one early, and what is left is a link with no
|
|
10
|
+
* text in it. `pnpm check:a11y` found it as five `link-name` violations, which
|
|
11
|
+
* is the honest description — a link a screen reader announces as nothing.
|
|
12
|
+
*
|
|
13
|
+
* READ OFF THE VNODES rather than declared by the caller: a heading does not
|
|
14
|
+
* know what a Markdown file put inside it, and asking is the only way to be
|
|
15
|
+
* right for content nobody has written yet.
|
|
16
|
+
*
|
|
17
|
+
* `href` rather than the tag or the component: MDC renders a link as `ProseA`,
|
|
18
|
+
* a raw one arrives as `a`, and both carry the prop. Recursive, because a
|
|
19
|
+
* heading may wrap its link in emphasis.
|
|
20
|
+
*/
|
|
21
|
+
import type { VNode } from 'vue';
|
|
22
|
+
|
|
23
|
+
export function headingHasLink(nodes: unknown): boolean {
|
|
24
|
+
if (Array.isArray(nodes)) return nodes.some(headingHasLink);
|
|
25
|
+
if (!nodes || typeof nodes !== 'object') return false;
|
|
26
|
+
|
|
27
|
+
const node = nodes as VNode;
|
|
28
|
+
|
|
29
|
+
if (node.props && 'href' in node.props) return true;
|
|
30
|
+
|
|
31
|
+
return headingHasLink(node.children);
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The route path without its locale segment.
|
|
3
|
+
*
|
|
4
|
+
* With `prefix_except_default` the browser is on `/de-DE/guide/deploying` while
|
|
5
|
+
* the documentation page is `/guide/deploying` — the locale translates the
|
|
6
|
+
* INTERFACE, not the content tree. Every lookup in the theme means the second
|
|
7
|
+
* one: which collection serves this route, which section is active, which page
|
|
8
|
+
* to query. Handing them the raw path 404s every page in every non-default
|
|
9
|
+
* locale, which is exactly what happened the first time this was wired up.
|
|
10
|
+
*
|
|
11
|
+
* Pure and separate from the composable so the seam can be tested without a
|
|
12
|
+
* Nuxt environment, like the source resolver next to it.
|
|
13
|
+
*/
|
|
14
|
+
export function stripLocalePrefix(
|
|
15
|
+
path: string,
|
|
16
|
+
codes: Iterable<string>
|
|
17
|
+
): string {
|
|
18
|
+
return splitLocalePath(path, codes).path;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The same split, keeping the half that was thrown away.
|
|
23
|
+
*
|
|
24
|
+
* Which language a URL asks for is not decoration: original and translation
|
|
25
|
+
* live under IDENTICAL content paths in different collections, so a lookup
|
|
26
|
+
* given only the stripped path cannot tell them apart and takes whichever
|
|
27
|
+
* sorted first. Anything choosing a collection needs both halves — see
|
|
28
|
+
* `sourcesForRoute`.
|
|
29
|
+
*/
|
|
30
|
+
export function splitLocalePath(
|
|
31
|
+
path: string,
|
|
32
|
+
codes: Iterable<string>
|
|
33
|
+
): { locale?: string; path: string } {
|
|
34
|
+
const first = path.split('/')[1];
|
|
35
|
+
if (!first) return { path };
|
|
36
|
+
|
|
37
|
+
for (const code of codes) {
|
|
38
|
+
if (code !== first) continue;
|
|
39
|
+
|
|
40
|
+
const rest = path.slice(first.length + 1);
|
|
41
|
+
return { locale: code, path: rest.startsWith('/') ? rest : rest || '/' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { path };
|
|
45
|
+
}
|