livemark 0.2.0 → 0.7.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.md +9 -0
- package/README.md +5 -1
- package/build/actions/changelog/build.d.ts +14 -0
- package/build/actions/changelog/build.js +158 -0
- package/build/actions/changelog/build.unit.js +66 -0
- package/build/actions/config/define.d.ts +6 -0
- package/build/actions/config/define.js +8 -0
- package/build/actions/config/define.unit.js +29 -0
- package/build/actions/config/load.d.ts +5 -0
- package/build/actions/config/load.js +25 -0
- package/build/actions/config/load.unit.js +23 -0
- package/build/commands/build.d.ts +5 -0
- package/build/commands/build.js +14 -0
- package/build/commands/escape.d.ts +9 -0
- package/build/commands/escape.js +56 -0
- package/build/commands/preview.d.ts +5 -0
- package/build/commands/preview.js +18 -0
- package/build/commands/start.d.ts +5 -0
- package/build/commands/start.js +19 -0
- package/build/docs/markdown/includes/example.d.ts +58 -0
- package/build/docs/markdown/includes/example.js +5 -0
- package/build/entrypoints/main.d.ts +2 -0
- package/build/entrypoints/main.js +12 -0
- package/build/helpers/program.d.ts +2 -0
- package/build/helpers/program.js +5 -0
- package/build/index.d.ts +1 -4
- package/build/index.js +2 -5
- package/build/livemark.config.d.ts +58 -0
- package/build/livemark.config.js +25 -0
- package/build/models/config.d.ts +340 -0
- package/build/models/config.js +34 -0
- package/build/models/link.d.ts +13 -0
- package/build/models/link.js +9 -0
- package/build/models/patch.d.ts +21 -0
- package/build/models/patch.js +7 -0
- package/build/models/section.d.ts +75 -0
- package/build/models/section.js +26 -0
- package/build/models/theme.d.ts +72 -0
- package/build/models/theme.js +73 -0
- package/build/program.d.ts +2 -0
- package/build/program.js +16 -0
- package/build/vite.config.d.ts +2 -0
- package/build/vite.config.js +48 -0
- package/build/website/assets/logo.svg +117 -0
- package/build/website/client.tsx +10 -0
- package/build/website/components/Abbr.tsx +17 -0
- package/build/website/components/AnsiCode.tsx +15 -0
- package/build/website/components/Article.tsx +172 -0
- package/build/website/components/BackToTop.tsx +32 -0
- package/build/website/components/Banner.tsx +32 -0
- package/build/website/components/BlogIndex.tsx +121 -0
- package/build/website/components/BlogSidebar.tsx +162 -0
- package/build/website/components/Breadcrumbs.tsx +22 -0
- package/build/website/components/Callout.tsx +72 -0
- package/build/website/components/Card.tsx +48 -0
- package/build/website/components/Cards.tsx +8 -0
- package/build/website/components/ChangelogIndex.tsx +111 -0
- package/build/website/components/ChangelogSidebar.tsx +120 -0
- package/build/website/components/CodeBlock.tsx +135 -0
- package/build/website/components/CodeTabs.tsx +83 -0
- package/build/website/components/Column.tsx +4 -0
- package/build/website/components/Columns.tsx +12 -0
- package/build/website/components/Content.tsx +7 -0
- package/build/website/components/ContentTab.tsx +7 -0
- package/build/website/components/ContentTabs.tsx +82 -0
- package/build/website/components/DefaultCatchBoundary.tsx +54 -0
- package/build/website/components/DefinitionList.tsx +21 -0
- package/build/website/components/Details.tsx +30 -0
- package/build/website/components/FileTree.tsx +52 -0
- package/build/website/components/Footer.tsx +16 -0
- package/build/website/components/FootnoteRef.tsx +52 -0
- package/build/website/components/Heading.tsx +20 -0
- package/build/website/components/InlineBadge.tsx +9 -0
- package/build/website/components/InlineIcon.tsx +10 -0
- package/build/website/components/InlineToc.tsx +31 -0
- package/build/website/components/Layout.tsx +190 -0
- package/build/website/components/LinkButton.tsx +31 -0
- package/build/website/components/Mermaid.tsx +18 -0
- package/build/website/components/MermaidRenderer.tsx +37 -0
- package/build/website/components/MiniSidebar.tsx +49 -0
- package/build/website/components/NotFound.tsx +27 -0
- package/build/website/components/PackageTabs.tsx +74 -0
- package/build/website/components/PageToolbar.tsx +75 -0
- package/build/website/components/PrevNext.tsx +60 -0
- package/build/website/components/Search.tsx +28 -0
- package/build/website/components/SearchDialog.tsx +245 -0
- package/build/website/components/Section.tsx +24 -0
- package/build/website/components/SectionHeader.tsx +35 -0
- package/build/website/components/Sidebar.tsx +160 -0
- package/build/website/components/SidebarLinks.tsx +70 -0
- package/build/website/components/SidebarSections.tsx +71 -0
- package/build/website/components/SiteTitle.tsx +21 -0
- package/build/website/components/SortableTable.tsx +81 -0
- package/build/website/components/SoundCloud.tsx +16 -0
- package/build/website/components/TagIndex.tsx +120 -0
- package/build/website/components/Theme.tsx +39 -0
- package/build/website/components/Toc.tsx +226 -0
- package/build/website/components/YouTube.tsx +16 -0
- package/build/website/components/ZoomImage.tsx +25 -0
- package/build/website/content/article.ts +78 -0
- package/build/website/content/tag.ts +29 -0
- package/build/website/content-collections.ts +208 -0
- package/build/website/elements/accordion.tsx +78 -0
- package/build/website/elements/alert-dialog.tsx +185 -0
- package/build/website/elements/alert.tsx +74 -0
- package/build/website/elements/aspect-ratio.tsx +22 -0
- package/build/website/elements/avatar.tsx +107 -0
- package/build/website/elements/badge.tsx +51 -0
- package/build/website/elements/breadcrumb.tsx +120 -0
- package/build/website/elements/button-group.tsx +87 -0
- package/build/website/elements/button.tsx +57 -0
- package/build/website/elements/calendar.tsx +229 -0
- package/build/website/elements/card.tsx +102 -0
- package/build/website/elements/carousel.tsx +240 -0
- package/build/website/elements/chart.tsx +372 -0
- package/build/website/elements/checkbox.tsx +26 -0
- package/build/website/elements/collapsible.tsx +19 -0
- package/build/website/elements/command.tsx +210 -0
- package/build/website/elements/context-menu.tsx +270 -0
- package/build/website/elements/dialog.tsx +152 -0
- package/build/website/elements/direction.tsx +4 -0
- package/build/website/elements/drawer.tsx +127 -0
- package/build/website/elements/dropdown-menu.tsx +260 -0
- package/build/website/elements/empty.tsx +104 -0
- package/build/website/elements/field.tsx +235 -0
- package/build/website/elements/hover-card.tsx +49 -0
- package/build/website/elements/input-group.tsx +171 -0
- package/build/website/elements/input-otp.tsx +84 -0
- package/build/website/elements/input.tsx +18 -0
- package/build/website/elements/item.tsx +199 -0
- package/build/website/elements/kbd.tsx +26 -0
- package/build/website/elements/label.tsx +17 -0
- package/build/website/elements/menubar.tsx +282 -0
- package/build/website/elements/native-select.tsx +56 -0
- package/build/website/elements/navigation-menu.tsx +171 -0
- package/build/website/elements/pagination.tsx +133 -0
- package/build/website/elements/popover.tsx +88 -0
- package/build/website/elements/progress.tsx +81 -0
- package/build/website/elements/radio-group.tsx +36 -0
- package/build/website/elements/resizable.tsx +48 -0
- package/build/website/elements/scroll-area.tsx +51 -0
- package/build/website/elements/select.tsx +200 -0
- package/build/website/elements/separator.tsx +22 -0
- package/build/website/elements/sheet.tsx +130 -0
- package/build/website/elements/sidebar.tsx +717 -0
- package/build/website/elements/skeleton.tsx +13 -0
- package/build/website/elements/slider.tsx +57 -0
- package/build/website/elements/sonner.tsx +40 -0
- package/build/website/elements/spinner.tsx +16 -0
- package/build/website/elements/switch.tsx +30 -0
- package/build/website/elements/table.tsx +112 -0
- package/build/website/elements/tabs.tsx +79 -0
- package/build/website/elements/textarea.tsx +16 -0
- package/build/website/elements/toggle-group.tsx +87 -0
- package/build/website/elements/toggle.tsx +43 -0
- package/build/website/elements/tooltip.tsx +67 -0
- package/build/website/env.d.ts +14 -0
- package/build/website/helpers/article-icon.ts +61 -0
- package/build/website/helpers/article.ts +90 -0
- package/build/website/helpers/changelog-source.ts +16 -0
- package/build/website/helpers/changelog-source.unit.ts +38 -0
- package/build/website/helpers/dynamic-icon-resolver.tsx +19 -0
- package/build/website/helpers/dynamic-icon.tsx +12 -0
- package/build/website/helpers/favicon.ts +7 -0
- package/build/website/helpers/model.ts +4 -0
- package/build/website/helpers/resolve-asset-path.ts +9 -0
- package/build/website/helpers/resolve-includes.ts +131 -0
- package/build/website/helpers/resolve-includes.unit.ts +126 -0
- package/build/website/helpers/search-text.ts +15 -0
- package/build/website/helpers/section.ts +50 -0
- package/build/website/helpers/section.unit.ts +71 -0
- package/build/website/helpers/style.ts +6 -0
- package/build/website/helpers/theme.ts +27 -0
- package/build/website/helpers/toc-context.ts +10 -0
- package/build/website/helpers/toc.ts +46 -0
- package/build/website/helpers/toc.unit.ts +74 -0
- package/build/website/hooks/in-view.ts +30 -0
- package/build/website/hooks/media.ts +19 -0
- package/build/website/models/article.ts +55 -0
- package/build/website/models/toc.ts +5 -0
- package/build/website/plugins/rehype-inline-code.ts +107 -0
- package/build/website/plugins/rehype-task-list-a11y.ts +33 -0
- package/build/website/plugins/remark-abbr.ts +46 -0
- package/build/website/plugins/remark-abbr.unit.ts +99 -0
- package/build/website/plugins/remark-ansi.ts +248 -0
- package/build/website/plugins/remark-ansi.unit.ts +197 -0
- package/build/website/plugins/remark-badge.ts +50 -0
- package/build/website/plugins/remark-badge.unit.ts +96 -0
- package/build/website/plugins/remark-button.ts +60 -0
- package/build/website/plugins/remark-button.unit.ts +147 -0
- package/build/website/plugins/remark-callout.ts +57 -0
- package/build/website/plugins/remark-card.ts +80 -0
- package/build/website/plugins/remark-code-tabs.ts +87 -0
- package/build/website/plugins/remark-columns.ts +66 -0
- package/build/website/plugins/remark-custom-heading-id.ts +70 -0
- package/build/website/plugins/remark-custom-heading-id.unit.ts +131 -0
- package/build/website/plugins/remark-deflist.ts +36 -0
- package/build/website/plugins/remark-details.ts +49 -0
- package/build/website/plugins/remark-filetree.ts +89 -0
- package/build/website/plugins/remark-filetree.unit.ts +137 -0
- package/build/website/plugins/remark-github-callout.ts +71 -0
- package/build/website/plugins/remark-icon.ts +42 -0
- package/build/website/plugins/remark-image.ts +34 -0
- package/build/website/plugins/remark-media.ts +62 -0
- package/build/website/plugins/remark-media.unit.ts +74 -0
- package/build/website/plugins/remark-mermaid.ts +23 -0
- package/build/website/plugins/remark-npm.ts +52 -0
- package/build/website/plugins/remark-steps.ts +36 -0
- package/build/website/plugins/remark-steps.unit.ts +97 -0
- package/build/website/plugins/remark-tab.ts +100 -0
- package/build/website/plugins/remark-tab.unit.ts +181 -0
- package/build/website/plugins/remark-toc.ts +39 -0
- package/build/website/plugins/remark-toc.unit.ts +82 -0
- package/build/website/plugins/shiki-icon.ts +82 -0
- package/build/website/plugins/shiki-line-highlight.ts +174 -0
- package/build/website/plugins/vite-livemark.ts +91 -0
- package/build/website/routeTree.gen.ts +86 -0
- package/build/website/router.tsx +34 -0
- package/build/website/routes/$.tsx +92 -0
- package/build/website/routes/__root.tsx +97 -0
- package/build/website/server.ts +7 -0
- package/build/website/styles/general.css +163 -0
- package/build/website/styles/markdown.css +214 -0
- package/build/website/vite.config.ts +71 -0
- package/entrypoints/main.ts +12 -0
- package/package.json +120 -12
- package/build/descriptor/index.d.ts +0 -3
- package/build/descriptor/index.js +0 -4
- package/build/descriptor/load.d.ts +0 -8
- package/build/descriptor/load.js +0 -40
- package/build/descriptor/node.d.ts +0 -125
- package/build/descriptor/node.js +0 -9
- package/build/descriptor/save.d.ts +0 -8
- package/build/descriptor/save.js +0 -17
- package/build/descriptor/save.spec.js +0 -80
- package/build/descriptor/validate.d.ts +0 -9
- package/build/descriptor/validate.js +0 -14
- package/build/descriptor/validate.spec.js +0 -124
- package/build/dialect/Dialect.d.ts +0 -88
- package/build/dialect/Dialect.js +0 -2
- package/build/dialect/index.d.ts +0 -3
- package/build/dialect/index.js +0 -3
- package/build/dialect/load.d.ts +0 -4
- package/build/dialect/load.js +0 -7
- package/build/dialect/save.d.ts +0 -9
- package/build/dialect/save.js +0 -13
- package/build/dialect/validate.d.ts +0 -1
- package/build/dialect/validate.js +0 -4
- package/build/metadata/index.d.ts +0 -3
- package/build/metadata/index.js +0 -4
- package/build/metadata/load.d.ts +0 -10
- package/build/metadata/load.js +0 -55
- package/build/metadata/node.d.ts +0 -125
- package/build/metadata/node.js +0 -9
- package/build/metadata/save.d.ts +0 -8
- package/build/metadata/save.js +0 -17
- package/build/metadata/save.spec.js +0 -80
- package/build/metadata/validate.d.ts +0 -9
- package/build/metadata/validate.js +0 -14
- package/build/metadata/validate.spec.d.ts +0 -1
- package/build/metadata/validate.spec.js +0 -124
- package/build/package/Contributor.d.ts +0 -21
- package/build/package/Contributor.js +0 -2
- package/build/package/Package.d.ts +0 -65
- package/build/package/Package.js +0 -2
- package/build/package/index.d.ts +0 -4
- package/build/package/index.js +0 -3
- package/build/package/load.d.ts +0 -4
- package/build/package/load.js +0 -7
- package/build/package/save.d.ts +0 -9
- package/build/package/save.js +0 -13
- package/build/package/validate.d.ts +0 -1
- package/build/package/validate.js +0 -4
- package/build/resource/License.d.ts +0 -18
- package/build/resource/License.js +0 -2
- package/build/resource/Resource.d.ts +0 -67
- package/build/resource/Resource.js +0 -2
- package/build/resource/Source.d.ts +0 -17
- package/build/resource/Source.js +0 -2
- package/build/resource/index.d.ts +0 -5
- package/build/resource/index.js +0 -2
- package/build/resource/load.d.ts +0 -1
- package/build/resource/load.js +0 -2
- package/build/resource/save.d.ts +0 -9
- package/build/resource/save.js +0 -13
- package/build/resource/types/index.d.ts +0 -1
- package/build/resource/types/index.js +0 -2
- package/build/resource/types/table.d.ts +0 -29
- package/build/resource/types/table.js +0 -2
- package/build/resource/validate.d.ts +0 -1
- package/build/resource/validate.js +0 -4
- package/build/schema/Field.d.ts +0 -5
- package/build/schema/Field.js +0 -2
- package/build/schema/Schema.d.ts +0 -55
- package/build/schema/Schema.js +0 -2
- package/build/schema/fields/any.d.ts +0 -20
- package/build/schema/fields/any.js +0 -2
- package/build/schema/fields/array.d.ts +0 -32
- package/build/schema/fields/array.js +0 -2
- package/build/schema/fields/base.d.ts +0 -42
- package/build/schema/fields/base.js +0 -2
- package/build/schema/fields/boolean.d.ts +0 -28
- package/build/schema/fields/boolean.js +0 -2
- package/build/schema/fields/date.d.ts +0 -35
- package/build/schema/fields/date.js +0 -2
- package/build/schema/fields/datetime.d.ts +0 -35
- package/build/schema/fields/datetime.js +0 -2
- package/build/schema/fields/duration.d.ts +0 -28
- package/build/schema/fields/duration.js +0 -2
- package/build/schema/fields/geojson.d.ts +0 -26
- package/build/schema/fields/geojson.js +0 -2
- package/build/schema/fields/geopoint.d.ts +0 -27
- package/build/schema/fields/geopoint.js +0 -2
- package/build/schema/fields/index.d.ts +0 -16
- package/build/schema/fields/index.js +0 -17
- package/build/schema/fields/integer.d.ts +0 -37
- package/build/schema/fields/integer.js +0 -2
- package/build/schema/fields/list.d.ts +0 -36
- package/build/schema/fields/list.js +0 -2
- package/build/schema/fields/number.d.ts +0 -40
- package/build/schema/fields/number.js +0 -2
- package/build/schema/fields/object.d.ts +0 -32
- package/build/schema/fields/object.js +0 -2
- package/build/schema/fields/string.d.ts +0 -40
- package/build/schema/fields/string.js +0 -2
- package/build/schema/fields/time.d.ts +0 -35
- package/build/schema/fields/time.js +0 -2
- package/build/schema/fields/year.d.ts +0 -28
- package/build/schema/fields/year.js +0 -2
- package/build/schema/fields/yearmonth.d.ts +0 -28
- package/build/schema/fields/yearmonth.js +0 -2
- package/build/schema/index.d.ts +0 -4
- package/build/schema/index.js +0 -2
- package/build/schema/load.d.ts +0 -1
- package/build/schema/load.js +0 -2
- package/build/schema/save.d.ts +0 -9
- package/build/schema/save.js +0 -13
- package/build/schema/validate.d.ts +0 -1
- package/build/schema/validate.js +0 -4
- package/build/tsconfig.tsbuildinfo +0 -1
- package/index.ts +0 -0
- package/tsconfig.json +0 -10
- package/typedoc.json +0 -4
- /package/build/{descriptor/save.spec.d.ts → actions/changelog/build.unit.d.ts} +0 -0
- /package/build/{descriptor/validate.spec.d.ts → actions/config/define.unit.d.ts} +0 -0
- /package/build/{metadata/save.spec.d.ts → actions/config/load.unit.d.ts} +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ArticleGroup, ArticleNode } from "../models/article.ts"
|
|
2
|
+
|
|
3
|
+
/** Build a tree from a flat sorted list of articles based on path hierarchy */
|
|
4
|
+
export function buildArticleTree(
|
|
5
|
+
articles: {
|
|
6
|
+
path: string
|
|
7
|
+
title: string
|
|
8
|
+
label?: string
|
|
9
|
+
icon: string
|
|
10
|
+
}[],
|
|
11
|
+
) {
|
|
12
|
+
const nodeMap = new Map<string, ArticleNode>()
|
|
13
|
+
|
|
14
|
+
for (const article of articles) {
|
|
15
|
+
nodeMap.set(article.path, {
|
|
16
|
+
path: article.path,
|
|
17
|
+
title: article.title,
|
|
18
|
+
label: article.label,
|
|
19
|
+
icon: article.icon,
|
|
20
|
+
children: [],
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const roots: ArticleNode[] = []
|
|
25
|
+
|
|
26
|
+
for (const article of articles) {
|
|
27
|
+
const node = nodeMap.get(article.path)
|
|
28
|
+
if (!node) continue
|
|
29
|
+
const trimmed = article.path.replace(/\/$/, "")
|
|
30
|
+
const lastSlash = trimmed.lastIndexOf("/")
|
|
31
|
+
const parentPath = lastSlash > 0 ? `${trimmed.slice(0, lastSlash)}/` : ""
|
|
32
|
+
const parent = parentPath ? nodeMap.get(parentPath) : undefined
|
|
33
|
+
|
|
34
|
+
if (parent) {
|
|
35
|
+
parent.children.push(node)
|
|
36
|
+
} else {
|
|
37
|
+
roots.push(node)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return roots
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Partition root nodes into sidebar sections.
|
|
46
|
+
*
|
|
47
|
+
* Walks root nodes in sort order. A root whose article has a `group` field
|
|
48
|
+
* opens a new section; subsequent roots without `group` append to the current
|
|
49
|
+
* section. Roots before the first `group` field land in a single leading
|
|
50
|
+
* section with `name: undefined` (rendered without a label).
|
|
51
|
+
*
|
|
52
|
+
* `group` on non-root articles is ignored — only roots gate section boundaries.
|
|
53
|
+
*/
|
|
54
|
+
export function groupArticleTree(
|
|
55
|
+
tree: ArticleNode[],
|
|
56
|
+
articles: { path: string; group?: string }[],
|
|
57
|
+
): ArticleGroup[] {
|
|
58
|
+
const groupByPathname = new Map<string, string | undefined>()
|
|
59
|
+
for (const article of articles) {
|
|
60
|
+
groupByPathname.set(article.path, article.group)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sections: ArticleGroup[] = []
|
|
64
|
+
let current: ArticleGroup | undefined
|
|
65
|
+
|
|
66
|
+
for (const node of tree) {
|
|
67
|
+
const group = groupByPathname.get(node.path)
|
|
68
|
+
if (group) {
|
|
69
|
+
current = { name: group, nodes: [node] }
|
|
70
|
+
sections.push(current)
|
|
71
|
+
} else if (current) {
|
|
72
|
+
current.nodes.push(node)
|
|
73
|
+
} else {
|
|
74
|
+
current = { name: undefined, nodes: [node] }
|
|
75
|
+
sections.push(current)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return sections
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Depth-first flatten of an article tree into a list of paths */
|
|
83
|
+
export function flattenArticleTree(nodes: ArticleNode[]) {
|
|
84
|
+
const result: string[] = []
|
|
85
|
+
for (const node of nodes) {
|
|
86
|
+
result.push(node.path)
|
|
87
|
+
result.push(...flattenArticleTree(node.children))
|
|
88
|
+
}
|
|
89
|
+
return result
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Returns true if a changelog source string is a GitHub repo URL */
|
|
2
|
+
export function isGitHubUrl(source: string) {
|
|
3
|
+
return /^(https?:\/\/)?(www\.)?github\.com\//.test(source)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Extract `{ owner, repo }` from a GitHub URL. Returns undefined if unparseable */
|
|
7
|
+
export function parseGitHubRepo(source: string) {
|
|
8
|
+
const match = source.match(
|
|
9
|
+
/^(?:https?:\/\/)?(?:www\.)?github\.com\/([^/]+)\/([^/#?]+)/,
|
|
10
|
+
)
|
|
11
|
+
if (!match) return undefined
|
|
12
|
+
const owner = match[1]
|
|
13
|
+
const repo = match[2]?.replace(/\.git$/, "")
|
|
14
|
+
if (!owner || !repo) return undefined
|
|
15
|
+
return { owner, repo }
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
2
|
+
import { isGitHubUrl, parseGitHubRepo } from "./changelog-source.ts"
|
|
3
|
+
|
|
4
|
+
describe("isGitHubUrl", () => {
|
|
5
|
+
it("recognizes https github URLs", () => {
|
|
6
|
+
expect(isGitHubUrl("https://github.com/foo/bar")).toBe(true)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("recognizes bare github.com URLs", () => {
|
|
10
|
+
expect(isGitHubUrl("github.com/foo/bar")).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("rejects local paths", () => {
|
|
14
|
+
expect(isGitHubUrl("CHANGELOG.md")).toBe(false)
|
|
15
|
+
expect(isGitHubUrl("./CHANGELOG.md")).toBe(false)
|
|
16
|
+
expect(isGitHubUrl("docs/CHANGELOG.md")).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe("parseGitHubRepo", () => {
|
|
21
|
+
it("extracts owner and repo from https URL", () => {
|
|
22
|
+
expect(parseGitHubRepo("https://github.com/datisthq/livemark")).toEqual({
|
|
23
|
+
owner: "datisthq",
|
|
24
|
+
repo: "livemark",
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("strips .git suffix", () => {
|
|
29
|
+
expect(parseGitHubRepo("https://github.com/foo/bar.git")).toEqual({
|
|
30
|
+
owner: "foo",
|
|
31
|
+
repo: "bar",
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("returns undefined for non-github input", () => {
|
|
36
|
+
expect(parseGitHubRepo("CHANGELOG.md")).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { icons } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
function toPascalCase(name: string) {
|
|
4
|
+
return name
|
|
5
|
+
.split("-")
|
|
6
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
7
|
+
.join("")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Resolves a Lucide icon by kebab-case name from the full registry */
|
|
11
|
+
export default function DynamicIconResolver(props: {
|
|
12
|
+
name: string
|
|
13
|
+
className?: string
|
|
14
|
+
}) {
|
|
15
|
+
const key = toPascalCase(props.name)
|
|
16
|
+
if (!(key in icons)) return null
|
|
17
|
+
const Icon = icons[key as keyof typeof icons]
|
|
18
|
+
return <Icon className={props.className} />
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Suspense, lazy } from "react"
|
|
2
|
+
|
|
3
|
+
const Resolver = lazy(() => import("./dynamic-icon-resolver.tsx"))
|
|
4
|
+
|
|
5
|
+
/** Lazily loads and renders a Lucide icon by kebab-case name */
|
|
6
|
+
export function DynamicIcon(props: { name: string; className?: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<Suspense>
|
|
9
|
+
<Resolver {...props} />
|
|
10
|
+
</Suspense>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Derive the MIME type for a favicon from its file extension */
|
|
2
|
+
export function faviconType(href: string) {
|
|
3
|
+
if (href.endsWith(".svg")) return "image/svg+xml"
|
|
4
|
+
if (href.endsWith(".png")) return "image/png"
|
|
5
|
+
if (href.endsWith(".ico")) return "image/x-icon"
|
|
6
|
+
return undefined
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { dirname, relative, resolve } from "node:path"
|
|
2
|
+
|
|
3
|
+
/** Resolve a relative asset path to an absolute URL serveable by Vite */
|
|
4
|
+
export function resolveAssetPath(url: string, filePath: string, root: string) {
|
|
5
|
+
if (url.startsWith("http") || url.startsWith("/") || url.startsWith("data:"))
|
|
6
|
+
return url
|
|
7
|
+
const absolute = resolve(dirname(filePath), url)
|
|
8
|
+
return "/" + relative(root, absolute)
|
|
9
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { dirname, extname, resolve } from "node:path"
|
|
3
|
+
|
|
4
|
+
const INCLUDE_RE = /^::include\{(.+)\}$/
|
|
5
|
+
const FRONTMATTER_RE = /^---\n[\s\S]*?\n---\n?/
|
|
6
|
+
const FENCE_RE = /^(`{3,}|~{3,})/
|
|
7
|
+
|
|
8
|
+
const extToLang: Record<string, string> = {
|
|
9
|
+
".ts": "typescript",
|
|
10
|
+
".tsx": "tsx",
|
|
11
|
+
".js": "javascript",
|
|
12
|
+
".jsx": "jsx",
|
|
13
|
+
".py": "python",
|
|
14
|
+
".rs": "rust",
|
|
15
|
+
".go": "go",
|
|
16
|
+
".rb": "ruby",
|
|
17
|
+
".java": "java",
|
|
18
|
+
".kt": "kotlin",
|
|
19
|
+
".swift": "swift",
|
|
20
|
+
".c": "c",
|
|
21
|
+
".cpp": "cpp",
|
|
22
|
+
".h": "c",
|
|
23
|
+
".cs": "csharp",
|
|
24
|
+
".css": "css",
|
|
25
|
+
".scss": "scss",
|
|
26
|
+
".html": "html",
|
|
27
|
+
".json": "json",
|
|
28
|
+
".yaml": "yaml",
|
|
29
|
+
".yml": "yaml",
|
|
30
|
+
".toml": "toml",
|
|
31
|
+
".xml": "xml",
|
|
32
|
+
".sql": "sql",
|
|
33
|
+
".sh": "bash",
|
|
34
|
+
".bash": "bash",
|
|
35
|
+
".zsh": "zsh",
|
|
36
|
+
".fish": "fish",
|
|
37
|
+
".ps1": "powershell",
|
|
38
|
+
".dockerfile": "dockerfile",
|
|
39
|
+
".graphql": "graphql",
|
|
40
|
+
".proto": "protobuf",
|
|
41
|
+
".lua": "lua",
|
|
42
|
+
".php": "php",
|
|
43
|
+
".r": "r",
|
|
44
|
+
".zig": "zig",
|
|
45
|
+
".ex": "elixir",
|
|
46
|
+
".exs": "elixir",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseAttributes(raw: string): Record<string, string> {
|
|
50
|
+
const attrs: Record<string, string> = {}
|
|
51
|
+
const re = /(\w+)="([^"]+)"/g
|
|
52
|
+
let m: RegExpExecArray | null = null
|
|
53
|
+
while ((m = re.exec(raw)) !== null) {
|
|
54
|
+
attrs[m[1]!] = m[2]!
|
|
55
|
+
}
|
|
56
|
+
return attrs
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveDirective(
|
|
60
|
+
rawAttrs: string,
|
|
61
|
+
fileDir: string,
|
|
62
|
+
depth: number,
|
|
63
|
+
): string | undefined {
|
|
64
|
+
const attrs = parseAttributes(rawAttrs)
|
|
65
|
+
const file = attrs.file
|
|
66
|
+
if (!file) return undefined
|
|
67
|
+
|
|
68
|
+
const absolutePath = resolve(fileDir, file)
|
|
69
|
+
try {
|
|
70
|
+
let included = readFileSync(absolutePath, "utf-8")
|
|
71
|
+
|
|
72
|
+
const ext = extname(file).toLowerCase()
|
|
73
|
+
const lang = extToLang[ext]
|
|
74
|
+
|
|
75
|
+
if (lang && !file.endsWith(".md")) {
|
|
76
|
+
const filename = file.split("/").pop()!
|
|
77
|
+
const title = `title="${filename}"`
|
|
78
|
+
const meta = attrs.meta ? ` ${attrs.meta}` : ""
|
|
79
|
+
return (
|
|
80
|
+
"```" + lang + " " + title + meta + "\n" + included.trimEnd() + "\n```"
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
included = included.replace(FRONTMATTER_RE, "")
|
|
85
|
+
return resolveIncludes(included.trim(), absolutePath, depth + 1)
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolves ::include{file="..."} directives by replacing them with referenced file contents */
|
|
92
|
+
export function resolveIncludes(
|
|
93
|
+
content: string,
|
|
94
|
+
filePath: string,
|
|
95
|
+
depth = 0,
|
|
96
|
+
): string {
|
|
97
|
+
if (depth > 5) return content
|
|
98
|
+
|
|
99
|
+
const fileDir = dirname(filePath)
|
|
100
|
+
const lines = content.split("\n")
|
|
101
|
+
const result: string[] = []
|
|
102
|
+
let fenceMarker: string | undefined
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const fenceMatch = FENCE_RE.exec(line)
|
|
106
|
+
if (fenceMatch) {
|
|
107
|
+
if (!fenceMarker) {
|
|
108
|
+
fenceMarker = fenceMatch[1]!
|
|
109
|
+
} else if (line.startsWith(fenceMarker)) {
|
|
110
|
+
fenceMarker = undefined
|
|
111
|
+
}
|
|
112
|
+
result.push(line)
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (fenceMarker) {
|
|
117
|
+
result.push(line)
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const includeMatch = INCLUDE_RE.exec(line)
|
|
122
|
+
if (includeMatch) {
|
|
123
|
+
const resolved = resolveDirective(includeMatch[1]!, fileDir, depth)
|
|
124
|
+
result.push(resolved ?? line)
|
|
125
|
+
} else {
|
|
126
|
+
result.push(line)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result.join("\n")
|
|
131
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
4
|
+
import { temporaryDirectory } from "tempy"
|
|
5
|
+
import { resolveIncludes } from "./resolve-includes.ts"
|
|
6
|
+
|
|
7
|
+
describe("resolveIncludes", () => {
|
|
8
|
+
it("should replace include directive with file content", () => {
|
|
9
|
+
const dir = temporaryDirectory()
|
|
10
|
+
writeFileSync(join(dir, "snippet.md"), "Hello from snippet")
|
|
11
|
+
const result = resolveIncludes(
|
|
12
|
+
'::include{file="snippet.md"}',
|
|
13
|
+
join(dir, "main.md"),
|
|
14
|
+
)
|
|
15
|
+
expect(result).toBe("Hello from snippet")
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("should strip frontmatter from included files", () => {
|
|
19
|
+
const dir = temporaryDirectory()
|
|
20
|
+
writeFileSync(
|
|
21
|
+
join(dir, "snippet.md"),
|
|
22
|
+
"---\ntitle: Test\n---\nContent here",
|
|
23
|
+
)
|
|
24
|
+
const result = resolveIncludes(
|
|
25
|
+
'::include{file="snippet.md"}',
|
|
26
|
+
join(dir, "main.md"),
|
|
27
|
+
)
|
|
28
|
+
expect(result).toBe("Content here")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should preserve surrounding content", () => {
|
|
32
|
+
const dir = temporaryDirectory()
|
|
33
|
+
writeFileSync(join(dir, "snippet.md"), "Included text")
|
|
34
|
+
const result = resolveIncludes(
|
|
35
|
+
'Before\n\n::include{file="snippet.md"}\n\nAfter',
|
|
36
|
+
join(dir, "main.md"),
|
|
37
|
+
)
|
|
38
|
+
expect(result).toBe("Before\n\nIncluded text\n\nAfter")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should keep directive when file is missing", () => {
|
|
42
|
+
const result = resolveIncludes(
|
|
43
|
+
'::include{file="nonexistent.md"}',
|
|
44
|
+
"/tmp/main.md",
|
|
45
|
+
)
|
|
46
|
+
expect(result).toBe('::include{file="nonexistent.md"}')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("should resolve nested includes", () => {
|
|
50
|
+
const dir = temporaryDirectory()
|
|
51
|
+
writeFileSync(join(dir, "inner.md"), "Inner content")
|
|
52
|
+
writeFileSync(join(dir, "outer.md"), '::include{file="inner.md"}')
|
|
53
|
+
const result = resolveIncludes(
|
|
54
|
+
'::include{file="outer.md"}',
|
|
55
|
+
join(dir, "main.md"),
|
|
56
|
+
)
|
|
57
|
+
expect(result).toBe("Inner content")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("should limit recursion depth", () => {
|
|
61
|
+
const dir = temporaryDirectory()
|
|
62
|
+
writeFileSync(join(dir, "recursive.md"), '::include{file="recursive.md"}')
|
|
63
|
+
const result = resolveIncludes(
|
|
64
|
+
'::include{file="recursive.md"}',
|
|
65
|
+
join(dir, "main.md"),
|
|
66
|
+
)
|
|
67
|
+
expect(result).toBe('::include{file="recursive.md"}')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("should resolve relative paths from included file location", () => {
|
|
71
|
+
const dir = temporaryDirectory()
|
|
72
|
+
mkdirSync(join(dir, "sub"))
|
|
73
|
+
writeFileSync(join(dir, "sub", "child.md"), "Child content")
|
|
74
|
+
writeFileSync(join(dir, "parent.md"), '::include{file="sub/child.md"}')
|
|
75
|
+
const result = resolveIncludes(
|
|
76
|
+
'::include{file="parent.md"}',
|
|
77
|
+
join(dir, "main.md"),
|
|
78
|
+
)
|
|
79
|
+
expect(result).toBe("Child content")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("should handle multiple includes in one file", () => {
|
|
83
|
+
const dir = temporaryDirectory()
|
|
84
|
+
writeFileSync(join(dir, "a.md"), "Alpha")
|
|
85
|
+
writeFileSync(join(dir, "b.md"), "Beta")
|
|
86
|
+
const result = resolveIncludes(
|
|
87
|
+
'::include{file="a.md"}\n\n::include{file="b.md"}',
|
|
88
|
+
join(dir, "main.md"),
|
|
89
|
+
)
|
|
90
|
+
expect(result).toBe("Alpha\n\nBeta")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("should wrap code files in fenced blocks", () => {
|
|
94
|
+
const dir = temporaryDirectory()
|
|
95
|
+
writeFileSync(join(dir, "example.ts"), 'const x = 1\nconst y = "hello"')
|
|
96
|
+
const result = resolveIncludes(
|
|
97
|
+
'::include{file="example.ts"}',
|
|
98
|
+
join(dir, "main.md"),
|
|
99
|
+
)
|
|
100
|
+
expect(result).toBe(
|
|
101
|
+
'```typescript title="example.ts"\nconst x = 1\nconst y = "hello"\n```',
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("should pass meta to code fence", () => {
|
|
106
|
+
const dir = temporaryDirectory()
|
|
107
|
+
writeFileSync(join(dir, "app.tsx"), "export default function App() {}")
|
|
108
|
+
const result = resolveIncludes(
|
|
109
|
+
'::include{file="app.tsx" meta="{1} lineNumbers"}',
|
|
110
|
+
join(dir, "main.md"),
|
|
111
|
+
)
|
|
112
|
+
expect(result).toBe(
|
|
113
|
+
'```tsx title="app.tsx" {1} lineNumbers\nexport default function App() {}\n```',
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("should treat unknown extensions as markdown", () => {
|
|
118
|
+
const dir = temporaryDirectory()
|
|
119
|
+
writeFileSync(join(dir, "notes.txt"), "Plain text")
|
|
120
|
+
const result = resolveIncludes(
|
|
121
|
+
'::include{file="notes.txt"}',
|
|
122
|
+
join(dir, "main.md"),
|
|
123
|
+
)
|
|
124
|
+
expect(result).toBe("Plain text")
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Strip markdown syntax to extract plain searchable text */
|
|
2
|
+
export function extractSearchText(markdown: string) {
|
|
3
|
+
return markdown
|
|
4
|
+
.replace(/^---[\s\S]*?---\n*/m, "")
|
|
5
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
6
|
+
.replace(/:::\w+[\s\S]*?:::/g, "")
|
|
7
|
+
.replace(/::\w+(?:\[.*?\])?(?:\{.*?\})?/g, "")
|
|
8
|
+
.replace(/!\[.*?\]\(.*?\)/g, "")
|
|
9
|
+
.replace(/\[([^\]]*)\]\(.*?\)/g, "$1")
|
|
10
|
+
.replace(/<[^>]+>/g, "")
|
|
11
|
+
.replace(/#{1,6}\s*/g, "")
|
|
12
|
+
.replace(/[*_~`>|]/g, "")
|
|
13
|
+
.replace(/\n{2,}/g, "\n")
|
|
14
|
+
.trim()
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Section } from "../../models/section.ts"
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ICONS: Record<Section["type"], string> = {
|
|
4
|
+
article: "book-open",
|
|
5
|
+
blog: "rss",
|
|
6
|
+
changelog: "history",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Return the section's icon, falling back to a type-based default */
|
|
10
|
+
export function sectionIcon(section: Pick<Section, "icon" | "type">) {
|
|
11
|
+
return section.icon ?? DEFAULT_ICONS[section.type]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Find the section whose prefix is the longest prefix of the article path */
|
|
15
|
+
export function matchSection<S extends { prefix: string }>(
|
|
16
|
+
articlePath: string,
|
|
17
|
+
sections: S[],
|
|
18
|
+
): S | undefined {
|
|
19
|
+
let best: S | undefined
|
|
20
|
+
let bestLen = 0
|
|
21
|
+
for (const section of sections) {
|
|
22
|
+
if (
|
|
23
|
+
articlePath.startsWith(section.prefix) &&
|
|
24
|
+
section.prefix.length > bestLen
|
|
25
|
+
) {
|
|
26
|
+
best = section
|
|
27
|
+
bestLen = section.prefix.length
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return best
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Split articles into per-section buckets. Unmatched articles go to "__default__" */
|
|
34
|
+
export function partitionBySection<T extends { path: string }>(
|
|
35
|
+
articles: T[],
|
|
36
|
+
sections: { prefix: string }[],
|
|
37
|
+
): Map<string, T[]> {
|
|
38
|
+
const buckets = new Map<string, T[]>()
|
|
39
|
+
for (const article of articles) {
|
|
40
|
+
const section = matchSection(article.path, sections)
|
|
41
|
+
const key = section?.prefix ?? "__default__"
|
|
42
|
+
const bucket = buckets.get(key)
|
|
43
|
+
if (bucket) {
|
|
44
|
+
bucket.push(article)
|
|
45
|
+
} else {
|
|
46
|
+
buckets.set(key, [article])
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return buckets
|
|
50
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
2
|
+
import { matchSection, partitionBySection } from "./section.ts"
|
|
3
|
+
|
|
4
|
+
describe("matchSection", () => {
|
|
5
|
+
const sections = [
|
|
6
|
+
{ title: "Docs", prefix: "/" },
|
|
7
|
+
{ title: "API", prefix: "/api/" },
|
|
8
|
+
{ title: "API Auth", prefix: "/api/auth/" },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
it("should match the most specific prefix", () => {
|
|
12
|
+
expect(matchSection("/api/auth/login/", sections)?.title).toBe("API Auth")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("should match a less specific prefix when no deeper match", () => {
|
|
16
|
+
expect(matchSection("/api/users/", sections)?.title).toBe("API")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("should match root section as fallback", () => {
|
|
20
|
+
expect(matchSection("/getting-started/", sections)?.title).toBe("Docs")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("should return undefined when no section matches", () => {
|
|
24
|
+
const narrow = [{ title: "API", prefix: "/api/" }]
|
|
25
|
+
expect(matchSection("/getting-started/", narrow)).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("should match a single root section for all articles", () => {
|
|
29
|
+
const root = [{ title: "Docs", prefix: "/" }]
|
|
30
|
+
expect(matchSection("/anything/deep/path/", root)?.title).toBe("Docs")
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe("partitionBySection", () => {
|
|
35
|
+
const sections = [
|
|
36
|
+
{ title: "Docs", prefix: "/" },
|
|
37
|
+
{ title: "API", prefix: "/api/" },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const articles = [
|
|
41
|
+
{ path: "/getting-started/" },
|
|
42
|
+
{ path: "/guide/" },
|
|
43
|
+
{ path: "/api/users/" },
|
|
44
|
+
{ path: "/api/auth/" },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
it("should split articles into section buckets", () => {
|
|
48
|
+
const result = partitionBySection(articles, sections)
|
|
49
|
+
expect(result.get("/")?.map(a => a.path)).toEqual([
|
|
50
|
+
"/getting-started/",
|
|
51
|
+
"/guide/",
|
|
52
|
+
])
|
|
53
|
+
expect(result.get("/api/")?.map(a => a.path)).toEqual([
|
|
54
|
+
"/api/users/",
|
|
55
|
+
"/api/auth/",
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should put unmatched articles in __default__ bucket", () => {
|
|
60
|
+
const narrow = [{ title: "API", prefix: "/api/" }]
|
|
61
|
+
const result = partitionBySection(articles, narrow)
|
|
62
|
+
expect(result.get("__default__")?.map(a => a.path)).toEqual([
|
|
63
|
+
"/getting-started/",
|
|
64
|
+
"/guide/",
|
|
65
|
+
])
|
|
66
|
+
expect(result.get("/api/")?.map(a => a.path)).toEqual([
|
|
67
|
+
"/api/users/",
|
|
68
|
+
"/api/auth/",
|
|
69
|
+
])
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const STORAGE_KEY = "livemark-theme"
|
|
2
|
+
|
|
3
|
+
export type Theme = "light" | "dark"
|
|
4
|
+
|
|
5
|
+
export function detectClientTheme(): Theme {
|
|
6
|
+
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null
|
|
7
|
+
return stored || "light"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function activateTheme(theme: Theme) {
|
|
11
|
+
if (theme === "dark") {
|
|
12
|
+
globalThis.document.documentElement.classList.add("dark")
|
|
13
|
+
} else {
|
|
14
|
+
globalThis.document.documentElement.classList.remove("dark")
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getCurrentTheme(): Theme {
|
|
19
|
+
return globalThis.document?.documentElement.classList.contains("dark")
|
|
20
|
+
? "dark"
|
|
21
|
+
: "light"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setTheme(theme: Theme) {
|
|
25
|
+
activateTheme(theme)
|
|
26
|
+
localStorage.setItem(STORAGE_KEY, theme)
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import type { TocItem } from "../models/toc.ts"
|
|
3
|
+
|
|
4
|
+
/** React context that provides TOC items to inline components */
|
|
5
|
+
export const TocContext = createContext<TocItem[]>([])
|
|
6
|
+
|
|
7
|
+
/** Consume the TOC items from the nearest TocContext provider */
|
|
8
|
+
export function useTocItems() {
|
|
9
|
+
return useContext(TocContext)
|
|
10
|
+
}
|