@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,16 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A layout's answer to "is there room on the right?".
|
|
5
|
+
*
|
|
6
|
+
* `pages/[...slug].vue` draws the contents column, the aside's links and the
|
|
7
|
+
* page's provenance in one sticky column, and for almost every page that is
|
|
8
|
+
* right. A layout that fills the same side itself — `reference`, whose
|
|
9
|
+
* operations put the request client there — provides `false` and the page
|
|
10
|
+
* leaves the space alone.
|
|
11
|
+
*
|
|
12
|
+
* Provided rather than matched on a layout name: the layout is the only thing
|
|
13
|
+
* that knows what it draws, and a name in a list in the page is a rule that
|
|
14
|
+
* goes stale the first time a consumer ships a layout of their own.
|
|
15
|
+
*/
|
|
16
|
+
export const DUXT_ASIDE: InjectionKey<boolean> = Symbol('duxt-aside');
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a release history needs beyond the pages the parser writes: the anchor a
|
|
3
|
+
* group is linked at, the colour it is drawn in, and the day it was cut.
|
|
4
|
+
*
|
|
5
|
+
* Here rather than in the components because TWO PLACES have to agree about an
|
|
6
|
+
* id — the heading `ChangelogGroup` draws, and the contents column built from
|
|
7
|
+
* the page's own AST (`generated-toc.ts`), which never sees that component
|
|
8
|
+
* render. A slug computed twice is a contents column whose links go nowhere.
|
|
9
|
+
*
|
|
10
|
+
* Named exports rather than auto-imports read from a test: the same reason
|
|
11
|
+
* `generated-sections.ts` gives, and the same tests.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The anchor a group heading gets, or nothing when the name has no ASCII in it.
|
|
16
|
+
*
|
|
17
|
+
* Undefined rather than a fallback: a heading a component draws is not in the
|
|
18
|
+
* outline Content slugifies, so there is nothing upstream to fall back TO, and
|
|
19
|
+
* pointing every group of every release at `#` is worse than not linking one.
|
|
20
|
+
*/
|
|
21
|
+
export function changelogAnchor(name: string): string | undefined {
|
|
22
|
+
return (
|
|
23
|
+
name
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '') || undefined
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The mark a release tool puts in FRONT of a heading, dropped.
|
|
32
|
+
*
|
|
33
|
+
* `\p{S}` is symbols — the `⚠` release-please writes on its breaking block, an
|
|
34
|
+
* emoji a hand-kept changelog opens a section with. LEADING ONLY: a name that
|
|
35
|
+
* uses a symbol anywhere else keeps it, so `C++ Support` survives, and a
|
|
36
|
+
* heading that is nothing BUT a mark comes back untouched rather than emptied.
|
|
37
|
+
*
|
|
38
|
+
* The stored name stays whole either way. This is what a label SHOWS — never
|
|
39
|
+
* what the filter matches, what the anchor is built from, or what travels as a
|
|
40
|
+
* prop — which is also why the mark is drawn away rather than parsed away: the
|
|
41
|
+
* file said it, and a site that wants it back overrides the component instead
|
|
42
|
+
* of the changelog.
|
|
43
|
+
*/
|
|
44
|
+
export function changelogLabel(name: string): string {
|
|
45
|
+
return name.replace(/^[\p{S}\s]+/u, '').trim() || name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The classes one group is drawn with: a dot, a run of text, a filter chip. */
|
|
49
|
+
export interface DuxtChangelogTone {
|
|
50
|
+
/** A filled dot — the group's identity wherever the name is already there. */
|
|
51
|
+
dot: string;
|
|
52
|
+
/**
|
|
53
|
+
* The NAME set in the tone, for the label a release page heads its group
|
|
54
|
+
* with.
|
|
55
|
+
*
|
|
56
|
+
* A separate value rather than the dot's colour reused, because a filled
|
|
57
|
+
* shape and a run of text do not clear contrast at the same lightness: the
|
|
58
|
+
* `500` a 6-pixel dot is drawn in is well under the floor for words.
|
|
59
|
+
*
|
|
60
|
+
* `700` / `400` is the most saturated pair the ramp offers that still clears
|
|
61
|
+
* AA on both grounds — measured, not picked: on white, `600` fails for four
|
|
62
|
+
* of the six hues (amber 3.2, emerald 3.7, cyan 3.7, sky 4.1), so the light
|
|
63
|
+
* side stops at `700`. The dark side has the room, which is why it takes
|
|
64
|
+
* `400` rather than the `300` the chip uses: on the page background it
|
|
65
|
+
* measures 7.0–11.5 and on a card 6.3–10.4, and it is the half a reader of a
|
|
66
|
+
* dark site actually sees.
|
|
67
|
+
*/
|
|
68
|
+
text: string;
|
|
69
|
+
/** A tinted surface with a matching ring, for the filter chip. */
|
|
70
|
+
chip: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The palette, as `DuxtOpenApiMethod` builds one: Tailwind's ramp with a
|
|
75
|
+
* lighter text in dark mode, so every pair clears the contrast floor in both
|
|
76
|
+
* themes without adding six token pairs a consumer would have to override.
|
|
77
|
+
*/
|
|
78
|
+
const TONES: DuxtChangelogTone[] = [
|
|
79
|
+
{
|
|
80
|
+
dot: 'bg-emerald-500',
|
|
81
|
+
text: 'text-emerald-700 dark:text-emerald-400',
|
|
82
|
+
chip: 'bg-emerald-500/10 text-emerald-700 ring-emerald-500/30 dark:text-emerald-300'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
dot: 'bg-sky-500',
|
|
86
|
+
text: 'text-sky-700 dark:text-sky-400',
|
|
87
|
+
chip: 'bg-sky-500/10 text-sky-700 ring-sky-500/30 dark:text-sky-300'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
dot: 'bg-amber-500',
|
|
91
|
+
text: 'text-amber-700 dark:text-amber-400',
|
|
92
|
+
chip: 'bg-amber-500/10 text-amber-700 ring-amber-500/30 dark:text-amber-300'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
dot: 'bg-violet-500',
|
|
96
|
+
text: 'text-violet-700 dark:text-violet-400',
|
|
97
|
+
chip: 'bg-violet-500/10 text-violet-700 ring-violet-500/30 dark:text-violet-300'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
dot: 'bg-rose-500',
|
|
101
|
+
text: 'text-rose-700 dark:text-rose-400',
|
|
102
|
+
chip: 'bg-rose-500/10 text-rose-700 ring-rose-500/30 dark:text-rose-300'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
dot: 'bg-cyan-500',
|
|
106
|
+
text: 'text-cyan-700 dark:text-cyan-400',
|
|
107
|
+
chip: 'bg-cyan-500/10 text-cyan-700 ring-cyan-500/30 dark:text-cyan-300'
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The names that get the colour a reader expects, keyed by an English word the
|
|
113
|
+
* heading CONTAINS.
|
|
114
|
+
*
|
|
115
|
+
* A HINT, never a taxonomy — which is the whole difference from a mapping the
|
|
116
|
+
* parser would have needed. `sections-changelog.ts` takes group names verbatim
|
|
117
|
+
* because a fixed set fails silently on the first heading it does not know;
|
|
118
|
+
* here the failure is that a group is drawn cyan instead of green, and the
|
|
119
|
+
* fallback below still gives it a colour of its own. So the list stays short
|
|
120
|
+
* and covers what release-please and Keep a Changelog write, and a changelog
|
|
121
|
+
* kept in another language simply falls through to the hash.
|
|
122
|
+
*/
|
|
123
|
+
const BREAKING = /break|inkompat|incompat/i;
|
|
124
|
+
|
|
125
|
+
const HINTS: [RegExp, number][] = [
|
|
126
|
+
[BREAKING, 4],
|
|
127
|
+
[/feat|added|neu/i, 0],
|
|
128
|
+
[/fix|bug|behoben/i, 2],
|
|
129
|
+
[/perf/i, 3],
|
|
130
|
+
[/doc/i, 1],
|
|
131
|
+
[/deprecat|remov|revert/i, 4],
|
|
132
|
+
[/depend|chore|build|ci|refactor|misc/i, 5]
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A stable colour for a group name.
|
|
137
|
+
*
|
|
138
|
+
* The fallback is a hash rather than one grey for everything, because the point
|
|
139
|
+
* of the colour is that a reader scanning forty releases finds the same kind of
|
|
140
|
+
* change in the same colour every time — which a hash gives in any language,
|
|
141
|
+
* for any heading, with nothing configured.
|
|
142
|
+
*/
|
|
143
|
+
export function changelogTone(name: string): DuxtChangelogTone {
|
|
144
|
+
for (const [pattern, index] of HINTS) {
|
|
145
|
+
if (pattern.test(name)) return TONES[index]!;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let hash = 0;
|
|
149
|
+
for (const character of name) {
|
|
150
|
+
hash = (hash * 31 + character.codePointAt(0)!) % 0xffffffff;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return TONES[hash % TONES.length]!;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A release day, in the reader's language.
|
|
158
|
+
*
|
|
159
|
+
* `timeZone: 'UTC'` because the value is a calendar date and not a moment: read
|
|
160
|
+
* as local time, `2026-09-08` is the 7th for every reader west of Greenwich —
|
|
161
|
+
* and a different day on the server than in the browser, which is a hydration
|
|
162
|
+
* mismatch as well as a wrong date.
|
|
163
|
+
*
|
|
164
|
+
* Here rather than beside one of its callers because TWO of them draw it — the
|
|
165
|
+
* timeline on the overview and the meta row on the release page — and a date
|
|
166
|
+
* formatted in two places is two chances to print two different days for one
|
|
167
|
+
* release.
|
|
168
|
+
*/
|
|
169
|
+
export function changelogDate(
|
|
170
|
+
value: string | undefined,
|
|
171
|
+
locale: string
|
|
172
|
+
): string | undefined {
|
|
173
|
+
if (!value) return undefined;
|
|
174
|
+
|
|
175
|
+
const date = new Date(value);
|
|
176
|
+
if (Number.isNaN(date.getTime())) return undefined;
|
|
177
|
+
|
|
178
|
+
return new Intl.DateTimeFormat(locale, {
|
|
179
|
+
dateStyle: 'medium',
|
|
180
|
+
timeZone: 'UTC'
|
|
181
|
+
}).format(date);
|
|
182
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { grammars as generated } from '#build/duxt-grammars.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The generated map, annotated here rather than trusted from the declaration.
|
|
5
|
+
*
|
|
6
|
+
* `writeGrammars` writes both the module and its `.d.ts`, and the file cannot
|
|
7
|
+
* import Shiki's own types to describe itself with — so the shape is stated at
|
|
8
|
+
* the one place that uses it.
|
|
9
|
+
*/
|
|
10
|
+
const grammars = generated as Record<string, () => Promise<unknown>>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Highlight code the site builds at RUNTIME, with the themes the fences use.
|
|
14
|
+
*
|
|
15
|
+
* Content highlights every Markdown fence at build time, and three things on
|
|
16
|
+
* this site never pass through it: the package-manager block's command, which
|
|
17
|
+
* is assembled from a prop; the example bodies and responses the API reference
|
|
18
|
+
* derives from a schema; and the request samples the try-it client rewrites on
|
|
19
|
+
* every keystroke. All of them came out as flat grey text beside fences that
|
|
20
|
+
* were coloured, which reads as the highlighter being switched off.
|
|
21
|
+
*
|
|
22
|
+
* BUILT ON `shiki/core`, not on the bundled entry, because this one runs in the
|
|
23
|
+
* BROWSER too — the sample changes as the reader types, and the server cannot
|
|
24
|
+
* re-render it. The bundled entry carries every grammar Shiki ships; the core
|
|
25
|
+
* takes only what this site actually generates, plus the JavaScript regex
|
|
26
|
+
* engine, which is what keeps the WASM out of the chunk. Loaded on demand either
|
|
27
|
+
* way, and cached for the life of the process.
|
|
28
|
+
*
|
|
29
|
+
* THE GRAMMAR SET IS GENERATED, from the samples a site configured — see
|
|
30
|
+
* `writeGrammars` in `modules/config.ts`. It used to be three names hard-coded
|
|
31
|
+
* here, which was right while the samples were `curl` and `fetch` and wrong the
|
|
32
|
+
* moment `duxt.requestSamples` became a list a consumer writes: a Python sample
|
|
33
|
+
* needs a Python grammar, and nobody wants to ship one to a site that has no
|
|
34
|
+
* Python sample. Every language in the map is bytes a reader downloads, which is
|
|
35
|
+
* exactly why the map is not simply all of Shiki.
|
|
36
|
+
*
|
|
37
|
+
* `defaultColor: false` emits both themes as custom properties per token,
|
|
38
|
+
* matching how Content's own output is styled.
|
|
39
|
+
*/
|
|
40
|
+
type Highlighter = {
|
|
41
|
+
codeToHtml: (code: string, options: object) => string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Spellings that are not grammar names.
|
|
46
|
+
*
|
|
47
|
+
* Shiki resolves its own aliases once a grammar is loaded, but the map above is
|
|
48
|
+
* keyed by the names the samples declare — so a `ts` fence has to be pointed at
|
|
49
|
+
* `typescript` before the lookup, not after it.
|
|
50
|
+
*/
|
|
51
|
+
const aliases: Record<string, string> = {
|
|
52
|
+
curl: 'bash',
|
|
53
|
+
js: 'typescript',
|
|
54
|
+
javascript: 'typescript',
|
|
55
|
+
sh: 'bash',
|
|
56
|
+
shell: 'bash',
|
|
57
|
+
ts: 'typescript'
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let highlighter: Promise<Highlighter> | undefined;
|
|
61
|
+
|
|
62
|
+
async function load(): Promise<Highlighter> {
|
|
63
|
+
const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] =
|
|
64
|
+
await Promise.all([
|
|
65
|
+
import('shiki/core'),
|
|
66
|
+
import('shiki/engine/javascript')
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
return createHighlighterCore({
|
|
70
|
+
themes: [
|
|
71
|
+
import('shiki/themes/github-light.mjs'),
|
|
72
|
+
import('shiki/themes/github-dark.mjs')
|
|
73
|
+
],
|
|
74
|
+
// Cast because the generated module is written by `writeGrammars` and
|
|
75
|
+
// cannot import Shiki's `LanguageRegistration` to type itself with — the
|
|
76
|
+
// values ARE those modules, which is what the loaders resolve to.
|
|
77
|
+
langs: Object.values(grammars).map((grammar) => grammar()) as Parameters<
|
|
78
|
+
typeof createHighlighterCore
|
|
79
|
+
>[0]['langs'],
|
|
80
|
+
engine: createJavaScriptRegexEngine()
|
|
81
|
+
}) as Promise<Highlighter>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The language, when it is one this site generates and highlights.
|
|
86
|
+
*
|
|
87
|
+
* Undefined for everything else, which is how a block asks for plain text
|
|
88
|
+
* rather than for a grammar the core highlighter was never given: an unknown
|
|
89
|
+
* `lang` makes Shiki throw, and a response body labelled `xml` must not take
|
|
90
|
+
* the page with it.
|
|
91
|
+
*/
|
|
92
|
+
export function duxtCodeLang(lang?: string): string | undefined {
|
|
93
|
+
if (!lang) return undefined;
|
|
94
|
+
|
|
95
|
+
const value = lang.toLowerCase();
|
|
96
|
+
const resolved = aliases[value] ?? value;
|
|
97
|
+
|
|
98
|
+
return resolved in grammars ? resolved : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function highlightCode(
|
|
102
|
+
code: string,
|
|
103
|
+
lang = 'bash'
|
|
104
|
+
): Promise<string> {
|
|
105
|
+
// One highlighter for the process: loading the grammars per call would repeat
|
|
106
|
+
// the most expensive part of this for every command on the page.
|
|
107
|
+
highlighter ??= load();
|
|
108
|
+
|
|
109
|
+
const shiki = await highlighter;
|
|
110
|
+
|
|
111
|
+
return shiki.codeToHtml(code, {
|
|
112
|
+
lang,
|
|
113
|
+
themes: { light: 'github-light', dark: 'github-dark' },
|
|
114
|
+
defaultColor: false
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The layer's defaults, and how a consumer's config is merged over them.
|
|
3
|
+
*
|
|
4
|
+
* Nuxt merges app.config with defu, which CONCATENATES arrays: a consumer
|
|
5
|
+
* setting `navigation: [...]` would get its own entries plus the layer's, in
|
|
6
|
+
* that order, with no way to remove ours. Every list here would be unusable.
|
|
7
|
+
*
|
|
8
|
+
* So the layer ships no lists in app.config at all. It keeps them here and
|
|
9
|
+
* merges them itself, replacing arrays instead of appending to them — a
|
|
10
|
+
* consumer's list is the list. Objects still merge key by key, so overriding
|
|
11
|
+
* `footer.note` leaves `footer.columns` alone.
|
|
12
|
+
*/
|
|
13
|
+
export const duxtDefaults: DuxtConfig = {
|
|
14
|
+
// A placeholder, not a name. The layer cannot know what a site is called,
|
|
15
|
+
// and `title` is read by thirteen call sites — the title template, the 404,
|
|
16
|
+
// the brand, the OG card, schema.org, llms.txt and the feed — several of
|
|
17
|
+
// which interpolate it into a template literal and would print the word
|
|
18
|
+
// `undefined` rather than degrade. So a neutral, translated word stands in
|
|
19
|
+
// until a consumer sets its own. It names nobody, which is the whole test.
|
|
20
|
+
title: 'duxt.defaults.title',
|
|
21
|
+
|
|
22
|
+
// No `version`. The layer knows nothing about the state of somebody else's
|
|
23
|
+
// project, and a default made every site that extended it wear a number it
|
|
24
|
+
// had never set — in the hero pill and, for a site with at most one source,
|
|
25
|
+
// in the header badge beside the search. A site that has a version says so
|
|
26
|
+
// in its own `app.config.ts`; `DuxtVersion` draws nothing until it does.
|
|
27
|
+
|
|
28
|
+
navigation: [
|
|
29
|
+
// No `to`: the header resolves it to the first section, so the entry works
|
|
30
|
+
// whether or not the consumer's URLs carry a prefix.
|
|
31
|
+
//
|
|
32
|
+
// One entry where `sections` and `links` ship empty, because "Docs" names
|
|
33
|
+
// the INTERFACE and not a tree or a repository. The dropdown that used to
|
|
34
|
+
// sit beside it pointed at duxt's own tech stack — five links belonging to
|
|
35
|
+
// this project and to no site that extends it. It lives in
|
|
36
|
+
// `www/app/app.config.ts` now.
|
|
37
|
+
{ label: 'duxt.defaults.navigation.docs', icon: 'lucide:book-open-text' }
|
|
38
|
+
],
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Empty, like `links` below — and for a reason one step further out.
|
|
42
|
+
*
|
|
43
|
+
* The section row names the top-level parts of a documentation TREE, and the
|
|
44
|
+
* tree belongs to the consumer. Shipping "Get started / Concepts / Guides /
|
|
45
|
+
* Reference" pointing at `/getting-started`, `/concepts`, … was the layer
|
|
46
|
+
* guessing at somebody else's folder names: a site whose docs are shaped
|
|
47
|
+
* differently got four tabs leading nowhere, and every one of them looked
|
|
48
|
+
* like a bug in duxt rather than a default it had never been told to change.
|
|
49
|
+
*
|
|
50
|
+
* Those labels were duxt's OWN documentation showing through the layer, so
|
|
51
|
+
* their translations left with them — `duxt.defaults.*` translates the interface
|
|
52
|
+
* layer draws, never content a site writes.
|
|
53
|
+
*
|
|
54
|
+
* Left empty, the row does not render and the sidebar falls back to the whole
|
|
55
|
+
* tree, which is the right shape for a site that has not split its docs into
|
|
56
|
+
* sections at all. duxt's own sections live in `www/app/app.config.ts`.
|
|
57
|
+
*/
|
|
58
|
+
sections: [],
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Empty, like `footer.legal`.
|
|
62
|
+
*
|
|
63
|
+
* These are the icon links on the right of the navbar, and they belong to
|
|
64
|
+
* whoever runs the site. Shipping duxt's own repository here gave a stranger
|
|
65
|
+
* extending the layer a GitHub icon pointing at somebody else's project —
|
|
66
|
+
* the same mistake the legal row already settled. kirchDev's own links live
|
|
67
|
+
* in `www/app/app.config.ts`, where they are an example rather than a
|
|
68
|
+
* default.
|
|
69
|
+
*
|
|
70
|
+
* Their i18n keys went with them. `duxt.defaults.*` translates the defaults
|
|
71
|
+
* the layer itself ships and nothing else — it is not a vocabulary for
|
|
72
|
+
* consumers to reach into. A consumer that did would depend on a key it
|
|
73
|
+
* cannot see being renamed, and i18n answers a missing key by printing the
|
|
74
|
+
* key, so the break would be silent. Consumers write a literal, their own
|
|
75
|
+
* key, or the record form; `www` is the worked example of the third.
|
|
76
|
+
*/
|
|
77
|
+
links: [],
|
|
78
|
+
|
|
79
|
+
/** Which package managers a command block offers, in the order it shows them. */
|
|
80
|
+
packageManagers: ['pnpm', 'npm', 'yarn', 'bun'],
|
|
81
|
+
|
|
82
|
+
/** A flat docs tree gets a trail that only repeats its own section name. */
|
|
83
|
+
breadcrumb: true,
|
|
84
|
+
|
|
85
|
+
landing: {
|
|
86
|
+
// No badge, no headline, no description. A pill above the headline says
|
|
87
|
+
// something about the state of a project — "beta", "v2 is out" — and the
|
|
88
|
+
// headline and the paragraph under it say what the project IS. The layer
|
|
89
|
+
// knows none of the three. Those two were duxt's OWN marketing showing
|
|
90
|
+
// through every site that extended it; they live in `www/app/app.config.ts`
|
|
91
|
+
// now, in the record form, because the Nitro routes that read them have no
|
|
92
|
+
// i18n — see `server/utils/duxt-server-text.ts`.
|
|
93
|
+
//
|
|
94
|
+
// Unset, the h1 falls back to `title` and the paragraph does not render at
|
|
95
|
+
// all. Set `landing.badge`, `landing.headline` or `landing.description`
|
|
96
|
+
// and each appears.
|
|
97
|
+
|
|
98
|
+
// One action, and a generic one: "read the docs" is true of every site
|
|
99
|
+
// built on this layer. A second button pointing at duxt's own repository
|
|
100
|
+
// was not — see `links` above.
|
|
101
|
+
// No `to`, for the same reason the section row above ships empty: the
|
|
102
|
+
// layer knows that a site HAS documentation, never what its first page is
|
|
103
|
+
// called. The landing resolves it to the first section, or to `/`.
|
|
104
|
+
actions: [
|
|
105
|
+
{
|
|
106
|
+
label: 'duxt.defaults.landing.actions.docs',
|
|
107
|
+
icon: 'lucide:arrow-right'
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
/**
|
|
111
|
+
* Empty, like `sections` and `links` above.
|
|
112
|
+
*
|
|
113
|
+
* The six cards that used to sit here described duxt — its source list, its
|
|
114
|
+
* git-native sourcing, its shadcn-vue base. That is duxt's own marketing
|
|
115
|
+
* copy, not a default another site would keep: a stranger extending the
|
|
116
|
+
* layer got a landing page selling somebody else's project.
|
|
117
|
+
*
|
|
118
|
+
* Their translations left with them — `duxt.defaults.*` translates the
|
|
119
|
+
* interface the layer draws, never content a site writes. duxt's own cards
|
|
120
|
+
* live in `www/app/app.config.ts`, where each one POINTS at the page that
|
|
121
|
+
* explains it, which the layer could never know.
|
|
122
|
+
*
|
|
123
|
+
* Left empty, the band does not render: the landing gates the whole section
|
|
124
|
+
* on `duxt.landing?.features?.length`.
|
|
125
|
+
*/
|
|
126
|
+
features: []
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Title only, no links — same reasoning as `links` above.
|
|
131
|
+
*
|
|
132
|
+
* "Star on GitHub" that stars duxt, an issue tracker that is not yours and a
|
|
133
|
+
* Discord that is somebody else's community are worse than an empty column.
|
|
134
|
+
* The block draws nothing until a consumer fills it.
|
|
135
|
+
*
|
|
136
|
+
* The title is the exception that stays: "Community" is interface text the layer
|
|
137
|
+
* draws itself, in every language it ships, and it is not anybody's link.
|
|
138
|
+
*/
|
|
139
|
+
aside: {
|
|
140
|
+
title: 'duxt.defaults.aside.title'
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
type Plain = Record<string, unknown>;
|
|
145
|
+
|
|
146
|
+
const isPlainObject = (value: unknown): value is Plain =>
|
|
147
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
148
|
+
|
|
149
|
+
/** Merge objects key by key; an array on the left replaces the one on the right. */
|
|
150
|
+
export function mergeDuxtConfig<T>(over: unknown, base: T): T {
|
|
151
|
+
if (Array.isArray(over)) return over as T;
|
|
152
|
+
if (over === undefined) return base;
|
|
153
|
+
if (!isPlainObject(over) || !isPlainObject(base)) return over as T;
|
|
154
|
+
|
|
155
|
+
const result: Plain = { ...base };
|
|
156
|
+
for (const [key, value] of Object.entries(over)) {
|
|
157
|
+
result[key] = mergeDuxtConfig(value, (base as Plain)[key]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result as T;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* How many ROWS a band draws, which is not always one.
|
|
165
|
+
*
|
|
166
|
+
* The bands alternate — prose left, prose right, prose left — and the side is
|
|
167
|
+
* decided by position. A split try-it client is two rows inside one band, so
|
|
168
|
+
* counting bands puts every band after it on the side it just used. Counting
|
|
169
|
+
* rows is what keeps the page alternating across it.
|
|
170
|
+
*
|
|
171
|
+
* Pure, and here rather than in the component, because the page needs the
|
|
172
|
+
* answer BEFORE it renders the band: the offset of band four depends on how
|
|
173
|
+
* many rows bands one to three drew.
|
|
174
|
+
*/
|
|
175
|
+
export function duxtShowcaseRows(showcase: {
|
|
176
|
+
full?: boolean;
|
|
177
|
+
demo?: { type?: string; layout?: string };
|
|
178
|
+
}): number {
|
|
179
|
+
const demo = showcase.demo;
|
|
180
|
+
if (demo?.type !== 'operation') return 1;
|
|
181
|
+
|
|
182
|
+
const layout = demo.layout ?? (showcase.full ? 'split' : 'panel');
|
|
183
|
+
|
|
184
|
+
return layout === 'split' ? 2 : 1;
|
|
185
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A configured string that may also be a translation.
|
|
3
|
+
*
|
|
4
|
+
* Three shapes, because they serve three different consumers and none of them
|
|
5
|
+
* is served well by the other two:
|
|
6
|
+
*
|
|
7
|
+
* - a LITERAL — `'Guide'` — for the single-language site, which is most of
|
|
8
|
+
* them, and which must not have to learn i18n to name a navbar entry;
|
|
9
|
+
* - a KEY — `'nav.guide'` — for the site that already has locale files and
|
|
10
|
+
* wants the theme's own interface text to live in them;
|
|
11
|
+
* - a RECORD — `{ 'en-GB': 'Guide', 'de-DE': 'Anleitung' }` — for the site
|
|
12
|
+
* with two languages and eight labels, where creating and registering a
|
|
13
|
+
* locale file per language costs more than it saves.
|
|
14
|
+
*
|
|
15
|
+
* A literal and a key are the same TYPE, and the difference between them is
|
|
16
|
+
* decided by whether the key is registered, never by a heuristic on the text.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** How a string is looked up. Returns undefined when nothing is registered. */
|
|
20
|
+
export type DuxtTextLookup = (key: string) => string | undefined;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Config keys whose values are prose.
|
|
24
|
+
*
|
|
25
|
+
* An allowlist rather than a walk over every string: `to`, `icon`, `variant`,
|
|
26
|
+
* `prefix` and `collection` are also strings, and running a URL through a
|
|
27
|
+
* translation lookup is how a link silently becomes something else.
|
|
28
|
+
*/
|
|
29
|
+
const TEXT_KEYS = new Set([
|
|
30
|
+
'badge',
|
|
31
|
+
'copyright',
|
|
32
|
+
'description',
|
|
33
|
+
'group',
|
|
34
|
+
'headline',
|
|
35
|
+
// A heading whose list is a sibling rather than a child, so it cannot be
|
|
36
|
+
// spelled `title` and be resolved by the entry above. A prose key added
|
|
37
|
+
// WITHOUT a line here does not fail — it renders its own locale record as
|
|
38
|
+
// JSON on the page, which is exactly how this one was found.
|
|
39
|
+
'highlightsTitle',
|
|
40
|
+
'label',
|
|
41
|
+
'title'
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A locale code, as a record's keys are written: `en`, `en-GB`, `zh-Hant-TW`.
|
|
46
|
+
*
|
|
47
|
+
* Needed because a text key may now hold something that is NOT a text: a badge
|
|
48
|
+
* is `{ label, icon, variant, to }`, and a record of locales is
|
|
49
|
+
* `{ 'en-GB': …, 'de-DE': … }`. Both are objects of strings, so the values
|
|
50
|
+
* cannot tell them apart — the KEYS can, and a config field whose keys are all
|
|
51
|
+
* locale codes is a locale record. `to` alone would match, which is why every
|
|
52
|
+
* key has to: `label` never will.
|
|
53
|
+
*/
|
|
54
|
+
const LOCALE_CODE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
55
|
+
|
|
56
|
+
/** Whether a value is a text to resolve, rather than a tree to walk into. */
|
|
57
|
+
function isTextValue(value: unknown): boolean {
|
|
58
|
+
if (typeof value === 'string') return true;
|
|
59
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
60
|
+
|
|
61
|
+
const entries = Object.entries(value);
|
|
62
|
+
return (
|
|
63
|
+
entries.length > 0 &&
|
|
64
|
+
entries.every(
|
|
65
|
+
([key, entry]) => typeof entry === 'string' && LOCALE_CODE.test(key)
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One value: key if one is registered, own language, base language, or as written. */
|
|
71
|
+
export function resolveDuxtText(
|
|
72
|
+
value: unknown,
|
|
73
|
+
locale: string,
|
|
74
|
+
lookup: DuxtTextLookup
|
|
75
|
+
): unknown {
|
|
76
|
+
if (typeof value === 'string') return lookup(value) ?? value;
|
|
77
|
+
|
|
78
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
79
|
+
const record = value as Record<string, string>;
|
|
80
|
+
if (Object.values(record).some((entry) => typeof entry !== 'string')) {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// `pt-BR` falls back to any `pt-*` the record does carry, mirroring what
|
|
85
|
+
// the locale FILES already do — a record written for `pt-PT` should not
|
|
86
|
+
// leave a Brazilian reader with English.
|
|
87
|
+
const base = locale.split('-')[0];
|
|
88
|
+
const sameLanguage = Object.entries(record).find(
|
|
89
|
+
([code]) => code.split('-')[0] === base
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return record[locale] ?? sameLanguage?.[1] ?? Object.values(record)[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The same, over a whole config tree: only the keys above are touched. */
|
|
99
|
+
export function resolveDuxtTexts<T>(
|
|
100
|
+
node: T,
|
|
101
|
+
locale: string,
|
|
102
|
+
lookup: DuxtTextLookup
|
|
103
|
+
): T {
|
|
104
|
+
if (Array.isArray(node)) {
|
|
105
|
+
return node.map((entry) =>
|
|
106
|
+
resolveDuxtTexts(entry, locale, lookup)
|
|
107
|
+
) as unknown as T;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!node || typeof node !== 'object') return node;
|
|
111
|
+
|
|
112
|
+
const result: Record<string, unknown> = {};
|
|
113
|
+
for (const [key, value] of Object.entries(node)) {
|
|
114
|
+
// A text key may carry a structure instead — `badge` takes either a text or
|
|
115
|
+
// a whole badge — so the value decides, not the key alone.
|
|
116
|
+
result[key] =
|
|
117
|
+
TEXT_KEYS.has(key) && isTextValue(value)
|
|
118
|
+
? resolveDuxtText(value, locale, lookup)
|
|
119
|
+
: resolveDuxtTexts(value, locale, lookup);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result as T;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A text field where a plain string is required — an aria-label, a title
|
|
127
|
+
* attribute, a list key.
|
|
128
|
+
*
|
|
129
|
+
* The config type keeps `DuxtText` because that is what a consumer may WRITE;
|
|
130
|
+
* by the time a component reads the value, `useDuxtConfig` has resolved it.
|
|
131
|
+
* This states that narrowing without asserting it: an unresolved record yields
|
|
132
|
+
* undefined rather than `[object Object]` in the DOM.
|
|
133
|
+
*/
|
|
134
|
+
export const asText = (value: DuxtText | undefined): string | undefined =>
|
|
135
|
+
typeof value === 'string' ? value : undefined;
|