@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,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `openapi` type: an API description, published as reference pages.
|
|
3
|
+
*
|
|
4
|
+
* The second type in the registry, and the first one that answers the questions
|
|
5
|
+
* the registry was built to ask. A changelog is one global history in the
|
|
6
|
+
* language it was written in; a reference is **per version** and **per locale**,
|
|
7
|
+
* and it needs a **layout** the docs chrome does not give it. Those three
|
|
8
|
+
* differences are policies on the type rather than branches in the scaffold,
|
|
9
|
+
* which is exactly what #9 claimed they would be — so this file adds a parser
|
|
10
|
+
* and three declarations, and nothing else.
|
|
11
|
+
*
|
|
12
|
+
* ONE PAGE PER OPERATION, plus an index per tag. Everything the layer can do
|
|
13
|
+
* hangs off pages: the search a reader actually performs is "find this
|
|
14
|
+
* endpoint", `llms.txt` lists pages, the MCP tools read pages, and a deep link
|
|
15
|
+
* to an endpoint has to survive being pasted into a ticket. A single-page
|
|
16
|
+
* reference with client-side routing has none of that.
|
|
17
|
+
*
|
|
18
|
+
* The pages are Markdown carrying MDC components, because that is what the
|
|
19
|
+
* scaffold's `getKeys`/`getItem` seam produces and what makes the result an
|
|
20
|
+
* ordinary collection. The prose — the operation's own description, a tag's,
|
|
21
|
+
* the document's — is written OUTSIDE the component, as the CommonMark the
|
|
22
|
+
* specification says it is, so Content parses it, the search indexes it and
|
|
23
|
+
* `llms-full.txt` carries it. Only what is structural — parameters, schemas,
|
|
24
|
+
* responses, the try-it client — travels as props.
|
|
25
|
+
*/
|
|
26
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
27
|
+
import type {
|
|
28
|
+
DuxtOpenApiOperation,
|
|
29
|
+
DuxtOpenApiSecurity,
|
|
30
|
+
DuxtOpenApiServer,
|
|
31
|
+
DuxtOpenApiSpec,
|
|
32
|
+
DuxtOpenApiTag
|
|
33
|
+
} from './openapi-model';
|
|
34
|
+
import { compactOpenApi, parseOpenApiDocument } from './openapi-parse';
|
|
35
|
+
import type {
|
|
36
|
+
DuxtSectionContext,
|
|
37
|
+
DuxtSectionPage,
|
|
38
|
+
DuxtSectionType
|
|
39
|
+
} from './sections-resolve';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The layout name this type binds — PUBLIC SURFACE, so renaming it is a
|
|
43
|
+
* `feat!:`. It is the first name to go through the shared slot, which is what
|
|
44
|
+
* makes it the first real test of it.
|
|
45
|
+
*/
|
|
46
|
+
export const DUXT_OPENAPI_LAYOUT = 'reference';
|
|
47
|
+
|
|
48
|
+
export const openapiSectionType: DuxtSectionType = {
|
|
49
|
+
parse: parseOpenApiSection,
|
|
50
|
+
/**
|
|
51
|
+
* PER VERSION, like any other page.
|
|
52
|
+
*
|
|
53
|
+
* An API description belongs to the release it describes: `v1` and `v2` have
|
|
54
|
+
* different endpoints, and a reader on `v1` asking what a field means must
|
|
55
|
+
* not be shown `v2`'s answer. Each version collection therefore carries the
|
|
56
|
+
* spec file at its own ref, and the version switcher works on the reference
|
|
57
|
+
* exactly as it works on the prose. This is the opposite answer to the
|
|
58
|
+
* changelog's, and the reason `versioning` is a policy rather than a rule.
|
|
59
|
+
*/
|
|
60
|
+
versioning: 'per-version',
|
|
61
|
+
/**
|
|
62
|
+
* PER LOCALE, where a locale ships a spec of its own.
|
|
63
|
+
*
|
|
64
|
+
* A description is written prose as much as it is structure — summaries,
|
|
65
|
+
* descriptions, response meanings — so a translated one is a real artefact
|
|
66
|
+
* rather than the same file relabelled. A locale that declares none builds no
|
|
67
|
+
* collection, and the existing fallback chain then serves the original with
|
|
68
|
+
* `DuxtTranslationBanner` saying so. What stays translatable either way are
|
|
69
|
+
* the LAYER'S OWN labels — the table headers, "Request", "Response", the
|
|
70
|
+
* status names — because those belong to the theme and not to the document.
|
|
71
|
+
*/
|
|
72
|
+
localisation: 'per-locale',
|
|
73
|
+
layout: DUXT_OPENAPI_LAYOUT,
|
|
74
|
+
icon: 'lucide:plug'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function parseOpenApiSection(
|
|
78
|
+
artefact: string,
|
|
79
|
+
context: DuxtSectionContext
|
|
80
|
+
): DuxtSectionPage[] {
|
|
81
|
+
let spec: DuxtOpenApiSpec;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
spec = parseOpenApiDocument(artefact);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// Rethrown rather than swallowed into an empty section: the scaffold turns
|
|
87
|
+
// "no pages" into the right severity for local and remote alike, but a
|
|
88
|
+
// message saying WHY reads better than one saying the file held nothing.
|
|
89
|
+
throw new Error(
|
|
90
|
+
`duxt: the generated section "${context.label}" could not read its ` +
|
|
91
|
+
`OpenAPI document — ${(error as Error).message}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Into the report rather than onto the console: a document is parsed while
|
|
96
|
+
// the config is loading, so a printed warning about a dropped `$ref` has
|
|
97
|
+
// scrolled away before the dev server has finished starting — and what it
|
|
98
|
+
// reports is content silently missing from the reference. See
|
|
99
|
+
// `DuxtSectionContext.warn`.
|
|
100
|
+
for (const warning of spec.warnings) context.warn?.(warning);
|
|
101
|
+
|
|
102
|
+
if (!spec.tags?.length) return [];
|
|
103
|
+
|
|
104
|
+
const groups = spec.tags.map((tag, index) => ({
|
|
105
|
+
tag,
|
|
106
|
+
slug: segment(tag.name),
|
|
107
|
+
order: order(index, spec.tags!.length)
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
// A tag whose name slugifies to the same segment as another's would serve
|
|
111
|
+
// two groups from one URL, and the later one would win silently.
|
|
112
|
+
const seen = new Set<string>();
|
|
113
|
+
for (const group of groups) {
|
|
114
|
+
let candidate = group.slug || 'group';
|
|
115
|
+
let attempt = 2;
|
|
116
|
+
while (seen.has(candidate)) candidate = `${group.slug}-${attempt++}`;
|
|
117
|
+
seen.add(candidate);
|
|
118
|
+
group.slug = candidate;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
overview(spec, groups, context),
|
|
123
|
+
...groups.flatMap((group) => pagesFor(spec, group, context))
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface Group {
|
|
128
|
+
tag: DuxtOpenApiTag;
|
|
129
|
+
slug: string;
|
|
130
|
+
order: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The section's own page: what the API is, where it is, how to authenticate. */
|
|
134
|
+
function overview(
|
|
135
|
+
spec: DuxtOpenApiSpec,
|
|
136
|
+
groups: Group[],
|
|
137
|
+
context: DuxtSectionContext
|
|
138
|
+
): DuxtSectionPage {
|
|
139
|
+
const props = {
|
|
140
|
+
version: spec.info.version,
|
|
141
|
+
// NO `summary`: it is this page's `description` below, and the shell draws
|
|
142
|
+
// a description under the title. Handed to the component as well, it was
|
|
143
|
+
// printed a second time under the rule where the body starts.
|
|
144
|
+
servers: spec.servers,
|
|
145
|
+
securitySchemes: spec.securitySchemes,
|
|
146
|
+
security: spec.security,
|
|
147
|
+
contact: spec.info.contact,
|
|
148
|
+
license: spec.info.license,
|
|
149
|
+
termsOfService: spec.info.termsOfService,
|
|
150
|
+
externalDocs: spec.externalDocs,
|
|
151
|
+
groups: groups.map((group) => ({
|
|
152
|
+
name: group.tag.name,
|
|
153
|
+
// One line, and plain: a card is not prose, and a tag description that
|
|
154
|
+
// renders as Markdown on its own page would otherwise show its asterisks
|
|
155
|
+
// here.
|
|
156
|
+
description: firstLine(group.tag.description),
|
|
157
|
+
webhooks: group.tag.webhooks ?? false,
|
|
158
|
+
operations: group.tag.operations?.length ?? 0,
|
|
159
|
+
to: `${context.prefix}/${group.slug}`
|
|
160
|
+
}))
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
file: 'index.md',
|
|
165
|
+
body: page(
|
|
166
|
+
{ title: spec.info.title, description: overviewDescription(spec) },
|
|
167
|
+
// NO `<h1>` OF ITS OWN. The shell draws the docs header — breadcrumb,
|
|
168
|
+
// title, the copy control beside it — for every generated page whose body
|
|
169
|
+
// opens on no heading, and these pages want exactly that: a reference
|
|
170
|
+
// page has a title and a trail like any written one, and the method chip
|
|
171
|
+
// that made this type look different is drawn below the heading anyway.
|
|
172
|
+
[component('open-api-overview', props, spec.info.description)]
|
|
173
|
+
)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function overviewDescription(spec: DuxtOpenApiSpec): string {
|
|
178
|
+
return (
|
|
179
|
+
spec.info.summary ??
|
|
180
|
+
firstLine(spec.info.description) ??
|
|
181
|
+
// `version` is optional once the model has been compacted, and a template
|
|
182
|
+
// literal writes an absent one out as the word `undefined`.
|
|
183
|
+
[spec.info.title, spec.info.version].filter(Boolean).join(' ')
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A tag's index page, and one page per operation under it. */
|
|
188
|
+
function pagesFor(
|
|
189
|
+
spec: DuxtOpenApiSpec,
|
|
190
|
+
group: Group,
|
|
191
|
+
context: DuxtSectionContext
|
|
192
|
+
): DuxtSectionPage[] {
|
|
193
|
+
const operations = group.tag.operations ?? [];
|
|
194
|
+
const slugs = operationSlugs(operations);
|
|
195
|
+
const width = operations.length;
|
|
196
|
+
|
|
197
|
+
const links = operations.map((operation, index) => ({
|
|
198
|
+
method: operation.method,
|
|
199
|
+
path: operation.path,
|
|
200
|
+
kind: operation.kind,
|
|
201
|
+
summary: operation.summary,
|
|
202
|
+
deprecated: operation.deprecated,
|
|
203
|
+
to: `${context.prefix}/${group.slug}/${slugs[index]}`
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
const index: DuxtSectionPage = {
|
|
207
|
+
file: `${group.order}.${group.slug}/index.md`,
|
|
208
|
+
body: page(
|
|
209
|
+
// No invented description where the tag has none: a count in the
|
|
210
|
+
// layer's own English would be untranslatable text the layer does not
|
|
211
|
+
// own, which `tests/i18n-ownership.test.ts` exists to keep out.
|
|
212
|
+
{ title: group.tag.name, description: firstLine(group.tag.description) },
|
|
213
|
+
[
|
|
214
|
+
component(
|
|
215
|
+
'open-api-operations',
|
|
216
|
+
{ operations: links, externalDocs: group.tag.externalDocs },
|
|
217
|
+
group.tag.description
|
|
218
|
+
)
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return [
|
|
224
|
+
index,
|
|
225
|
+
...operations.map((operation, position) =>
|
|
226
|
+
operationPage(
|
|
227
|
+
spec,
|
|
228
|
+
group,
|
|
229
|
+
operation,
|
|
230
|
+
slugs[position]!,
|
|
231
|
+
order(position, width)
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function operationPage(
|
|
238
|
+
spec: DuxtOpenApiSpec,
|
|
239
|
+
group: Group,
|
|
240
|
+
operation: DuxtOpenApiOperation,
|
|
241
|
+
slug: string,
|
|
242
|
+
position: string
|
|
243
|
+
): DuxtSectionPage {
|
|
244
|
+
const title =
|
|
245
|
+
operation.summary?.trim() ||
|
|
246
|
+
`${operation.method.toUpperCase()} ${operation.path}`;
|
|
247
|
+
|
|
248
|
+
// The description travels in the SLOT rather than in the props, so what the
|
|
249
|
+
// document wrote as CommonMark is rendered as CommonMark. `servers` and
|
|
250
|
+
// `security` leave with it: both are RESOLVED against the document below, and
|
|
251
|
+
// an unresolved copy beside the resolved one is two answers to one question.
|
|
252
|
+
// The underscores are what marks them deliberately dropped rather than
|
|
253
|
+
// forgotten — `no-unused-vars` reads the prefix, and this repository denies
|
|
254
|
+
// warnings.
|
|
255
|
+
const {
|
|
256
|
+
description,
|
|
257
|
+
servers: _servers,
|
|
258
|
+
security: _security,
|
|
259
|
+
...rest
|
|
260
|
+
} = operation;
|
|
261
|
+
|
|
262
|
+
const props = {
|
|
263
|
+
operation: rest,
|
|
264
|
+
servers: serversFor(spec, operation),
|
|
265
|
+
security: securityFor(spec, operation),
|
|
266
|
+
securitySchemes: spec.securitySchemes
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
file: `${group.order}.${group.slug}/${position}.${slug}.md`,
|
|
271
|
+
body: page(
|
|
272
|
+
{
|
|
273
|
+
title,
|
|
274
|
+
description:
|
|
275
|
+
firstLine(description) ??
|
|
276
|
+
`${operation.method.toUpperCase()} ${operation.path}`
|
|
277
|
+
},
|
|
278
|
+
[component('open-api-operation', props, description)]
|
|
279
|
+
)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The servers this operation is reachable at.
|
|
285
|
+
*
|
|
286
|
+
* An operation's own list REPLACES the document's rather than adding to it,
|
|
287
|
+
* which is what the specification says and what the try-it client has to obey:
|
|
288
|
+
* offering a server the endpoint is not served from is a request that fails for
|
|
289
|
+
* a reason nothing on the page explains.
|
|
290
|
+
*/
|
|
291
|
+
function serversFor(
|
|
292
|
+
spec: DuxtOpenApiSpec,
|
|
293
|
+
operation: DuxtOpenApiOperation
|
|
294
|
+
): DuxtOpenApiServer[] {
|
|
295
|
+
return operation.servers?.length ? operation.servers : (spec.servers ?? []);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The security this operation demands.
|
|
300
|
+
*
|
|
301
|
+
* An operation's own `security` replaces the document's, and an EMPTY LIST is a
|
|
302
|
+
* statement rather than an absence: it is how an endpoint opts out of a global
|
|
303
|
+
* requirement, which is why this cannot be a `||`.
|
|
304
|
+
*/
|
|
305
|
+
function securityFor(
|
|
306
|
+
spec: DuxtOpenApiSpec,
|
|
307
|
+
operation: DuxtOpenApiOperation
|
|
308
|
+
): DuxtOpenApiSecurity | undefined {
|
|
309
|
+
return operation.security ?? spec.security;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A URL segment per operation, unique inside its tag.
|
|
314
|
+
*
|
|
315
|
+
* `operationId` first, because it is the name the document itself gives the
|
|
316
|
+
* endpoint and the one an SDK generator uses — a reader following a link from
|
|
317
|
+
* generated code lands on the right page. Without one the method and path are
|
|
318
|
+
* the only stable identity there is.
|
|
319
|
+
*/
|
|
320
|
+
function operationSlugs(operations: DuxtOpenApiOperation[]): string[] {
|
|
321
|
+
const taken = new Set<string>();
|
|
322
|
+
|
|
323
|
+
return operations.map((operation) => {
|
|
324
|
+
const base =
|
|
325
|
+
segment(operation.operationId ?? '') ||
|
|
326
|
+
segment(`${operation.method}-${operation.path}`) ||
|
|
327
|
+
operation.method;
|
|
328
|
+
|
|
329
|
+
let candidate = base;
|
|
330
|
+
let attempt = 2;
|
|
331
|
+
while (taken.has(candidate)) candidate = `${base}-${attempt++}`;
|
|
332
|
+
taken.add(candidate);
|
|
333
|
+
|
|
334
|
+
return candidate;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A URL segment: lowercase, no dots.
|
|
340
|
+
*
|
|
341
|
+
* The dots matter. Content reads a name made of digits and dots as a version
|
|
342
|
+
* and stops refining it, which would leave the `NN.` ordering prefix in the
|
|
343
|
+
* URL — the same trap `sections-changelog.ts` answers with a leading `v`. An
|
|
344
|
+
* endpoint at `/v1.0/pets` walks straight into it, so the dot goes.
|
|
345
|
+
*/
|
|
346
|
+
function segment(value: string): string {
|
|
347
|
+
return value
|
|
348
|
+
.toLowerCase()
|
|
349
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
350
|
+
.replace(/^-+|-+$/g, '');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Zero-padded so ten pages still sort the way they read. */
|
|
354
|
+
function order(index: number, total: number): string {
|
|
355
|
+
return String(index + 1).padStart(String(total).length, '0');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The first paragraph of a description, as one line of PLAIN text.
|
|
360
|
+
*
|
|
361
|
+
* Plain matters because of where this goes: a page's `description`
|
|
362
|
+
* frontmatter, which becomes the meta description, the og tag and the card in
|
|
363
|
+
* a search result, and the summary on a group card. A CommonMark description
|
|
364
|
+
* carrying `*emphasis*` or a `[link](url)` renders as prose everywhere it is
|
|
365
|
+
* rendered as prose — and as its own punctuation everywhere it is not.
|
|
366
|
+
*
|
|
367
|
+
* Deliberately small. It undoes the four inline constructs a one-line summary
|
|
368
|
+
* actually meets — emphasis, code spans, links and images — and leaves
|
|
369
|
+
* everything else alone rather than growing into a second Markdown parser
|
|
370
|
+
* whose disagreements with the first would be invisible.
|
|
371
|
+
*/
|
|
372
|
+
function firstLine(value?: string): string | undefined {
|
|
373
|
+
const line = plain(value?.split(/\n\s*\n/)[0] ?? '')
|
|
374
|
+
.replace(/\s+/g, ' ')
|
|
375
|
+
.trim();
|
|
376
|
+
|
|
377
|
+
return line || undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Inline Markdown, as the text it renders to. */
|
|
381
|
+
function plain(value: string): string {
|
|
382
|
+
return (
|
|
383
|
+
value
|
|
384
|
+
// An image before a link: `` is a link with a `!` in front, and
|
|
385
|
+
// taking the link first would leave the `!` behind.
|
|
386
|
+
.replaceAll(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
387
|
+
.replaceAll(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
388
|
+
.replaceAll(/`([^`]*)`/g, '$1')
|
|
389
|
+
.replaceAll(/(\*\*|__)(.+?)\1/g, '$2')
|
|
390
|
+
.replaceAll(/(\*|_)(.+?)\1/g, '$2')
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* An MDC block component with YAML props and Markdown inside it.
|
|
396
|
+
*
|
|
397
|
+
* The fence LENGTH is computed rather than fixed: a description containing a
|
|
398
|
+
* line of colons would otherwise close the component early and spill its props
|
|
399
|
+
* into the page. Three is the floor — enough that `::callout` inside a
|
|
400
|
+
* description is content rather than a closing fence.
|
|
401
|
+
*/
|
|
402
|
+
function component(
|
|
403
|
+
name: string,
|
|
404
|
+
props: Record<string, unknown>,
|
|
405
|
+
slot?: string
|
|
406
|
+
): string {
|
|
407
|
+
const body = slot?.trim() ?? '';
|
|
408
|
+
|
|
409
|
+
const longest = Math.max(
|
|
410
|
+
2,
|
|
411
|
+
...body.split('\n').map((line) => /^\s*(:+)/.exec(line)?.[1]?.length ?? 0)
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const fence = ':'.repeat(Math.max(3, longest + 1));
|
|
415
|
+
|
|
416
|
+
return [
|
|
417
|
+
`${fence}${name}`,
|
|
418
|
+
'---',
|
|
419
|
+
stringifyYaml(compactOpenApi(props)).trimEnd(),
|
|
420
|
+
'---',
|
|
421
|
+
...(body ? [body] : []),
|
|
422
|
+
fence
|
|
423
|
+
].join('\n');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** A page: frontmatter, then the lines. */
|
|
427
|
+
function page(
|
|
428
|
+
fields: Record<string, string | undefined>,
|
|
429
|
+
lines: string[]
|
|
430
|
+
): string {
|
|
431
|
+
return [frontmatter(fields), '', ...lines, ''].join('\n');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* A frontmatter block YAML can read back.
|
|
436
|
+
*
|
|
437
|
+
* Every value is a JSON string, which is also a YAML double-quoted scalar — so
|
|
438
|
+
* a summary carrying a colon cannot end the mapping early. The rule
|
|
439
|
+
* `tests/frontmatter-yaml.test.ts` exists over, and a title like
|
|
440
|
+
* `GET /pets/{petId}: not found` is exactly the shape that breaks it.
|
|
441
|
+
*/
|
|
442
|
+
function frontmatter(fields: Record<string, string | undefined>): string {
|
|
443
|
+
return [
|
|
444
|
+
'---',
|
|
445
|
+
...Object.entries(fields)
|
|
446
|
+
.filter(([, value]) => value !== undefined && value !== '')
|
|
447
|
+
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`),
|
|
448
|
+
'---'
|
|
449
|
+
].join('\n');
|
|
450
|
+
}
|