livemark 0.0.0-dev → 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/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.d.ts +1 -0
- package/build/actions/changelog/build.unit.js +66 -0
- package/build/actions/config/define.unit.js +11 -16
- package/build/actions/config/load.js +6 -3
- package/build/actions/config/load.unit.js +3 -3
- package/build/commands/build.js +4 -3
- package/build/commands/escape.d.ts +9 -0
- package/build/commands/escape.js +56 -0
- package/build/docs/markdown/includes/example.d.ts +53 -4
- package/build/docs/markdown/includes/example.js +2 -2
- package/build/livemark.config.d.ts +53 -4
- package/build/livemark.config.js +22 -2
- package/build/models/config.d.ts +315 -8
- package/build/models/config.js +25 -5
- 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.js +4 -2
- package/build/vite.config.d.ts +1 -1
- package/build/website/assets/logo.svg +117 -0
- package/build/website/components/Article.tsx +172 -0
- package/build/website/components/BackToTop.tsx +32 -0
- package/build/website/components/Banner.tsx +2 -2
- package/build/website/components/BlogIndex.tsx +121 -0
- package/build/website/components/BlogSidebar.tsx +162 -0
- package/build/website/components/Breadcrumbs.tsx +5 -2
- package/build/website/components/Card.tsx +48 -0
- package/build/website/components/Cards.tsx +0 -58
- package/build/website/components/ChangelogIndex.tsx +111 -0
- package/build/website/components/ChangelogSidebar.tsx +120 -0
- package/build/website/components/CodeBlock.tsx +57 -40
- package/build/website/components/Column.tsx +4 -0
- package/build/website/components/Columns.tsx +0 -5
- package/build/website/components/Footer.tsx +12 -5
- package/build/website/components/InlineIcon.tsx +2 -13
- package/build/website/components/Layout.tsx +155 -185
- package/build/website/components/Mermaid.tsx +14 -33
- package/build/website/components/MermaidRenderer.tsx +37 -0
- package/build/website/components/MiniSidebar.tsx +49 -0
- package/build/website/components/PageToolbar.tsx +21 -11
- package/build/website/components/PrevNext.tsx +20 -12
- package/build/website/components/Search.tsx +28 -0
- package/build/website/components/SearchDialog.tsx +10 -10
- 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 +13 -8
- package/build/website/components/TagIndex.tsx +120 -0
- package/build/website/components/Theme.tsx +0 -1
- package/build/website/components/Toc.tsx +128 -1
- package/build/website/components/ZoomImage.tsx +15 -20
- package/build/website/content/article.ts +78 -0
- package/build/website/content/tag.ts +29 -0
- package/build/website/content-collections.ts +53 -26
- package/build/website/elements/sidebar.tsx +4 -4
- package/build/website/env.d.ts +4 -0
- package/build/website/helpers/article-icon.ts +2 -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/section.ts +50 -0
- package/build/website/helpers/section.unit.ts +71 -0
- package/build/website/helpers/toc-context.ts +1 -1
- package/build/website/helpers/toc.ts +1 -6
- package/build/website/models/article.ts +48 -33
- package/build/website/models/toc.ts +5 -0
- package/build/website/plugins/rehype-task-list-a11y.ts +33 -0
- package/build/website/plugins/remark-ansi.ts +1 -0
- package/build/website/plugins/remark-image.ts +22 -13
- package/build/website/plugins/vite-livemark.ts +91 -0
- package/build/website/routeTree.gen.ts +9 -9
- package/build/website/routes/$.tsx +74 -131
- package/build/website/routes/__root.tsx +36 -23
- package/build/website/styles/general.css +26 -210
- package/build/website/styles/markdown.css +214 -0
- package/build/website/vite.config.ts +38 -3
- package/package.json +25 -16
- package/build/website/globals.d.ts +0 -8
- package/build/website/helpers/articles.ts +0 -24
- package/build/website/public/logo.svg +0 -72
- package/build/website/routes/index.tsx +0 -13
|
@@ -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
|
+
})
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import GithubSlugger from "github-slugger"
|
|
2
|
-
|
|
3
|
-
export interface TocItem {
|
|
4
|
-
url: string
|
|
5
|
-
title: string
|
|
6
|
-
depth: number
|
|
7
|
-
}
|
|
2
|
+
import type { TocItem } from "../models/toc.ts"
|
|
8
3
|
|
|
9
4
|
const CUSTOM_ID_PATTERN = /\s*\[#([^\]]+)\]\s*$/
|
|
10
5
|
const TOC_HIDDEN_PATTERN = /\s*\[!toc\]\s*$/
|
|
@@ -1,40 +1,55 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { nonEmptyString } from "../helpers/model.ts"
|
|
3
|
+
import type { TocItem } from "./toc.ts"
|
|
4
|
+
|
|
5
|
+
/** Frontmatter schema for a markdown article. */
|
|
6
|
+
export type Article = z.infer<typeof Article>
|
|
7
|
+
export const Article = z.object({
|
|
8
|
+
content: z.string(),
|
|
9
|
+
title: nonEmptyString.optional(),
|
|
10
|
+
label: nonEmptyString.optional(),
|
|
11
|
+
description: nonEmptyString.optional(),
|
|
12
|
+
icon: nonEmptyString.optional(),
|
|
13
|
+
order: z.number().optional(),
|
|
14
|
+
path: nonEmptyString.optional(),
|
|
15
|
+
group: nonEmptyString.optional(),
|
|
16
|
+
sidebar: z.boolean().default(true),
|
|
17
|
+
toc: z.boolean().default(true),
|
|
18
|
+
image: nonEmptyString.optional(),
|
|
19
|
+
author: z.union([nonEmptyString, z.array(nonEmptyString)]).optional(),
|
|
20
|
+
date: nonEmptyString.optional(),
|
|
21
|
+
tags: z.array(nonEmptyString).optional(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** Processed article ready for page rendering (result of content-collections transform). */
|
|
25
|
+
export interface ArticleView {
|
|
26
|
+
path: string
|
|
27
|
+
content: string
|
|
28
|
+
file: string
|
|
29
|
+
mdx: string
|
|
30
|
+
tocItems: TocItem[]
|
|
31
|
+
toc?: boolean
|
|
32
|
+
lastUpdated?: string
|
|
33
|
+
sidebar?: boolean
|
|
34
|
+
image?: string
|
|
35
|
+
author?: string | string[]
|
|
36
|
+
date?: string
|
|
37
|
+
tags?: string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
1
40
|
export interface ArticleNode {
|
|
2
|
-
|
|
41
|
+
path: string
|
|
3
42
|
title: string
|
|
43
|
+
label?: string
|
|
4
44
|
icon: string
|
|
5
45
|
children: ArticleNode[]
|
|
6
46
|
}
|
|
7
47
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
nodeMap.set(article.pathname, {
|
|
16
|
-
pathname: article.pathname,
|
|
17
|
-
title: article.title,
|
|
18
|
-
icon: article.icon,
|
|
19
|
-
children: [],
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const roots: ArticleNode[] = []
|
|
24
|
-
|
|
25
|
-
for (const article of articles) {
|
|
26
|
-
const node = nodeMap.get(article.pathname)!
|
|
27
|
-
const trimmed = article.pathname.replace(/\/$/, "")
|
|
28
|
-
const lastSlash = trimmed.lastIndexOf("/")
|
|
29
|
-
const parentPath = lastSlash > 0 ? `${trimmed.slice(0, lastSlash)}/` : ""
|
|
30
|
-
const parent = parentPath ? nodeMap.get(parentPath) : undefined
|
|
31
|
-
|
|
32
|
-
if (parent) {
|
|
33
|
-
parent.children.push(node)
|
|
34
|
-
} else {
|
|
35
|
-
roots.push(node)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return roots
|
|
48
|
+
/**
|
|
49
|
+
* A contiguous section of root articles sharing the same `group` label.
|
|
50
|
+
* `name: undefined` represents the leading ungrouped section (at most one).
|
|
51
|
+
*/
|
|
52
|
+
export interface ArticleGroup {
|
|
53
|
+
name?: string
|
|
54
|
+
nodes: ArticleNode[]
|
|
40
55
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { visit } from "unist-util-visit"
|
|
2
|
+
|
|
3
|
+
interface HastNode {
|
|
4
|
+
type: string
|
|
5
|
+
tagName?: string
|
|
6
|
+
properties?: Record<string, unknown>
|
|
7
|
+
children?: HastNode[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Give each GFM task-list checkbox an accessible name so screen readers and
|
|
11
|
+
* axe-based audits don't flag them as form controls without labels. */
|
|
12
|
+
export function rehypeTaskListA11y() {
|
|
13
|
+
return (tree: HastNode) => {
|
|
14
|
+
visit(tree, "element", (node: HastNode) => {
|
|
15
|
+
if (node.tagName !== "li") return
|
|
16
|
+
const classes = node.properties?.className
|
|
17
|
+
const hasTaskClass =
|
|
18
|
+
Array.isArray(classes) && classes.includes("task-list-item")
|
|
19
|
+
if (!hasTaskClass) return
|
|
20
|
+
|
|
21
|
+
for (const child of node.children ?? []) {
|
|
22
|
+
if (child.type !== "element") continue
|
|
23
|
+
if (child.tagName !== "input") continue
|
|
24
|
+
if (child.properties?.type !== "checkbox") continue
|
|
25
|
+
child.properties = {
|
|
26
|
+
...child.properties,
|
|
27
|
+
ariaLabel: child.properties?.checked ? "Completed task" : "Task",
|
|
28
|
+
}
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -172,6 +172,7 @@ function applyParams(state: AnsiState, params: ReadonlyArray<number>) {
|
|
|
172
172
|
|
|
173
173
|
/** Convert a string containing ANSI escape sequences into HTML with inline styles */
|
|
174
174
|
export function ansiToHtml(input: string): string {
|
|
175
|
+
// oxlint-disable-next-line no-control-regex -- ANSI escape sequences start with U+001B
|
|
175
176
|
const sgr = /\u001b\[([0-9;]*)m/g
|
|
176
177
|
const state = makeDefaultState()
|
|
177
178
|
const parts: string[] = []
|
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Image, Root } from "mdast"
|
|
1
|
+
import type { Root } from "mdast"
|
|
3
2
|
import type { Plugin } from "unified"
|
|
4
3
|
import { visit } from "unist-util-visit"
|
|
4
|
+
import { resolveAssetPath } from "../helpers/resolve-asset-path.ts"
|
|
5
5
|
|
|
6
|
-
/** Remark plugin that resolves relative image paths
|
|
6
|
+
/** Remark plugin that resolves relative image paths and unwraps image-only paragraphs */
|
|
7
7
|
export const remarkImage: Plugin<
|
|
8
8
|
[{ filePath: string; root: string }],
|
|
9
9
|
Root
|
|
10
10
|
> = ({ filePath, root }) => {
|
|
11
|
-
const fileDir = dirname(filePath)
|
|
12
|
-
|
|
13
11
|
return tree => {
|
|
14
|
-
visit(tree, "image",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
visit(tree, "image", node => {
|
|
13
|
+
node.url = resolveAssetPath(node.url, filePath, root)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
visit(tree, "paragraph", (node, index, parent) => {
|
|
17
|
+
if (!parent || typeof index !== "number") return
|
|
18
|
+
const isImageOnly = node.children.every(
|
|
19
|
+
child =>
|
|
20
|
+
child.type === "image" ||
|
|
21
|
+
child.type === "imageReference" ||
|
|
22
|
+
child.type === "break" ||
|
|
23
|
+
(child.type === "text" && child.value.trim() === ""),
|
|
24
|
+
)
|
|
25
|
+
if (!isImageOnly) return
|
|
26
|
+
const images = node.children.filter(
|
|
27
|
+
child => child.type === "image" || child.type === "imageReference",
|
|
19
28
|
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
if (images.length === 0) return
|
|
30
|
+
parent.children.splice(index, 1, ...images)
|
|
31
|
+
return index + images.length
|
|
23
32
|
})
|
|
24
33
|
}
|
|
25
34
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import type { Plugin } from "vite"
|
|
4
|
+
|
|
5
|
+
/** Sub-directories of `website/` that participate in the override mechanism. */
|
|
6
|
+
export const OVERRIDE_SUBDIRS: readonly string[] = [
|
|
7
|
+
"components",
|
|
8
|
+
"elements",
|
|
9
|
+
"styles",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export interface LivemarkOptions {
|
|
13
|
+
defaultsRoot: string
|
|
14
|
+
overridesRoot: string
|
|
15
|
+
subdirs: string[]
|
|
16
|
+
configPath: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Livemark dev/build integration:
|
|
20
|
+
* - Redirects imports that land in `<defaultsRoot>/<subdir>/` to the matching
|
|
21
|
+
* user file under `<overridesRoot>/<subdir>/` when it exists.
|
|
22
|
+
* - Watches `livemark.config.ts` and `.livemark/**` so edits, additions and
|
|
23
|
+
* deletions trigger the right reload without manual restart. */
|
|
24
|
+
export function livemark(opts: LivemarkOptions): Plugin {
|
|
25
|
+
const EXT_RE = /\.(tsx|ts|jsx|js|css)(\?|$)/
|
|
26
|
+
return {
|
|
27
|
+
name: "livemark",
|
|
28
|
+
enforce: "pre",
|
|
29
|
+
async resolveId(id, importer, options) {
|
|
30
|
+
if (!importer || !EXT_RE.test(id)) return null
|
|
31
|
+
const resolved = await this.resolve(id, importer, {
|
|
32
|
+
...options,
|
|
33
|
+
skipSelf: true,
|
|
34
|
+
})
|
|
35
|
+
if (!resolved) return null
|
|
36
|
+
const queryIdx = resolved.id.indexOf("?")
|
|
37
|
+
const path =
|
|
38
|
+
queryIdx === -1 ? resolved.id : resolved.id.slice(0, queryIdx)
|
|
39
|
+
const query = queryIdx === -1 ? "" : resolved.id.slice(queryIdx)
|
|
40
|
+
for (const subdir of opts.subdirs) {
|
|
41
|
+
const defaultsSubdir = `${join(opts.defaultsRoot, subdir)}/`
|
|
42
|
+
if (!path.startsWith(defaultsSubdir)) continue
|
|
43
|
+
const rel = path.slice(defaultsSubdir.length)
|
|
44
|
+
const userPath = join(opts.overridesRoot, subdir, rel)
|
|
45
|
+
if (existsSync(userPath)) return userPath + query
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
return null
|
|
49
|
+
},
|
|
50
|
+
configureServer(server) {
|
|
51
|
+
server.watcher.add(opts.configPath)
|
|
52
|
+
server.watcher.add(join(opts.overridesRoot, "**/*"))
|
|
53
|
+
|
|
54
|
+
const overrideRoots = opts.subdirs.map(
|
|
55
|
+
subdir => `${join(opts.overridesRoot, subdir)}/`,
|
|
56
|
+
)
|
|
57
|
+
const routesRoot = `${join(opts.overridesRoot, "routes")}/`
|
|
58
|
+
|
|
59
|
+
const onChange = (file: string) => {
|
|
60
|
+
if (file === opts.configPath) {
|
|
61
|
+
server.restart()
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (file.startsWith(routesRoot)) {
|
|
66
|
+
server.restart()
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < opts.subdirs.length; i++) {
|
|
71
|
+
const root = overrideRoots[i]
|
|
72
|
+
if (!root || !file.startsWith(root)) continue
|
|
73
|
+
const rel = file.slice(root.length)
|
|
74
|
+
const subdir = opts.subdirs[i]
|
|
75
|
+
if (!subdir) continue
|
|
76
|
+
const defaultPath = join(opts.defaultsRoot, subdir, rel)
|
|
77
|
+
const mod = server.moduleGraph.getModuleById(defaultPath)
|
|
78
|
+
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
79
|
+
server.ws.send({ type: "full-reload" })
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
server.watcher.on("add", onChange)
|
|
85
|
+
server.watcher.on("unlink", onChange)
|
|
86
|
+
server.watcher.on("change", file => {
|
|
87
|
+
if (file === opts.configPath) server.restart()
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
10
|
|
|
11
11
|
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
-
import { Route as
|
|
13
|
-
import { Route as IndexRouteImport } from '
|
|
12
|
+
import { Route as splatRouteImport } from './routes/$'
|
|
13
|
+
import { Route as IndexRouteImport } from './../.livemark/routes/index'
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const splatRoute = splatRouteImport.update({
|
|
16
16
|
id: '/$',
|
|
17
17
|
path: '/$',
|
|
18
18
|
getParentRoute: () => rootRouteImport,
|
|
@@ -25,16 +25,16 @@ const IndexRoute = IndexRouteImport.update({
|
|
|
25
25
|
|
|
26
26
|
export interface FileRoutesByFullPath {
|
|
27
27
|
'/': typeof IndexRoute
|
|
28
|
-
'/$': typeof
|
|
28
|
+
'/$': typeof splatRoute
|
|
29
29
|
}
|
|
30
30
|
export interface FileRoutesByTo {
|
|
31
31
|
'/': typeof IndexRoute
|
|
32
|
-
'/$': typeof
|
|
32
|
+
'/$': typeof splatRoute
|
|
33
33
|
}
|
|
34
34
|
export interface FileRoutesById {
|
|
35
35
|
__root__: typeof rootRouteImport
|
|
36
36
|
'/': typeof IndexRoute
|
|
37
|
-
'/$': typeof
|
|
37
|
+
'/$': typeof splatRoute
|
|
38
38
|
}
|
|
39
39
|
export interface FileRouteTypes {
|
|
40
40
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
@@ -46,7 +46,7 @@ export interface FileRouteTypes {
|
|
|
46
46
|
}
|
|
47
47
|
export interface RootRouteChildren {
|
|
48
48
|
IndexRoute: typeof IndexRoute
|
|
49
|
-
|
|
49
|
+
splatRoute: typeof splatRoute
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
declare module '@tanstack/react-router' {
|
|
@@ -55,7 +55,7 @@ declare module '@tanstack/react-router' {
|
|
|
55
55
|
id: '/$'
|
|
56
56
|
path: '/$'
|
|
57
57
|
fullPath: '/$'
|
|
58
|
-
preLoaderRoute: typeof
|
|
58
|
+
preLoaderRoute: typeof splatRouteImport
|
|
59
59
|
parentRoute: typeof rootRouteImport
|
|
60
60
|
}
|
|
61
61
|
'/': {
|
|
@@ -70,7 +70,7 @@ declare module '@tanstack/react-router' {
|
|
|
70
70
|
|
|
71
71
|
const rootRouteChildren: RootRouteChildren = {
|
|
72
72
|
IndexRoute: IndexRoute,
|
|
73
|
-
|
|
73
|
+
splatRoute: splatRoute,
|
|
74
74
|
}
|
|
75
75
|
export const routeTree = rootRouteImport
|
|
76
76
|
._addFileChildren(rootRouteChildren)
|
|
@@ -1,149 +1,92 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createFileRoute, notFound } from "@tanstack/react-router"
|
|
3
|
-
import { sortedArticles } from "../helpers/articles.ts"
|
|
4
|
-
import { Callout } from "../components/Callout.tsx"
|
|
5
|
-
import { Columns, Column } from "../components/Columns.tsx"
|
|
6
|
-
import { Card, Cards } from "../components/Cards.tsx"
|
|
7
|
-
import { CodeBlock } from "../components/CodeBlock.tsx"
|
|
8
|
-
import { headingComponents } from "../components/Heading.tsx"
|
|
9
|
-
import { Calendar } from "lucide-react"
|
|
10
|
-
import { CodeTabs } from "../components/CodeTabs.tsx"
|
|
11
|
-
import { ContentTab } from "../components/ContentTab.tsx"
|
|
12
|
-
import { ContentTabs } from "../components/ContentTabs.tsx"
|
|
13
|
-
import { Details } from "../components/Details.tsx"
|
|
14
|
-
import { FileTree } from "../components/FileTree.tsx"
|
|
15
|
-
import { InlineIcon } from "../components/InlineIcon.tsx"
|
|
16
|
-
import { Abbr } from "../components/Abbr.tsx"
|
|
17
|
-
import { FootnoteRef } from "../components/FootnoteRef.tsx"
|
|
1
|
+
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
|
|
18
2
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import
|
|
30
|
-
import { InlineToc } from "../components/InlineToc.tsx"
|
|
31
|
-
import { PageToolbar } from "../components/PageToolbar.tsx"
|
|
32
|
-
import { PrevNext } from "../components/PrevNext.tsx"
|
|
33
|
-
import { Toc } from "../components/Toc.tsx"
|
|
34
|
-
import { ZoomImage } from "../components/ZoomImage.tsx"
|
|
35
|
-
import { Separator } from "../elements/separator.tsx"
|
|
36
|
-
import { TocContext } from "../helpers/toc-context.ts"
|
|
37
|
-
import {
|
|
38
|
-
SortableTable,
|
|
39
|
-
TableHeader,
|
|
40
|
-
TableBody,
|
|
41
|
-
TableRow,
|
|
42
|
-
TableHead,
|
|
43
|
-
TableCell,
|
|
44
|
-
} from "../components/SortableTable.tsx"
|
|
3
|
+
currentSection,
|
|
4
|
+
firstArticle,
|
|
5
|
+
homeArticle,
|
|
6
|
+
sortedArticles,
|
|
7
|
+
} from "../content/article.ts"
|
|
8
|
+
import { sectionTags } from "../content/tag.ts"
|
|
9
|
+
import { Article } from "../components/Article.tsx"
|
|
10
|
+
import { BlogIndex } from "../components/BlogIndex.tsx"
|
|
11
|
+
import { ChangelogIndex } from "../components/ChangelogIndex.tsx"
|
|
12
|
+
import { TagIndex } from "../components/TagIndex.tsx"
|
|
13
|
+
import markdownCss from "../styles/markdown.css?url"
|
|
45
14
|
|
|
46
15
|
export const Route = createFileRoute("/$")({
|
|
47
16
|
loader: ({ params }) => {
|
|
17
|
+
if (!params._splat) {
|
|
18
|
+
if (homeArticle) return { ...homeArticle, sidebar: homeArticle.sidebar }
|
|
19
|
+
if (!firstArticle) throw notFound()
|
|
20
|
+
throw redirect({
|
|
21
|
+
to: "/$",
|
|
22
|
+
params: { _splat: firstArticle.path.replace(/^\/|\/$/g, "") },
|
|
23
|
+
})
|
|
24
|
+
}
|
|
48
25
|
const splat = `/${params._splat}/`
|
|
49
|
-
const article = sortedArticles.find(a => a.
|
|
50
|
-
if (
|
|
51
|
-
|
|
26
|
+
const article = sortedArticles.find(a => a.path === splat)
|
|
27
|
+
if (article) return article
|
|
28
|
+
|
|
29
|
+
const section = currentSection(splat)
|
|
30
|
+
if (section?.type === "changelog" && section.prefix === splat) {
|
|
31
|
+
return {
|
|
32
|
+
changelogIndex: true as const,
|
|
33
|
+
sectionPrefix: section.prefix,
|
|
34
|
+
title: section.title,
|
|
35
|
+
sidebar: true,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (section?.type === "blog") {
|
|
39
|
+
if (section.prefix === splat) {
|
|
40
|
+
return {
|
|
41
|
+
blogIndex: true as const,
|
|
42
|
+
sectionPrefix: section.prefix,
|
|
43
|
+
title: section.title,
|
|
44
|
+
sidebar: true,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tagMatch = splat.match(
|
|
49
|
+
new RegExp(`^${section.prefix}tags/([^/]+)/$`),
|
|
50
|
+
)
|
|
51
|
+
const tag = tagMatch?.[1]
|
|
52
|
+
if (tag) {
|
|
53
|
+
const tagMap = sectionTags.get(section.prefix)
|
|
54
|
+
if (tagMap?.has(tag)) {
|
|
55
|
+
return {
|
|
56
|
+
tagPage: true as const,
|
|
57
|
+
sectionPrefix: section.prefix,
|
|
58
|
+
tag,
|
|
59
|
+
title: `Posts tagged '${tag}'`,
|
|
60
|
+
sidebar: true,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw notFound()
|
|
52
67
|
},
|
|
53
68
|
head: ({ loaderData }) => ({
|
|
54
69
|
meta: [
|
|
55
70
|
...(loaderData ? [{ title: loaderData.title }] : []),
|
|
56
|
-
...(loaderData
|
|
71
|
+
...("description" in loaderData && loaderData.description
|
|
57
72
|
? [{ name: "description", content: loaderData.description }]
|
|
58
73
|
: []),
|
|
59
74
|
],
|
|
75
|
+
links: [{ rel: "stylesheet", href: markdownCss }],
|
|
60
76
|
}),
|
|
61
77
|
component: Component,
|
|
62
78
|
})
|
|
63
79
|
|
|
64
80
|
function Component() {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
</h1>
|
|
77
|
-
{lastUpdated && (
|
|
78
|
-
<p className="flex items-center gap-1.5 text-sm text-muted-foreground !-mt-4 !mb-6">
|
|
79
|
-
<Calendar className="size-3.5" />
|
|
80
|
-
Last updated {lastUpdated}
|
|
81
|
-
</p>
|
|
82
|
-
)}
|
|
83
|
-
</>
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<TocContext.Provider value={article.toc}>
|
|
88
|
-
<div className="flex flex-1 gap-10 p-6 md:p-10">
|
|
89
|
-
<div className="flex-1 min-w-0 mx-auto max-w-3xl">
|
|
90
|
-
<div className="prose dark:prose-invert max-w-none">
|
|
91
|
-
<MDXContent
|
|
92
|
-
code={article.mdx}
|
|
93
|
-
components={{
|
|
94
|
-
hr: Separator,
|
|
95
|
-
img: ZoomImage,
|
|
96
|
-
pre: CodeBlock,
|
|
97
|
-
table: SortableTable,
|
|
98
|
-
thead: TableHeader,
|
|
99
|
-
tbody: TableBody,
|
|
100
|
-
tr: TableRow,
|
|
101
|
-
th: TableHead,
|
|
102
|
-
td: TableCell,
|
|
103
|
-
a: FootnoteRef,
|
|
104
|
-
Abbr,
|
|
105
|
-
AnsiCode,
|
|
106
|
-
Callout,
|
|
107
|
-
Card,
|
|
108
|
-
Cards,
|
|
109
|
-
Column,
|
|
110
|
-
Columns,
|
|
111
|
-
CodeTabs,
|
|
112
|
-
ContentTab,
|
|
113
|
-
ContentTabs,
|
|
114
|
-
DefinitionDetail,
|
|
115
|
-
DefinitionList,
|
|
116
|
-
DefinitionTerm,
|
|
117
|
-
Details,
|
|
118
|
-
FileTree,
|
|
119
|
-
InlineBadge,
|
|
120
|
-
InlineIcon,
|
|
121
|
-
InlineToc,
|
|
122
|
-
LinkButton,
|
|
123
|
-
Mermaid,
|
|
124
|
-
PackageTabs,
|
|
125
|
-
SoundCloud,
|
|
126
|
-
YouTube,
|
|
127
|
-
...headingComponents,
|
|
128
|
-
h1: H1,
|
|
129
|
-
}}
|
|
130
|
-
/>
|
|
131
|
-
</div>
|
|
132
|
-
<PrevNext pathname={article.pathname} />
|
|
133
|
-
</div>
|
|
134
|
-
<Toc items={article.toc}>
|
|
135
|
-
<PageToolbar filePath={article.filePath} content={article.content} />
|
|
136
|
-
</Toc>
|
|
137
|
-
</div>
|
|
138
|
-
</TocContext.Provider>
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function formatDate(iso?: string) {
|
|
143
|
-
if (!iso) return undefined
|
|
144
|
-
return new Date(iso).toLocaleDateString("en-US", {
|
|
145
|
-
year: "numeric",
|
|
146
|
-
month: "short",
|
|
147
|
-
day: "numeric",
|
|
148
|
-
})
|
|
81
|
+
const data = Route.useLoaderData()
|
|
82
|
+
if ("blogIndex" in data) {
|
|
83
|
+
return <BlogIndex sectionPrefix={data.sectionPrefix} />
|
|
84
|
+
}
|
|
85
|
+
if ("changelogIndex" in data) {
|
|
86
|
+
return <ChangelogIndex sectionPrefix={data.sectionPrefix} />
|
|
87
|
+
}
|
|
88
|
+
if ("tagPage" in data) {
|
|
89
|
+
return <TagIndex sectionPrefix={data.sectionPrefix} tag={data.tag} />
|
|
90
|
+
}
|
|
91
|
+
return <Article article={data} />
|
|
149
92
|
}
|