livemark 0.24.1 → 0.26.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/README.md +3 -2
- package/build/actions/config/define.d.ts +6 -0
- package/build/actions/config/define.unit.js +8 -1
- package/build/actions/escape/copy.d.ts +11 -0
- package/build/actions/escape/copy.js +29 -0
- package/build/actions/escape/copy.unit.d.ts +1 -0
- package/build/actions/escape/copy.unit.js +37 -0
- package/build/actions/escape/list.d.ts +12 -0
- package/build/actions/escape/list.js +27 -0
- package/build/actions/escape/list.unit.d.ts +1 -0
- package/build/actions/escape/list.unit.js +36 -0
- package/build/actions/target/build.d.ts +9 -26
- package/build/actions/target/build.js +50 -71
- package/build/actions/target/build.unit.d.ts +1 -0
- package/build/actions/target/build.unit.js +76 -0
- package/build/actions/target/clear.d.ts +4 -0
- package/build/actions/target/clear.js +14 -0
- package/build/actions/target/clear.unit.d.ts +1 -0
- package/build/actions/target/clear.unit.js +23 -0
- package/build/commands/build.js +1 -2
- package/build/commands/clear.d.ts +5 -0
- package/build/commands/clear.js +15 -0
- package/build/commands/escape.d.ts +4 -3
- package/build/commands/escape.js +45 -48
- package/build/commands/preview.js +1 -2
- package/build/commands/start.js +1 -2
- package/build/components/Abbr.d.ts +1 -1
- package/build/components/Abbr.js +2 -4
- package/build/components/AnsiCode.d.ts +1 -1
- package/build/components/AnsiCode.js +2 -2
- package/build/components/Article.d.ts +2 -1
- package/build/components/Article.js +7 -14
- package/build/components/BackToTop.d.ts +1 -1
- package/build/components/Banner.d.ts +2 -2
- package/build/components/Banner.js +7 -3
- package/build/components/BlogIndex.d.ts +1 -1
- package/build/components/BlogIndex.js +2 -11
- package/build/components/Breadcrumbs.d.ts +1 -1
- package/build/components/Breadcrumbs.js +2 -3
- package/build/components/Callout.d.ts +1 -1
- package/build/components/Callout.js +5 -9
- package/build/components/Card.d.ts +1 -1
- package/build/components/Card.js +2 -3
- package/build/components/Cards.d.ts +1 -1
- package/build/components/ChangelogIndex.d.ts +1 -1
- package/build/components/ChangelogIndex.js +2 -11
- package/build/components/ChoiceList.d.ts +15 -0
- package/build/components/ChoiceList.js +13 -0
- package/build/components/CodeBlock.d.ts +1 -1
- package/build/components/CodeBlock.js +3 -16
- package/build/components/CodeTabs.d.ts +1 -1
- package/build/components/CodeTabs.js +2 -3
- package/build/components/Column.d.ts +1 -1
- package/build/components/Columns.d.ts +1 -1
- package/build/components/Content.d.ts +1 -1
- package/build/components/ContentTab.d.ts +1 -1
- package/build/components/ContentTabs.d.ts +1 -1
- package/build/components/ContentTabs.js +2 -3
- package/build/components/DefaultCatchBoundary.d.ts +1 -1
- package/build/components/DefaultCatchBoundary.js +5 -7
- package/build/components/DefinitionList.d.ts +3 -3
- package/build/components/Details.d.ts +1 -1
- package/build/components/Details.js +2 -5
- package/build/components/FileTree.d.ts +1 -1
- package/build/components/FileTree.js +3 -5
- package/build/components/Footer.d.ts +1 -1
- package/build/components/Footer.js +2 -5
- package/build/components/FootnoteRef.d.ts +1 -1
- package/build/components/FootnoteRef.js +2 -4
- package/build/components/Header.d.ts +6 -0
- package/build/components/Header.js +26 -0
- package/build/components/HeaderMenu.d.ts +2 -0
- package/build/components/HeaderMenu.js +27 -0
- package/build/components/HeaderNav.d.ts +6 -0
- package/build/components/HeaderNav.js +74 -0
- package/build/components/HeaderSearch.d.ts +4 -0
- package/build/components/HeaderSearch.js +33 -0
- package/build/components/Heading.d.ts +6 -6
- package/build/components/InlineBadge.d.ts +2 -2
- package/build/components/InlineBadge.js +1 -1
- package/build/components/InlineIcon.d.ts +1 -1
- package/build/components/InlineToc.d.ts +1 -1
- package/build/components/InlineToc.js +2 -4
- package/build/components/Layout.d.ts +2 -1
- package/build/components/Layout.js +5 -58
- package/build/components/LinkButton.d.ts +2 -2
- package/build/components/LinkButton.js +1 -1
- package/build/components/MenuSheet.d.ts +22 -0
- package/build/components/MenuSheet.js +27 -0
- package/build/components/Mermaid.d.ts +1 -1
- package/build/components/MermaidRenderer.d.ts +1 -1
- package/build/components/MermaidRenderer.js +20 -8
- package/build/components/NotFound.d.ts +1 -1
- package/build/components/NotFound.js +3 -6
- package/build/components/PackageTabs.d.ts +1 -1
- package/build/components/PackageTabs.js +3 -7
- package/build/components/PageToolbar.d.ts +1 -1
- package/build/components/PageToolbar.js +2 -10
- package/build/components/PrevNext.d.ts +1 -1
- package/build/components/PrevNext.js +2 -10
- package/build/components/Root.d.ts +4 -0
- package/build/components/Root.js +8 -0
- package/build/components/Search.d.ts +5 -2
- package/build/components/Search.js +11 -9
- package/build/components/SearchDialog.d.ts +1 -1
- package/build/components/SearchDialog.js +10 -133
- package/build/components/Section.d.ts +1 -1
- package/build/components/SectionHeader.d.ts +1 -1
- package/build/components/SheetPanel.d.ts +12 -0
- package/build/components/SheetPanel.js +19 -0
- package/build/components/Sidebar.d.ts +1 -1
- package/build/components/Sidebar.js +12 -14
- package/build/components/SidebarArticles.d.ts +4 -1
- package/build/components/SidebarArticles.js +5 -9
- package/build/components/SidebarLinks.d.ts +4 -1
- package/build/components/SidebarLinks.js +6 -9
- package/build/components/SidebarPosts.d.ts +4 -1
- package/build/components/SidebarPosts.js +7 -23
- package/build/components/SidebarReleases.d.ts +4 -1
- package/build/components/SidebarReleases.js +5 -15
- package/build/components/SidebarSectionPicker.d.ts +8 -0
- package/build/components/SidebarSectionPicker.js +31 -0
- package/build/components/SidebarSections.d.ts +1 -1
- package/build/components/SidebarSections.js +5 -9
- package/build/components/SiteLink.d.ts +1 -1
- package/build/components/SiteTitle.d.ts +1 -1
- package/build/components/SiteTitle.js +2 -6
- package/build/components/SortableTable.d.ts +1 -1
- package/build/components/SoundCloud.d.ts +1 -1
- package/build/components/SoundCloud.js +2 -2
- package/build/components/TagIndex.d.ts +1 -1
- package/build/components/TagIndex.js +2 -11
- package/build/components/Theme.d.ts +2 -1
- package/build/components/Theme.js +35 -15
- package/build/components/Toc.d.ts +5 -4
- package/build/components/Toc.js +35 -35
- package/build/components/YouTube.d.ts +1 -1
- package/build/components/YouTube.js +2 -2
- package/build/components/ZoomImage.d.ts +1 -1
- package/build/content-collections.js +5 -1
- package/build/elements/accordion.d.ts +4 -4
- package/build/elements/accordion.js +2 -3
- package/build/elements/alert-dialog.d.ts +12 -12
- package/build/elements/alert-dialog.js +2 -4
- package/build/elements/alert.d.ts +4 -4
- package/build/elements/alert.js +2 -2
- package/build/elements/aspect-ratio.d.ts +1 -1
- package/build/elements/avatar.d.ts +6 -6
- package/build/elements/badge.d.ts +1 -1
- package/build/elements/badge.js +3 -1
- package/build/elements/breadcrumb.d.ts +6 -6
- package/build/elements/breadcrumb.js +2 -4
- package/build/elements/button-group.d.ts +2 -2
- package/build/elements/button-group.js +5 -5
- package/build/elements/button.d.ts +2 -2
- package/build/elements/button.js +9 -7
- package/build/elements/calendar.d.ts +2 -2
- package/build/elements/calendar.js +2 -2
- package/build/elements/card.d.ts +7 -7
- package/build/elements/card.js +4 -4
- package/build/elements/carousel.d.ts +5 -5
- package/build/elements/carousel.js +3 -7
- package/build/elements/chart.d.ts +6 -6
- package/build/elements/chart.js +5 -11
- package/build/elements/checkbox.d.ts +1 -1
- package/build/elements/collapsible.d.ts +3 -3
- package/build/elements/command.d.ts +9 -9
- package/build/elements/command.js +5 -11
- package/build/elements/context-menu.d.ts +15 -15
- package/build/elements/context-menu.js +6 -9
- package/build/elements/dialog.d.ts +10 -10
- package/build/elements/dialog.js +2 -6
- package/build/elements/drawer.d.ts +14 -12
- package/build/elements/drawer.js +14 -15
- package/build/elements/dropdown-menu.d.ts +15 -15
- package/build/elements/dropdown-menu.js +7 -10
- package/build/elements/empty.d.ts +6 -6
- package/build/elements/empty.js +2 -2
- package/build/elements/field.d.ts +10 -10
- package/build/elements/field.js +3 -4
- package/build/elements/hover-card.d.ts +3 -3
- package/build/elements/hover-card.js +2 -2
- package/build/elements/input-group.d.ts +7 -7
- package/build/elements/input-group.js +5 -5
- package/build/elements/input-otp.d.ts +4 -4
- package/build/elements/input.d.ts +1 -1
- package/build/elements/input.js +2 -2
- package/build/elements/item.d.ts +9 -9
- package/build/elements/item.js +2 -2
- package/build/elements/kbd.d.ts +2 -2
- package/build/elements/label.d.ts +1 -1
- package/build/elements/menubar.d.ts +16 -16
- package/build/elements/menubar.js +9 -11
- package/build/elements/native-select.d.ts +3 -3
- package/build/elements/native-select.js +2 -4
- package/build/elements/navigation-menu.d.ts +8 -8
- package/build/elements/navigation-menu.js +3 -5
- package/build/elements/pagination.d.ts +7 -7
- package/build/elements/pagination.js +4 -10
- package/build/elements/popover.d.ts +6 -6
- package/build/elements/popover.js +2 -2
- package/build/elements/progress.d.ts +5 -5
- package/build/elements/progress.js +2 -3
- package/build/elements/radio-group.d.ts +2 -2
- package/build/elements/resizable.d.ts +3 -3
- package/build/elements/scroll-area.d.ts +2 -2
- package/build/elements/scroll-area.js +2 -4
- package/build/elements/select.d.ts +9 -9
- package/build/elements/select.js +4 -9
- package/build/elements/separator.d.ts +1 -1
- package/build/elements/sheet.d.ts +8 -8
- package/build/elements/sheet.js +2 -6
- package/build/elements/sidebar.d.ts +19 -19
- package/build/elements/sidebar.js +15 -37
- package/build/elements/skeleton.d.ts +1 -1
- package/build/elements/slider.d.ts +2 -1
- package/build/elements/slider.js +2 -3
- package/build/elements/sonner.d.ts +1 -1
- package/build/elements/spinner.d.ts +1 -1
- package/build/elements/switch.d.ts +1 -1
- package/build/elements/table.d.ts +8 -8
- package/build/elements/tabs.d.ts +4 -4
- package/build/elements/tabs.js +3 -3
- package/build/elements/textarea.d.ts +1 -1
- package/build/elements/textarea.js +2 -2
- package/build/elements/toggle-group.d.ts +3 -2
- package/build/elements/toggle-group.js +3 -3
- package/build/elements/toggle.d.ts +1 -1
- package/build/elements/toggle.js +2 -2
- package/build/elements/tooltip.d.ts +4 -4
- package/build/elements/tooltip.js +2 -3
- package/build/head.d.ts +4 -0
- package/build/head.js +6 -0
- package/build/helpers/article-search.d.ts +19 -0
- package/build/helpers/article-search.js +134 -0
- package/build/helpers/dependency-dir.d.ts +5 -0
- package/build/helpers/dependency-dir.js +20 -0
- package/build/helpers/dependency-dir.unit.d.ts +1 -0
- package/build/helpers/dependency-dir.unit.js +25 -0
- package/build/helpers/dynamic-icon-resolver.d.ts +1 -1
- package/build/helpers/dynamic-icon.d.ts +1 -1
- package/build/helpers/head.d.ts +53 -0
- package/build/helpers/head.js +69 -0
- package/build/helpers/head.unit.d.ts +1 -0
- package/build/helpers/head.unit.js +108 -0
- package/build/helpers/nav-items.d.ts +11 -0
- package/build/helpers/nav-items.js +65 -0
- package/build/helpers/render-inline-markdown.d.ts +1 -0
- package/build/helpers/render-inline-markdown.js +34 -0
- package/build/helpers/render-inline-markdown.unit.d.ts +1 -0
- package/build/helpers/render-inline-markdown.unit.js +20 -0
- package/build/helpers/search-highlight.d.ts +8 -0
- package/build/helpers/search-highlight.js +59 -0
- package/build/helpers/search-highlight.unit.d.ts +1 -0
- package/build/helpers/search-highlight.unit.js +36 -0
- package/build/helpers/target-export.d.ts +9 -0
- package/build/helpers/target-export.js +24 -0
- package/build/helpers/target-export.unit.d.ts +1 -0
- package/build/helpers/target-export.unit.js +25 -0
- package/build/helpers/theme.d.ts +6 -2
- package/build/helpers/theme.js +40 -7
- package/build/helpers/theme.unit.d.ts +1 -0
- package/build/helpers/theme.unit.js +30 -0
- package/build/hooks/hydrated.d.ts +2 -0
- package/build/hooks/hydrated.js +15 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -1
- package/build/models/config.d.ts +32 -6
- package/build/models/config.js +10 -1
- package/build/models/section.d.ts +4 -2
- package/build/models/section.js +1 -1
- package/build/plugins/vite-livemark.d.ts +4 -5
- package/build/plugins/vite-livemark.js +56 -90
- package/build/program.js +4 -2
- package/build/routes/$.d.ts +4 -1
- package/build/routes/$.js +20 -9
- package/build/routes/__root.js +29 -63
- package/build/settings.js +2 -1
- package/package.json +68 -66
- package/{.agents/skills → skills}/livemark-authoring/SKILL.md +2 -2
- package/{.agents/skills → skills}/livemark-configuration/SKILL.md +12 -5
- package/skills/livemark-customization/SKILL.md +145 -0
- package/source/actions/config/define.unit.ts +9 -0
- package/source/actions/escape/copy.ts +35 -0
- package/source/actions/escape/copy.unit.ts +51 -0
- package/source/actions/escape/list.ts +30 -0
- package/source/actions/escape/list.unit.ts +42 -0
- package/source/actions/target/build.ts +49 -69
- package/source/actions/target/build.unit.ts +95 -0
- package/source/actions/target/clear.ts +14 -0
- package/source/actions/target/clear.unit.ts +25 -0
- package/source/commands/build.ts +0 -1
- package/source/commands/clear.ts +15 -0
- package/source/commands/escape.ts +42 -59
- package/source/commands/preview.ts +0 -1
- package/source/commands/start.ts +0 -1
- package/source/components/AnsiCode.tsx +1 -1
- package/source/components/Article.tsx +8 -4
- package/source/components/Banner.tsx +12 -2
- package/source/components/BlogIndex.tsx +1 -1
- package/source/components/Callout.tsx +4 -6
- package/source/components/ChangelogIndex.tsx +1 -1
- package/source/components/ChoiceList.tsx +69 -0
- package/source/components/CodeBlock.tsx +6 -6
- package/source/components/CodeTabs.tsx +1 -1
- package/source/components/ContentTabs.tsx +1 -1
- package/source/components/DefaultCatchBoundary.tsx +4 -6
- package/source/components/Details.tsx +1 -1
- package/source/components/FileTree.tsx +1 -1
- package/source/components/Header.tsx +52 -0
- package/source/components/HeaderMenu.tsx +41 -0
- package/source/components/HeaderNav.tsx +145 -0
- package/source/components/HeaderSearch.tsx +77 -0
- package/source/components/InlineBadge.tsx +1 -1
- package/source/components/InlineToc.tsx +1 -1
- package/source/components/Layout.tsx +16 -160
- package/source/components/LinkButton.tsx +9 -1
- package/source/components/MenuSheet.tsx +92 -0
- package/source/components/MermaidRenderer.tsx +19 -8
- package/source/components/NotFound.tsx +3 -5
- package/source/components/PackageTabs.tsx +2 -2
- package/source/components/PageToolbar.tsx +11 -3
- package/source/components/PrevNext.tsx +2 -2
- package/source/components/Root.tsx +13 -0
- package/source/components/Search.tsx +28 -8
- package/source/components/SearchDialog.tsx +15 -189
- package/source/components/SheetPanel.tsx +69 -0
- package/source/components/Sidebar.tsx +27 -14
- package/source/components/SidebarArticles.tsx +4 -3
- package/source/components/SidebarLinks.tsx +3 -2
- package/source/components/SidebarPosts.tsx +3 -2
- package/source/components/SidebarReleases.tsx +3 -2
- package/source/components/SidebarSectionPicker.tsx +70 -0
- package/source/components/SiteTitle.tsx +5 -5
- package/source/components/SoundCloud.tsx +1 -1
- package/source/components/TagIndex.tsx +1 -1
- package/source/components/Theme.tsx +55 -29
- package/source/components/Toc.tsx +76 -55
- package/source/components/YouTube.tsx +1 -1
- package/source/content-collections.ts +4 -0
- package/source/elements/alert-dialog.tsx +1 -1
- package/source/elements/alert.tsx +1 -1
- package/source/elements/badge.tsx +4 -0
- package/source/elements/button-group.tsx +4 -4
- package/source/elements/button.tsx +10 -6
- package/source/elements/calendar.tsx +1 -1
- package/source/elements/card.tsx +3 -3
- package/source/elements/command.tsx +7 -4
- package/source/elements/context-menu.tsx +5 -5
- package/source/elements/dialog.tsx +1 -1
- package/source/elements/drawer.tsx +59 -43
- package/source/elements/dropdown-menu.tsx +6 -6
- package/source/elements/empty.tsx +1 -1
- package/source/elements/field.tsx +1 -1
- package/source/elements/hover-card.tsx +1 -1
- package/source/elements/input-group.tsx +4 -4
- package/source/elements/input.tsx +1 -1
- package/source/elements/item.tsx +1 -1
- package/source/elements/menubar.tsx +8 -8
- package/source/elements/native-select.tsx +1 -1
- package/source/elements/popover.tsx +1 -1
- package/source/elements/select.tsx +3 -3
- package/source/elements/sidebar.tsx +8 -21
- package/source/elements/tabs.tsx +2 -2
- package/source/elements/textarea.tsx +1 -1
- package/source/elements/toggle-group.tsx +2 -2
- package/source/elements/toggle.tsx +1 -1
- package/source/env.d.ts +1 -0
- package/source/head.ts +7 -0
- package/source/helpers/article-search.tsx +192 -0
- package/source/helpers/dependency-dir.ts +20 -0
- package/source/helpers/dependency-dir.unit.ts +39 -0
- package/source/helpers/head.ts +94 -0
- package/source/helpers/head.unit.ts +116 -0
- package/source/helpers/nav-items.tsx +102 -0
- package/source/helpers/render-inline-markdown.ts +44 -0
- package/source/helpers/render-inline-markdown.unit.ts +30 -0
- package/source/helpers/search-highlight.ts +73 -0
- package/source/helpers/search-highlight.unit.ts +42 -0
- package/source/helpers/target-export.ts +28 -0
- package/source/helpers/target-export.unit.ts +37 -0
- package/source/helpers/theme.ts +40 -6
- package/source/helpers/theme.unit.ts +32 -0
- package/source/hooks/hydrated.ts +18 -0
- package/source/index.ts +1 -0
- package/source/models/config.ts +13 -0
- package/source/models/section.ts +3 -0
- package/source/plugins/vite-livemark.ts +54 -95
- package/source/program.ts +2 -0
- package/source/routes/$.tsx +27 -9
- package/source/routes/__root.tsx +51 -71
- package/source/settings.ts +1 -0
- package/source/styles/general.css +59 -11
- package/source/styles/markdown.css +13 -7
- package/source/styles/shape.css +14 -0
- package/.agents/skills/change-review/SKILL.md +0 -47
- package/.agents/skills/livemark-customization/SKILL.md +0 -118
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { config } from "livemark:virtual"
|
|
2
|
+
import { Link, useLocation } from "@tanstack/react-router"
|
|
3
|
+
import {
|
|
4
|
+
sectionFirstArticle,
|
|
5
|
+
sectionFlatArticles,
|
|
6
|
+
sortedArticles,
|
|
7
|
+
} from "../content/article.ts"
|
|
8
|
+
import { DynamicIcon } from "./dynamic-icon.tsx"
|
|
9
|
+
import { prefixUrl } from "./prefix-url.ts"
|
|
10
|
+
import { customSectionActive } from "./section.ts"
|
|
11
|
+
|
|
12
|
+
export type NavItem = {
|
|
13
|
+
key: string
|
|
14
|
+
title: string
|
|
15
|
+
icon?: string
|
|
16
|
+
suffix?: string
|
|
17
|
+
active: boolean
|
|
18
|
+
href?: string
|
|
19
|
+
target?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useNavItems(activePrefix?: string) {
|
|
23
|
+
const pathname = useLocation({ select: l => l.pathname })
|
|
24
|
+
const sections = config.sections ?? []
|
|
25
|
+
const items: NavItem[] = sections
|
|
26
|
+
.filter(section => section.position === "header")
|
|
27
|
+
.map(section => {
|
|
28
|
+
if (section.type === "custom") {
|
|
29
|
+
return {
|
|
30
|
+
key: section.url,
|
|
31
|
+
title: section.title,
|
|
32
|
+
icon: section.icon,
|
|
33
|
+
active: customSectionActive(section.url, pathname),
|
|
34
|
+
href: section.url,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const suffix =
|
|
38
|
+
section.type === "changelog" && section.version
|
|
39
|
+
? sortedArticles.find(
|
|
40
|
+
a => a.path === sectionFlatArticles.get(section.prefix)?.[0],
|
|
41
|
+
)?.title
|
|
42
|
+
: undefined
|
|
43
|
+
return {
|
|
44
|
+
key: section.prefix,
|
|
45
|
+
title: section.title,
|
|
46
|
+
icon: section.icon,
|
|
47
|
+
suffix,
|
|
48
|
+
active: activePrefix === section.prefix,
|
|
49
|
+
target:
|
|
50
|
+
section.type === "blog" || section.type === "changelog"
|
|
51
|
+
? section.prefix
|
|
52
|
+
: sectionFirstArticle.get(section.prefix),
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
if (!sections.length) {
|
|
56
|
+
items.push({ key: "/", title: "Docs", active: true, target: "/" })
|
|
57
|
+
}
|
|
58
|
+
for (const link of config.links ?? []) {
|
|
59
|
+
if (link.position !== "header") continue
|
|
60
|
+
if (link.prefix && link.prefix !== activePrefix) continue
|
|
61
|
+
items.push({
|
|
62
|
+
key: link.url,
|
|
63
|
+
title: link.title,
|
|
64
|
+
icon: link.icon,
|
|
65
|
+
active: false,
|
|
66
|
+
href: link.url,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
return items
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function renderNavAnchor(item: NavItem, className?: string) {
|
|
73
|
+
const children = (
|
|
74
|
+
<>
|
|
75
|
+
{item.icon && <DynamicIcon name={item.icon} className="size-3.5" />}
|
|
76
|
+
<span>
|
|
77
|
+
{item.title}
|
|
78
|
+
{item.suffix && <span className="opacity-80"> ({item.suffix})</span>}
|
|
79
|
+
</span>
|
|
80
|
+
</>
|
|
81
|
+
)
|
|
82
|
+
if (item.href) {
|
|
83
|
+
return (
|
|
84
|
+
<a href={prefixUrl(item.href, config.base)} className={className}>
|
|
85
|
+
{children}
|
|
86
|
+
</a>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
return (
|
|
90
|
+
<Link
|
|
91
|
+
to={item.target === "/" ? "/" : "/$"}
|
|
92
|
+
params={item.target && item.target !== "/" ? splatFor(item.target) : {}}
|
|
93
|
+
className={className}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</Link>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function splatFor(pathname: string) {
|
|
101
|
+
return { _splat: pathname.replace(/^\/|\/$/g, "") }
|
|
102
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import rehypeStringify from "rehype-stringify"
|
|
2
|
+
import remarkGfm from "remark-gfm"
|
|
3
|
+
import remarkParse from "remark-parse"
|
|
4
|
+
import remarkRehype from "remark-rehype"
|
|
5
|
+
import { unified } from "unified"
|
|
6
|
+
import { visit } from "unist-util-visit"
|
|
7
|
+
import { prefixUrl } from "./prefix-url.ts"
|
|
8
|
+
|
|
9
|
+
interface HastNode {
|
|
10
|
+
type: string
|
|
11
|
+
tagName?: string
|
|
12
|
+
properties?: Record<string, unknown>
|
|
13
|
+
children?: HastNode[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderInlineMarkdown(markdown: string, base: string | undefined) {
|
|
17
|
+
const html = unified()
|
|
18
|
+
.use(remarkParse)
|
|
19
|
+
.use(remarkGfm)
|
|
20
|
+
.use(remarkRehype)
|
|
21
|
+
.use(() => (tree: HastNode) => rewriteLinks(tree, base))
|
|
22
|
+
.use(() => unwrapParagraphs)
|
|
23
|
+
.use(rehypeStringify)
|
|
24
|
+
.processSync(markdown)
|
|
25
|
+
return String(html).trim()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rewriteLinks(tree: HastNode, base: string | undefined) {
|
|
29
|
+
visit(tree, "element", (node: HastNode) => {
|
|
30
|
+
if (node.tagName !== "a" || typeof node.properties?.href !== "string") return
|
|
31
|
+
const href = node.properties.href
|
|
32
|
+
node.properties.href = prefixUrl(href, base)
|
|
33
|
+
if (/^https?:\/\//.test(href)) {
|
|
34
|
+
node.properties.target = "_blank"
|
|
35
|
+
node.properties.rel = ["noopener", "noreferrer"]
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unwrapParagraphs(tree: HastNode) {
|
|
41
|
+
tree.children = (tree.children ?? []).flatMap(child =>
|
|
42
|
+
child.type === "element" && child.tagName === "p" ? (child.children ?? []) : [child],
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
2
|
+
import { renderInlineMarkdown } from "./render-inline-markdown.ts"
|
|
3
|
+
|
|
4
|
+
describe("renderInlineMarkdown", () => {
|
|
5
|
+
it("should render plain text without a paragraph", () => {
|
|
6
|
+
expect(renderInlineMarkdown("Hello world", undefined)).toBe("Hello world")
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("should render bold, emphasis and code", () => {
|
|
10
|
+
expect(renderInlineMarkdown("**a** _b_ `c`", undefined)).toBe(
|
|
11
|
+
"<strong>a</strong> <em>b</em> <code>c</code>",
|
|
12
|
+
)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("should open absolute links in a new tab", () => {
|
|
16
|
+
expect(renderInlineMarkdown("[x](https://example.com)", undefined)).toBe(
|
|
17
|
+
'<a href="https://example.com" target="_blank" rel="noopener noreferrer">x</a>',
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("should prefix root-relative links with the base", () => {
|
|
22
|
+
expect(renderInlineMarkdown("[x](/blog/)", "/repo")).toBe(
|
|
23
|
+
'<a href="/repo/blog/">x</a>',
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("should drop raw HTML tags", () => {
|
|
28
|
+
expect(renderInlineMarkdown("a <b onclick=x>c</b>", undefined)).toBe("a c")
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEffect, type RefObject } from "react"
|
|
2
|
+
|
|
3
|
+
const HIGHLIGHT_NAME = "search-match"
|
|
4
|
+
|
|
5
|
+
/** Build a case-insensitive pattern matching each word of a search query */
|
|
6
|
+
export function buildHighlightPattern(query: string) {
|
|
7
|
+
const words = [...new Set(query.toLowerCase().split(/\s+/))]
|
|
8
|
+
.filter(word => word.length >= 2)
|
|
9
|
+
.sort((a, b) => b.length - a.length)
|
|
10
|
+
.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
11
|
+
if (!words.length) return undefined
|
|
12
|
+
return new RegExp(words.join("|"), "gi")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Find the `[start, end]` offsets of every pattern match in a text */
|
|
16
|
+
export function findTextMatches(text: string, pattern: RegExp) {
|
|
17
|
+
return [...text.matchAll(pattern)].map(
|
|
18
|
+
match => [match.index, match.index + match[0].length] as const,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Highlight search query matches under `root` and scroll to the first visible one;
|
|
23
|
+
* `page` re-runs it when the same query lands on another page */
|
|
24
|
+
export function useSearchHighlight(
|
|
25
|
+
root: RefObject<HTMLElement | null>,
|
|
26
|
+
page: string,
|
|
27
|
+
query?: string,
|
|
28
|
+
) {
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const pattern = query ? buildHighlightPattern(query) : undefined
|
|
31
|
+
if (!pattern || !root.current || typeof CSS === "undefined" || !CSS.highlights) return
|
|
32
|
+
|
|
33
|
+
const ranges = collectMatchRanges(root.current, pattern)
|
|
34
|
+
CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges))
|
|
35
|
+
|
|
36
|
+
// An explicit anchor in the URL wins over the first match. The frame delay lets
|
|
37
|
+
// the router's scroll restoration, which resets to the top, run first.
|
|
38
|
+
const frame = window.location.hash
|
|
39
|
+
? undefined
|
|
40
|
+
: requestAnimationFrame(() => scrollToFirstVisible(ranges))
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
if (frame !== undefined) cancelAnimationFrame(frame)
|
|
44
|
+
CSS.highlights.delete(HIGHLIGHT_NAME)
|
|
45
|
+
}
|
|
46
|
+
}, [root, page, query])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectMatchRanges(root: HTMLElement, pattern: RegExp) {
|
|
50
|
+
const ranges: Range[] = []
|
|
51
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
52
|
+
acceptNode: node =>
|
|
53
|
+
node.parentElement?.closest("pre, script, style, svg, textarea")
|
|
54
|
+
? NodeFilter.FILTER_REJECT
|
|
55
|
+
: NodeFilter.FILTER_ACCEPT,
|
|
56
|
+
})
|
|
57
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
58
|
+
for (const [start, end] of findTextMatches(node.textContent ?? "", pattern)) {
|
|
59
|
+
const range = new Range()
|
|
60
|
+
range.setStart(node, start)
|
|
61
|
+
range.setEnd(node, end)
|
|
62
|
+
ranges.push(range)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return ranges
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scrollToFirstVisible(ranges: Range[]) {
|
|
69
|
+
const first = ranges.find(range =>
|
|
70
|
+
range.startContainer.parentElement?.checkVisibility(),
|
|
71
|
+
)
|
|
72
|
+
first?.startContainer.parentElement?.scrollIntoView({ block: "center" })
|
|
73
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
2
|
+
import { buildHighlightPattern, findTextMatches } from "./search-highlight.ts"
|
|
3
|
+
|
|
4
|
+
describe("buildHighlightPattern", () => {
|
|
5
|
+
it("should return undefined for an empty query", () => {
|
|
6
|
+
expect(buildHighlightPattern(" ")).toBeUndefined()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("should drop single-character words", () => {
|
|
10
|
+
expect(buildHighlightPattern("a b")).toBeUndefined()
|
|
11
|
+
expect(buildHighlightPattern("a config")?.source).toBe("config")
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("should put longer words first and dedupe case-insensitively", () => {
|
|
15
|
+
expect(buildHighlightPattern("Side sidebar side")?.source).toBe("sidebar|side")
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("should escape regex syntax", () => {
|
|
19
|
+
expect(buildHighlightPattern("c++ (x)")?.source).toBe("c\\+\\+|\\(x\\)")
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe("findTextMatches", () => {
|
|
24
|
+
it("should find every word case-insensitively", () => {
|
|
25
|
+
const pattern = buildHighlightPattern("search config")
|
|
26
|
+
expect(pattern && findTextMatches("Search the Config, search!", pattern)).toEqual([
|
|
27
|
+
[0, 6],
|
|
28
|
+
[11, 17],
|
|
29
|
+
[19, 25],
|
|
30
|
+
])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("should prefer the longest word at a shared position", () => {
|
|
34
|
+
const pattern = buildHighlightPattern("side sidebar")
|
|
35
|
+
expect(pattern && findTextMatches("the sidebar", pattern)).toEqual([[4, 11]])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("should return no matches when nothing matches", () => {
|
|
39
|
+
const pattern = buildHighlightPattern("orama")
|
|
40
|
+
expect(pattern && findTextMatches("nothing here", pattern)).toEqual([])
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Map a `livemark/<subpath>` import onto the target copy its `exports` entry names, so
|
|
5
|
+
* escapes importing Livemark modules reach other escapes, not the package originals.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveTargetExport(props: {
|
|
8
|
+
id: string
|
|
9
|
+
targetDir: string
|
|
10
|
+
exports: Record<string, string>
|
|
11
|
+
}) {
|
|
12
|
+
const queryIndex = props.id.indexOf("?")
|
|
13
|
+
const specifier = queryIndex === -1 ? props.id : props.id.slice(0, queryIndex)
|
|
14
|
+
const query = queryIndex === -1 ? "" : props.id.slice(queryIndex)
|
|
15
|
+
if (!specifier.startsWith("livemark/")) return undefined
|
|
16
|
+
|
|
17
|
+
const subpath = `./${specifier.slice("livemark/".length)}`
|
|
18
|
+
for (const [key, value] of Object.entries(props.exports)) {
|
|
19
|
+
const [prefix, suffix] = key.split("*")
|
|
20
|
+
if (suffix === undefined || !prefix || !value.startsWith("./source/")) continue
|
|
21
|
+
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue
|
|
22
|
+
const name = subpath.slice(prefix.length, subpath.length - suffix.length)
|
|
23
|
+
return (
|
|
24
|
+
join(props.targetDir, value.slice("./source/".length).replace("*", name)) + query
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
3
|
+
import packageJson from "../../package.json" with { type: "json" }
|
|
4
|
+
import { resolveTargetExport } from "./target-export.ts"
|
|
5
|
+
|
|
6
|
+
const targetDir = "/project/.livemark/.target"
|
|
7
|
+
|
|
8
|
+
function resolveId(id: string) {
|
|
9
|
+
return resolveTargetExport({ id, targetDir, exports: packageJson.exports })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("resolveTargetExport", () => {
|
|
13
|
+
it("maps each source subpath export into the target", () => {
|
|
14
|
+
expect(resolveId("livemark/components/Footer")).toBe(
|
|
15
|
+
join(targetDir, "components/Footer.tsx"),
|
|
16
|
+
)
|
|
17
|
+
expect(resolveId("livemark/elements/button")).toBe(
|
|
18
|
+
join(targetDir, "elements/button.tsx"),
|
|
19
|
+
)
|
|
20
|
+
expect(resolveId("livemark/hooks/in-view")).toBe(join(targetDir, "hooks/in-view.ts"))
|
|
21
|
+
expect(resolveId("livemark/styles/general")).toBe(
|
|
22
|
+
join(targetDir, "styles/general.css"),
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("keeps the query", () => {
|
|
27
|
+
expect(resolveId("livemark/styles/general?url")).toBe(
|
|
28
|
+
`${join(targetDir, "styles/general.css")}?url`,
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("leaves other specifiers alone", () => {
|
|
33
|
+
expect(resolveId("livemark")).toBeUndefined()
|
|
34
|
+
expect(resolveId("livemark/unknown/thing")).toBeUndefined()
|
|
35
|
+
expect(resolveId("react")).toBeUndefined()
|
|
36
|
+
})
|
|
37
|
+
})
|
package/source/helpers/theme.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const STORAGE_KEY = "livemark-theme"
|
|
2
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)"
|
|
2
3
|
|
|
3
4
|
export type Theme = "light" | "dark"
|
|
5
|
+
export type ThemePref = Theme | "system"
|
|
4
6
|
|
|
5
|
-
export function
|
|
6
|
-
|
|
7
|
-
return
|
|
7
|
+
export function resolveTheme(pref: ThemePref, prefersDark: boolean): Theme {
|
|
8
|
+
if (pref === "system") return prefersDark ? "dark" : "light"
|
|
9
|
+
return pref
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export function activateTheme(theme: Theme) {
|
|
@@ -21,7 +23,39 @@ export function getCurrentTheme(): Theme {
|
|
|
21
23
|
: "light"
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// Mirrors `buildThemeScript`: anything but an explicit light or dark follows the OS.
|
|
27
|
+
export function getThemePref(): ThemePref {
|
|
28
|
+
let stored: string | null = null
|
|
29
|
+
try {
|
|
30
|
+
stored = localStorage.getItem(STORAGE_KEY)
|
|
31
|
+
} catch {}
|
|
32
|
+
return stored === "light" || stored === "dark" ? stored : "system"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setTheme(pref: ThemePref) {
|
|
36
|
+
applyThemePref(pref)
|
|
37
|
+
try {
|
|
38
|
+
localStorage.setItem(STORAGE_KEY, pref)
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Returns the OS listener's cleanup, so an effect StrictMode runs twice keeps one.
|
|
43
|
+
export function startTheme() {
|
|
44
|
+
const query = matchMedia(DARK_QUERY)
|
|
45
|
+
const handleChange = () => {
|
|
46
|
+
if (getThemePref() === "system") applyThemePref("system")
|
|
47
|
+
}
|
|
48
|
+
applyThemePref(getThemePref())
|
|
49
|
+
query.addEventListener("change", handleChange)
|
|
50
|
+
return () => query.removeEventListener("change", handleChange)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The pre-paint twin of `startTheme`, inlined in <head> so a dark reader never sees
|
|
54
|
+
// the light palette flash before hydration.
|
|
55
|
+
export function buildThemeScript() {
|
|
56
|
+
return `try{var t=localStorage.getItem(${JSON.stringify(STORAGE_KEY)})}catch(e){}if(t==="dark"||(t!=="light"&&matchMedia(${JSON.stringify(DARK_QUERY)}).matches))document.documentElement.classList.add("dark");`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function applyThemePref(pref: ThemePref) {
|
|
60
|
+
activateTheme(resolveTheme(pref, matchMedia(DARK_QUERY).matches))
|
|
27
61
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vite-plus/test"
|
|
2
|
+
import { buildThemeScript, resolveTheme } from "./theme.ts"
|
|
3
|
+
|
|
4
|
+
function runThemeScript(stored: string | null, prefersDark: boolean) {
|
|
5
|
+
const classes = new Set<string>()
|
|
6
|
+
vi.stubGlobal("localStorage", { getItem: () => stored })
|
|
7
|
+
vi.stubGlobal("matchMedia", () => ({ matches: prefersDark }))
|
|
8
|
+
vi.stubGlobal("document", {
|
|
9
|
+
documentElement: { classList: { add: (name: string) => classes.add(name) } },
|
|
10
|
+
})
|
|
11
|
+
new Function(buildThemeScript())()
|
|
12
|
+
vi.unstubAllGlobals()
|
|
13
|
+
return classes.has("dark") ? "dark" : "light"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("buildThemeScript", () => {
|
|
17
|
+
it.each([
|
|
18
|
+
["light", false],
|
|
19
|
+
["light", true],
|
|
20
|
+
["dark", false],
|
|
21
|
+
["dark", true],
|
|
22
|
+
["system", false],
|
|
23
|
+
["system", true],
|
|
24
|
+
] as const)("paints what resolveTheme(%s, prefersDark %s) does", (pref, dark) => {
|
|
25
|
+
expect(runThemeScript(pref, dark)).toBe(resolveTheme(pref, dark))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it.each([null, "", "auto"])("follows the OS for a stored %j", stored => {
|
|
29
|
+
expect(runThemeScript(stored, true)).toBe("dark")
|
|
30
|
+
expect(runThemeScript(stored, false)).toBe("light")
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react"
|
|
2
|
+
|
|
3
|
+
/** False during SSR and the hydrating render, true from the first client render on. */
|
|
4
|
+
export function useHydrated() {
|
|
5
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function subscribe() {
|
|
9
|
+
return () => {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getSnapshot() {
|
|
13
|
+
return true
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getServerSnapshot() {
|
|
17
|
+
return false
|
|
18
|
+
}
|
package/source/index.ts
CHANGED
package/source/models/config.ts
CHANGED
|
@@ -22,6 +22,15 @@ export type ViteOverride =
|
|
|
22
22
|
* resolves it to a string before any runtime sees it. */
|
|
23
23
|
export type Base = string | ((command: "build" | "serve") => string | undefined)
|
|
24
24
|
|
|
25
|
+
/** Extra site-wide `<head>` tags, appended after Livemark's defaults. A meta tag
|
|
26
|
+
* sharing a default's `name` or `property` replaces it. */
|
|
27
|
+
export type Head = z.infer<typeof Head>
|
|
28
|
+
export const Head = z.object({
|
|
29
|
+
meta: z.array(z.record(z.string(), z.string())).optional(),
|
|
30
|
+
links: z.array(z.record(z.string(), z.string())).optional(),
|
|
31
|
+
scripts: z.array(z.record(z.string(), z.string())).optional(),
|
|
32
|
+
})
|
|
33
|
+
|
|
25
34
|
/**
|
|
26
35
|
* User-provided configuration for Livemark.
|
|
27
36
|
*
|
|
@@ -46,6 +55,7 @@ export const UserConfig = z.object({
|
|
|
46
55
|
logo: z.string().optional(),
|
|
47
56
|
title: z.string().default("Livemark"),
|
|
48
57
|
description: z.string().default("Markdown site generator"),
|
|
58
|
+
banner: z.string().optional(),
|
|
49
59
|
include: z.union([z.string(), z.array(z.string())]).default("**/*.md"),
|
|
50
60
|
exclude: z.union([z.string(), z.array(z.string())]).optional(),
|
|
51
61
|
codeThemeLight: CodeThemeLight.default("catppuccin-latte"),
|
|
@@ -53,6 +63,7 @@ export const UserConfig = z.object({
|
|
|
53
63
|
links: z.array(Link).optional(),
|
|
54
64
|
sections: z.array(Section).optional(),
|
|
55
65
|
patches: z.array(Patch).optional(),
|
|
66
|
+
head: Head.optional(),
|
|
56
67
|
// Stored as `unknown` in the schema (zod can't type a Vite UserConfig
|
|
57
68
|
// or a function returning one meaningfully); the public TypeScript
|
|
58
69
|
// surface above lifts it back to ViteOverride for editor support.
|
|
@@ -69,8 +80,10 @@ export const WebsiteConfig = UserConfig.pick({
|
|
|
69
80
|
site: true,
|
|
70
81
|
favicon: true,
|
|
71
82
|
logo: true,
|
|
83
|
+
banner: true,
|
|
72
84
|
links: true,
|
|
73
85
|
sections: true,
|
|
86
|
+
head: true,
|
|
74
87
|
}).extend({
|
|
75
88
|
base: z.string().optional(),
|
|
76
89
|
})
|