veslx 0.1.14 → 0.1.15
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 +262 -55
- package/bin/lib/build.ts +65 -13
- package/bin/lib/import-config.ts +10 -9
- package/bin/lib/init.ts +21 -22
- package/bin/lib/serve.ts +66 -12
- package/bin/veslx.ts +2 -2
- package/dist/client/App.js +3 -9
- package/dist/client/App.js.map +1 -1
- package/dist/client/components/front-matter.js +11 -25
- package/dist/client/components/front-matter.js.map +1 -1
- package/dist/client/components/gallery/components/figure-caption.js +6 -4
- package/dist/client/components/gallery/components/figure-caption.js.map +1 -1
- package/dist/client/components/gallery/components/figure-header.js +3 -3
- package/dist/client/components/gallery/components/figure-header.js.map +1 -1
- package/dist/client/components/gallery/components/lightbox.js +13 -13
- package/dist/client/components/gallery/components/lightbox.js.map +1 -1
- package/dist/client/components/gallery/components/loading-image.js +11 -10
- package/dist/client/components/gallery/components/loading-image.js.map +1 -1
- package/dist/client/components/gallery/hooks/use-gallery-images.js +31 -7
- package/dist/client/components/gallery/hooks/use-gallery-images.js.map +1 -1
- package/dist/client/components/gallery/index.js +22 -15
- package/dist/client/components/gallery/index.js.map +1 -1
- package/dist/client/components/header.js +5 -3
- package/dist/client/components/header.js.map +1 -1
- package/dist/client/components/mdx-components.js +42 -8
- package/dist/client/components/mdx-components.js.map +1 -1
- package/dist/client/components/post-list.js +97 -90
- package/dist/client/components/post-list.js.map +1 -1
- package/dist/client/components/running-bar.js +1 -1
- package/dist/client/components/running-bar.js.map +1 -1
- package/dist/client/components/slide.js +18 -0
- package/dist/client/components/slide.js.map +1 -0
- package/dist/client/components/slides-renderer.js +7 -71
- package/dist/client/components/slides-renderer.js.map +1 -1
- package/dist/client/hooks/use-mdx-content.js +55 -9
- package/dist/client/hooks/use-mdx-content.js.map +1 -1
- package/dist/client/main.js +1 -0
- package/dist/client/main.js.map +1 -1
- package/dist/client/pages/content-router.js +19 -0
- package/dist/client/pages/content-router.js.map +1 -0
- package/dist/client/pages/home.js +11 -7
- package/dist/client/pages/home.js.map +1 -1
- package/dist/client/pages/post.js +8 -20
- package/dist/client/pages/post.js.map +1 -1
- package/dist/client/pages/slides.js +62 -86
- package/dist/client/pages/slides.js.map +1 -1
- package/dist/client/plugin/src/client.js +58 -96
- package/dist/client/plugin/src/client.js.map +1 -1
- package/dist/client/plugin/src/directory-tree.js +111 -0
- package/dist/client/plugin/src/directory-tree.js.map +1 -0
- package/index.html +1 -1
- package/package.json +27 -15
- package/plugin/src/client.tsx +64 -116
- package/plugin/src/directory-tree.ts +171 -0
- package/plugin/src/lib.ts +6 -249
- package/plugin/src/plugin.ts +93 -50
- package/plugin/src/remark-slides.ts +100 -0
- package/plugin/src/types.ts +22 -0
- package/src/App.tsx +3 -6
- package/src/components/front-matter.tsx +14 -29
- package/src/components/gallery/components/figure-caption.tsx +15 -7
- package/src/components/gallery/components/figure-header.tsx +3 -3
- package/src/components/gallery/components/lightbox.tsx +15 -13
- package/src/components/gallery/components/loading-image.tsx +15 -12
- package/src/components/gallery/hooks/use-gallery-images.ts +51 -10
- package/src/components/gallery/index.tsx +32 -26
- package/src/components/header.tsx +14 -9
- package/src/components/mdx-components.tsx +61 -8
- package/src/components/post-list.tsx +149 -115
- package/src/components/running-bar.tsx +1 -1
- package/src/components/slide.tsx +22 -5
- package/src/components/slides-renderer.tsx +7 -115
- package/src/components/welcome.tsx +11 -14
- package/src/hooks/use-mdx-content.ts +94 -9
- package/src/index.css +159 -0
- package/src/main.tsx +1 -0
- package/src/pages/content-router.tsx +27 -0
- package/src/pages/home.tsx +16 -2
- package/src/pages/post.tsx +10 -13
- package/src/pages/slides.tsx +75 -88
- package/src/vite-env.d.ts +7 -17
- package/vite.config.ts +25 -6
|
@@ -1,42 +1,48 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const nextDir = currentDir.children.find(
|
|
19
|
-
(child) => child.type === "directory" && child.name === part
|
|
1
|
+
import { useState, useMemo, useEffect } from "react";
|
|
2
|
+
import { buildDirectoryTree, navigateToPath } from "./directory-tree.js";
|
|
3
|
+
import { frontmatters, files } from "virtual:content-modules";
|
|
4
|
+
function findReadme(directory) {
|
|
5
|
+
const indexFiles = [
|
|
6
|
+
"index.mdx",
|
|
7
|
+
"index.md",
|
|
8
|
+
"README.mdx",
|
|
9
|
+
"Readme.mdx",
|
|
10
|
+
"readme.mdx",
|
|
11
|
+
"README.md",
|
|
12
|
+
"Readme.md",
|
|
13
|
+
"readme.md"
|
|
14
|
+
];
|
|
15
|
+
for (const filename of indexFiles) {
|
|
16
|
+
const found = directory.children.find(
|
|
17
|
+
(child) => child.type === "file" && child.name === filename
|
|
20
18
|
);
|
|
21
|
-
if (
|
|
22
|
-
throw new Error(`Path not found: ${path}`);
|
|
23
|
-
}
|
|
24
|
-
currentDir = nextDir;
|
|
19
|
+
if (found) return found;
|
|
25
20
|
}
|
|
26
|
-
return
|
|
21
|
+
return null;
|
|
27
22
|
}
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
function findMdxFiles(directory) {
|
|
24
|
+
const indexFiles = [
|
|
25
|
+
"index.mdx",
|
|
26
|
+
"index.md",
|
|
27
|
+
"README.mdx",
|
|
28
|
+
"Readme.mdx",
|
|
29
|
+
"readme.mdx",
|
|
30
|
+
"README.md",
|
|
31
|
+
"Readme.md",
|
|
32
|
+
"readme.md"
|
|
33
|
+
];
|
|
34
|
+
const slideFiles = [
|
|
35
|
+
"SLIDES.mdx",
|
|
36
|
+
"Slides.mdx",
|
|
37
|
+
"slides.mdx",
|
|
38
|
+
"SLIDES.md",
|
|
39
|
+
"Slides.md",
|
|
40
|
+
"slides.md"
|
|
41
|
+
];
|
|
42
|
+
const excludeFiles = [...indexFiles, ...slideFiles];
|
|
43
|
+
return directory.children.filter(
|
|
44
|
+
(child) => child.type === "file" && (child.name.endsWith(".mdx") || child.name.endsWith(".md")) && !excludeFiles.includes(child.name) && !child.name.endsWith(".slides.mdx") && !child.name.endsWith(".slides.md")
|
|
38
45
|
);
|
|
39
|
-
return readme || null;
|
|
40
46
|
}
|
|
41
47
|
function findSlides(directory) {
|
|
42
48
|
const readme = directory.children.find(
|
|
@@ -51,70 +57,25 @@ function findSlides(directory) {
|
|
|
51
57
|
);
|
|
52
58
|
return readme || null;
|
|
53
59
|
}
|
|
60
|
+
const directoryTree = buildDirectoryTree(Object.keys(files), frontmatters);
|
|
54
61
|
function useDirectory(path = ".") {
|
|
55
|
-
const [directory, setDirectory] = useState(null);
|
|
56
|
-
const [file, setFile] = useState(null);
|
|
57
|
-
const [loading, setLoading] = useState(true);
|
|
58
62
|
const [error, setError] = useState(null);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
setError({ type: "config_not_found", message: ".veslx.json not found" });
|
|
68
|
-
} else {
|
|
69
|
-
setError({ type: "fetch_error", message: `Failed to fetch: ${response.status} ${response.statusText}` });
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
let json;
|
|
74
|
-
try {
|
|
75
|
-
json = await response.json();
|
|
76
|
-
} catch {
|
|
77
|
-
setError({ type: "parse_error", message: "Failed to parse .veslx.json" });
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
let parsed;
|
|
81
|
-
try {
|
|
82
|
-
parsed = await parsePath(json, path);
|
|
83
|
-
} catch {
|
|
84
|
-
setError({ type: "path_not_found", message: `Path not found: ${path}`, status: 404 });
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
parsed.directory.children.sort((a, b) => {
|
|
88
|
-
let aDate, bDate;
|
|
89
|
-
if (a.children) {
|
|
90
|
-
const readme = findReadme(a);
|
|
91
|
-
if (readme && readme.frontmatter && readme.frontmatter.date) {
|
|
92
|
-
aDate = new Date(readme.frontmatter.date);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (b.children) {
|
|
96
|
-
const readme = findReadme(b);
|
|
97
|
-
if (readme && readme.frontmatter && readme.frontmatter.date) {
|
|
98
|
-
bDate = new Date(readme.frontmatter.date);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (aDate && bDate) {
|
|
102
|
-
return bDate.getTime() - aDate.getTime();
|
|
103
|
-
}
|
|
104
|
-
return 0;
|
|
105
|
-
});
|
|
106
|
-
setDirectory(parsed.directory);
|
|
107
|
-
setFile(parsed.file);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
setError({ type: "fetch_error", message: err.message || "Unknown error" });
|
|
110
|
-
} finally {
|
|
111
|
-
setLoading(false);
|
|
112
|
-
}
|
|
113
|
-
})();
|
|
114
|
-
return () => {
|
|
115
|
-
};
|
|
63
|
+
const result = useMemo(() => {
|
|
64
|
+
try {
|
|
65
|
+
const { directory, file } = navigateToPath(directoryTree, path);
|
|
66
|
+
return { directory, file };
|
|
67
|
+
} catch {
|
|
68
|
+
setError({ type: "path_not_found", message: `Path not found: ${path}`, status: 404 });
|
|
69
|
+
return { directory: null, file: null };
|
|
70
|
+
}
|
|
116
71
|
}, [path]);
|
|
117
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
directory: result.directory,
|
|
74
|
+
file: result.file,
|
|
75
|
+
loading: false,
|
|
76
|
+
// No async loading needed
|
|
77
|
+
error
|
|
78
|
+
};
|
|
118
79
|
}
|
|
119
80
|
function useFileContent(path) {
|
|
120
81
|
const [blob, setBlob] = useState(null);
|
|
@@ -176,6 +137,7 @@ function isSimulationRunning() {
|
|
|
176
137
|
return running;
|
|
177
138
|
}
|
|
178
139
|
export {
|
|
140
|
+
findMdxFiles,
|
|
179
141
|
findReadme,
|
|
180
142
|
findSlides,
|
|
181
143
|
isSimulationRunning,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sources":["../../../../plugin/src/client.tsx"],"sourcesContent":["import { useState, useEffect } from \"react\";\nimport { DirectoryEntry, FileEntry } from \"./lib\";\n\nasync function parsePath(directory: DirectoryEntry, path: string): Promise<{ directory: DirectoryEntry; file: FileEntry | null }> {\n const parts = path === \".\" ? [] : path.split(\"/\").filter(Boolean);\n\n let file = null;\n let currentDir = directory;\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n const isLastPart = i === parts.length - 1;\n\n // Check if this part matches a file (only on last part)\n if (isLastPart) {\n const matchedFile = currentDir.children.find(\n (child) => child.type === \"file\" && child.name === part\n ) as FileEntry | undefined;\n\n if (matchedFile) {\n file = matchedFile;\n break;\n }\n }\n\n // Otherwise, look for a directory\n const nextDir = currentDir.children.find(\n (child) => child.type === \"directory\" && child.name === part\n ) as DirectoryEntry | undefined;\n\n if (!nextDir) {\n throw new Error(`Path not found: ${path}`);\n }\n\n currentDir = nextDir;\n }\n\n return { directory: currentDir, file };\n}\n\nexport function findReadme(directory: DirectoryEntry): FileEntry | null {\n const readme = directory.children.find((child) => \n child.type === \"file\" && \n [\n \"README.md\", \"Readme.md\", \"readme.md\",\n \"README.mdx\", \"Readme.mdx\", \"readme.mdx\"\n ].includes(child.name)\n ) as FileEntry | undefined;\n\n return readme || null;\n}\n\nexport function findSlides(directory: DirectoryEntry): FileEntry | null {\n const readme = directory.children.find((child) =>\n child.type === \"file\" &&\n [\n \"SLIDES.md\", \"Slides.md\", \"slides.md\",\n \"SLIDES.mdx\", \"Slides.mdx\", \"slides.mdx\"\n ].includes(child.name)\n ) as FileEntry | undefined;\n\n return readme || null;\n}\n\n\nexport type DirectoryError =\n | { type: 'config_not_found'; message: string }\n | { type: 'path_not_found'; message: string; status: 404 }\n | { type: 'fetch_error'; message: string }\n | { type: 'parse_error'; message: string };\n\nexport function useDirectory(path: string = \".\") {\n const [directory, setDirectory] = useState<DirectoryEntry | null>(null);\n const [file, setFile] = useState<FileEntry | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<DirectoryError | null>(null);\n\n useEffect(() => {\n setLoading(true);\n setError(null);\n\n (async () => {\n try {\n const response = await fetch(`/raw/.veslx.json`);\n\n if (!response.ok) {\n if (response.status === 404) {\n setError({ type: 'config_not_found', message: '.veslx.json not found' });\n } else {\n setError({ type: 'fetch_error', message: `Failed to fetch: ${response.status} ${response.statusText}` });\n }\n return;\n }\n\n let json;\n try {\n json = await response.json();\n } catch {\n setError({ type: 'parse_error', message: 'Failed to parse .veslx.json' });\n return;\n }\n\n let parsed: { directory: DirectoryEntry; file: FileEntry | null };\n try {\n parsed = await parsePath(json, path);\n } catch {\n setError({ type: 'path_not_found', message: `Path not found: ${path}`, status: 404 });\n return;\n }\n\n parsed.directory.children.sort((a: any, b: any) => {\n let aDate, bDate;\n if (a.children) {\n const readme = findReadme(a);\n if (readme && readme.frontmatter && readme.frontmatter.date) {\n aDate = new Date(readme.frontmatter.date as string | number | Date)\n }\n }\n if (b.children) {\n const readme = findReadme(b);\n if (readme && readme.frontmatter && readme.frontmatter.date) {\n bDate = new Date(readme.frontmatter.date as string | number | Date)\n }\n }\n if (aDate && bDate) {\n return bDate.getTime() - aDate.getTime()\n }\n return 0;\n });\n\n setDirectory(parsed.directory);\n setFile(parsed.file);\n } catch (err: any) {\n setError({ type: 'fetch_error', message: err.message || 'Unknown error' });\n } finally {\n setLoading(false);\n }\n })();\n\n return () => {};\n }, [path]);\n\n return { directory, file, loading, error };\n}\n\nexport function useFileContent(path: string) {\n const [blob, setBlob] = useState<Blob | null>(null);\n const [content, setContent] = useState<string | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const controller = new AbortController();\n setLoading(true);\n setError(null);\n\n (async () => {\n try {\n const res = await fetch(`/raw/${path}`, {\n signal: controller.signal,\n });\n\n if (!res.ok) {\n throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);\n }\n\n const fetchedBlob = await res.blob();\n setBlob(fetchedBlob);\n\n // Try to read as text - some binary files may fail\n try {\n const text = await fetchedBlob.text();\n setContent(text);\n } catch {\n // Binary file - text content not available\n setContent(null);\n }\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n return; // Ignore abort errors\n }\n setError(err instanceof Error ? err.message : 'Unknown error');\n } finally {\n setLoading(false);\n }\n })();\n\n return () => controller.abort();\n }, [path]);\n\n return { blob, content, loading, error };\n}\n\nexport function isSimulationRunning() {\n const [running, setRunning] = useState<boolean>(false);\n\n useEffect(() => {\n let interval: ReturnType<typeof setInterval>;\n\n const fetchStatus = async () => {\n const response = await fetch(`/raw/.running`);\n\n // this is an elaborate workaround to stop devtools logging errors on 404s\n const text = await response.text()\n if (text === \"\") {\n setRunning(true);\n } else {\n setRunning(false);\n }\n };\n\n // Initial fetch\n fetchStatus();\n\n // Poll every second\n interval = setInterval(fetchStatus, 1000);\n\n return () => {\n clearInterval(interval);\n };\n }, []);\n\n return running;\n}"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","sources":["../../../../plugin/src/client.tsx"],"sourcesContent":["import { useState, useEffect, useMemo } from \"react\";\nimport { DirectoryEntry, FileEntry } from \"./lib\";\nimport { buildDirectoryTree, navigateToPath } from \"./directory-tree\";\n// @ts-ignore - virtual module\nimport { files, frontmatters } from \"virtual:content-modules\";\n\n/**\n * Find the main content file for a directory.\n * Supports (in order of preference):\n * - index.mdx / index.md (modern convention)\n * - README.mdx / README.md (traditional convention)\n */\nexport function findReadme(directory: DirectoryEntry): FileEntry | null {\n const indexFiles = [\n \"index.mdx\", \"index.md\",\n \"README.mdx\", \"Readme.mdx\", \"readme.mdx\",\n \"README.md\", \"Readme.md\", \"readme.md\",\n ];\n\n for (const filename of indexFiles) {\n const found = directory.children.find((child) =>\n child.type === \"file\" && child.name === filename\n ) as FileEntry | undefined;\n if (found) return found;\n }\n\n return null;\n}\n\n/**\n * Find all MDX files in a directory (excluding index/README and slides)\n */\nexport function findMdxFiles(directory: DirectoryEntry): FileEntry[] {\n const indexFiles = [\n \"index.mdx\", \"index.md\",\n \"README.mdx\", \"Readme.mdx\", \"readme.mdx\",\n \"README.md\", \"Readme.md\", \"readme.md\",\n ];\n const slideFiles = [\n \"SLIDES.mdx\", \"Slides.mdx\", \"slides.mdx\",\n \"SLIDES.md\", \"Slides.md\", \"slides.md\",\n ];\n const excludeFiles = [...indexFiles, ...slideFiles];\n\n return directory.children.filter((child): child is FileEntry =>\n child.type === \"file\" &&\n (child.name.endsWith('.mdx') || child.name.endsWith('.md')) &&\n !excludeFiles.includes(child.name) &&\n !child.name.endsWith('.slides.mdx') &&\n !child.name.endsWith('.slides.md')\n );\n}\n\nexport function findSlides(directory: DirectoryEntry): FileEntry | null {\n const readme = directory.children.find((child) =>\n child.type === \"file\" &&\n [\n \"SLIDES.md\", \"Slides.md\", \"slides.md\",\n \"SLIDES.mdx\", \"Slides.mdx\", \"slides.mdx\"\n ].includes(child.name)\n ) as FileEntry | undefined;\n\n return readme || null;\n}\n\n\nexport type DirectoryError =\n | { type: 'path_not_found'; message: string; status: 404 };\n\n// Build directory tree once from glob keys, with frontmatter metadata\nconst directoryTree = buildDirectoryTree(Object.keys(files), frontmatters as Record<string, FileEntry['frontmatter']>);\n\nexport function useDirectory(path: string = \".\") {\n const [error, setError] = useState<DirectoryError | null>(null);\n\n const result = useMemo(() => {\n try {\n const { directory, file } = navigateToPath(directoryTree, path);\n return { directory, file };\n } catch {\n setError({ type: 'path_not_found', message: `Path not found: ${path}`, status: 404 });\n return { directory: null, file: null };\n }\n }, [path]);\n\n return {\n directory: result.directory,\n file: result.file,\n loading: false, // No async loading needed\n error\n };\n}\n\nexport function useFileContent(path: string) {\n const [blob, setBlob] = useState<Blob | null>(null);\n const [content, setContent] = useState<string | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const controller = new AbortController();\n setLoading(true);\n setError(null);\n\n (async () => {\n try {\n const res = await fetch(`/raw/${path}`, {\n signal: controller.signal,\n });\n\n if (!res.ok) {\n throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);\n }\n\n const fetchedBlob = await res.blob();\n setBlob(fetchedBlob);\n\n // Try to read as text - some binary files may fail\n try {\n const text = await fetchedBlob.text();\n setContent(text);\n } catch {\n // Binary file - text content not available\n setContent(null);\n }\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n return; // Ignore abort errors\n }\n setError(err instanceof Error ? err.message : 'Unknown error');\n } finally {\n setLoading(false);\n }\n })();\n\n return () => controller.abort();\n }, [path]);\n\n return { blob, content, loading, error };\n}\n\nexport function isSimulationRunning() {\n const [running, setRunning] = useState<boolean>(false);\n\n useEffect(() => {\n let interval: ReturnType<typeof setInterval>;\n\n const fetchStatus = async () => {\n const response = await fetch(`/raw/.running`);\n\n // this is an elaborate workaround to stop devtools logging errors on 404s\n const text = await response.text()\n if (text === \"\") {\n setRunning(true);\n } else {\n setRunning(false);\n }\n };\n\n // Initial fetch\n fetchStatus();\n\n // Poll every second\n interval = setInterval(fetchStatus, 1000);\n\n return () => {\n clearInterval(interval);\n };\n }, []);\n\n return running;\n}"],"names":[],"mappings":";;;AAYO,SAAS,WAAW,WAA6C;AACtE,QAAM,aAAa;AAAA,IACjB;AAAA,IAAa;AAAA,IACb;AAAA,IAAc;AAAA,IAAc;AAAA,IAC5B;AAAA,IAAa;AAAA,IAAa;AAAA,EAAA;AAG5B,aAAW,YAAY,YAAY;AACjC,UAAM,QAAQ,UAAU,SAAS;AAAA,MAAK,CAAC,UACrC,MAAM,SAAS,UAAU,MAAM,SAAS;AAAA,IAAA;AAE1C,QAAI,MAAO,QAAO;AAAA,EACpB;AAEA,SAAO;AACT;AAKO,SAAS,aAAa,WAAwC;AACnE,QAAM,aAAa;AAAA,IACjB;AAAA,IAAa;AAAA,IACb;AAAA,IAAc;AAAA,IAAc;AAAA,IAC5B;AAAA,IAAa;AAAA,IAAa;AAAA,EAAA;AAE5B,QAAM,aAAa;AAAA,IACjB;AAAA,IAAc;AAAA,IAAc;AAAA,IAC5B;AAAA,IAAa;AAAA,IAAa;AAAA,EAAA;AAE5B,QAAM,eAAe,CAAC,GAAG,YAAY,GAAG,UAAU;AAElD,SAAO,UAAU,SAAS;AAAA,IAAO,CAAC,UAChC,MAAM,SAAS,WACd,MAAM,KAAK,SAAS,MAAM,KAAK,MAAM,KAAK,SAAS,KAAK,MACzD,CAAC,aAAa,SAAS,MAAM,IAAI,KACjC,CAAC,MAAM,KAAK,SAAS,aAAa,KAClC,CAAC,MAAM,KAAK,SAAS,YAAY;AAAA,EAAA;AAErC;AAEO,SAAS,WAAW,WAA6C;AACtE,QAAM,SAAS,UAAU,SAAS;AAAA,IAAK,CAAC,UACtC,MAAM,SAAS,UACf;AAAA,MACE;AAAA,MAAa;AAAA,MAAa;AAAA,MAC1B;AAAA,MAAc;AAAA,MAAc;AAAA,IAAA,EAC5B,SAAS,MAAM,IAAI;AAAA,EAAA;AAGvB,SAAO,UAAU;AACnB;AAOA,MAAM,gBAAgB,mBAAmB,OAAO,KAAK,KAAK,GAAG,YAAwD;AAE9G,SAAS,aAAa,OAAe,KAAK;AAC/C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAgC,IAAI;AAE9D,QAAM,SAAS,QAAQ,MAAM;AAC3B,QAAI;AACF,YAAM,EAAE,WAAW,KAAA,IAAS,eAAe,eAAe,IAAI;AAC9D,aAAO,EAAE,WAAW,KAAA;AAAA,IACtB,QAAQ;AACN,eAAS,EAAE,MAAM,kBAAkB,SAAS,mBAAmB,IAAI,IAAI,QAAQ,KAAK;AACpF,aAAO,EAAE,WAAW,MAAM,MAAM,KAAA;AAAA,IAClC;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,SAAO;AAAA,IACL,WAAW,OAAO;AAAA,IAClB,MAAM,OAAO;AAAA,IACb,SAAS;AAAA;AAAA,IACT;AAAA,EAAA;AAEJ;AAEO,SAAS,eAAe,MAAc;AAC3C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,IAAI;AAClD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,IAAI;AAC1D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AAEtD,YAAU,MAAM;AACd,UAAM,aAAa,IAAI,gBAAA;AACvB,eAAW,IAAI;AACf,aAAS,IAAI;AAEb,KAAC,YAAY;AACX,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,QAAQ,IAAI,IAAI;AAAA,UACtC,QAAQ,WAAW;AAAA,QAAA,CACpB;AAED,YAAI,CAAC,IAAI,IAAI;AACX,gBAAM,IAAI,MAAM,oBAAoB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,QACpE;AAEA,cAAM,cAAc,MAAM,IAAI,KAAA;AAC9B,gBAAQ,WAAW;AAGnB,YAAI;AACF,gBAAM,OAAO,MAAM,YAAY,KAAA;AAC/B,qBAAW,IAAI;AAAA,QACjB,QAAQ;AAEN,qBAAW,IAAI;AAAA,QACjB;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD;AAAA,QACF;AACA,iBAAS,eAAe,QAAQ,IAAI,UAAU,eAAe;AAAA,MAC/D,UAAA;AACE,mBAAW,KAAK;AAAA,MAClB;AAAA,IACF,GAAA;AAEA,WAAO,MAAM,WAAW,MAAA;AAAA,EAC1B,GAAG,CAAC,IAAI,CAAC;AAET,SAAO,EAAE,MAAM,SAAS,SAAS,MAAA;AACnC;AAEO,SAAS,sBAAsB;AACpC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAkB,KAAK;AAErD,YAAU,MAAM;AACd,QAAI;AAEJ,UAAM,cAAc,YAAY;AAC9B,YAAM,WAAW,MAAM,MAAM,eAAe;AAG5C,YAAM,OAAO,MAAM,SAAS,KAAA;AAC5B,UAAI,SAAS,IAAI;AACf,mBAAW,IAAI;AAAA,MACjB,OAAO;AACL,mBAAW,KAAK;AAAA,MAClB;AAAA,IACF;AAGA,gBAAA;AAGA,eAAW,YAAY,aAAa,GAAI;AAExC,WAAO,MAAM;AACX,oBAAc,QAAQ;AAAA,IACxB;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO;AACT;"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
function findCommonPrefix(paths) {
|
|
2
|
+
if (paths.length === 0) return "";
|
|
3
|
+
const dirPaths = paths.map((p) => {
|
|
4
|
+
const parts = p.split("/").filter(Boolean);
|
|
5
|
+
return parts.slice(0, -1);
|
|
6
|
+
});
|
|
7
|
+
if (dirPaths.length === 0 || dirPaths[0].length === 0) return "";
|
|
8
|
+
const firstDirParts = dirPaths[0];
|
|
9
|
+
let commonLength = 0;
|
|
10
|
+
for (let i = 0; i < firstDirParts.length; i++) {
|
|
11
|
+
const segment = firstDirParts[i];
|
|
12
|
+
const allMatch = dirPaths.every((parts) => parts[i] === segment);
|
|
13
|
+
if (allMatch) {
|
|
14
|
+
commonLength = i + 1;
|
|
15
|
+
} else {
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (commonLength > 0) {
|
|
20
|
+
return "/" + firstDirParts.slice(0, commonLength).join("/");
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
function buildDirectoryTree(globKeys, frontmatters) {
|
|
25
|
+
const root = {
|
|
26
|
+
type: "directory",
|
|
27
|
+
name: ".",
|
|
28
|
+
path: ".",
|
|
29
|
+
children: []
|
|
30
|
+
};
|
|
31
|
+
if (globKeys.length === 0) return root;
|
|
32
|
+
const commonPrefix = findCommonPrefix(globKeys);
|
|
33
|
+
for (const key of globKeys) {
|
|
34
|
+
let relativePath = key;
|
|
35
|
+
if (commonPrefix && key.startsWith(commonPrefix)) {
|
|
36
|
+
relativePath = key.slice(commonPrefix.length);
|
|
37
|
+
}
|
|
38
|
+
if (relativePath.startsWith("/")) {
|
|
39
|
+
relativePath = relativePath.slice(1);
|
|
40
|
+
}
|
|
41
|
+
if (relativePath.split("/").some((part) => part.startsWith("."))) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const parts = relativePath.split("/").filter(Boolean);
|
|
45
|
+
if (parts.length === 0) continue;
|
|
46
|
+
let current = root;
|
|
47
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
48
|
+
const dirName = parts[i];
|
|
49
|
+
let dir = current.children.find(
|
|
50
|
+
(c) => c.type === "directory" && c.name === dirName
|
|
51
|
+
);
|
|
52
|
+
if (!dir) {
|
|
53
|
+
dir = {
|
|
54
|
+
type: "directory",
|
|
55
|
+
name: dirName,
|
|
56
|
+
path: parts.slice(0, i + 1).join("/"),
|
|
57
|
+
children: []
|
|
58
|
+
};
|
|
59
|
+
current.children.push(dir);
|
|
60
|
+
}
|
|
61
|
+
current = dir;
|
|
62
|
+
}
|
|
63
|
+
const filename = parts[parts.length - 1];
|
|
64
|
+
const exists = current.children.some(
|
|
65
|
+
(c) => c.type === "file" && c.name === filename
|
|
66
|
+
);
|
|
67
|
+
if (exists) continue;
|
|
68
|
+
const frontmatter = frontmatters == null ? void 0 : frontmatters[key];
|
|
69
|
+
const fileEntry = {
|
|
70
|
+
type: "file",
|
|
71
|
+
name: filename,
|
|
72
|
+
path: relativePath,
|
|
73
|
+
size: 0,
|
|
74
|
+
// Size not available from glob keys
|
|
75
|
+
frontmatter
|
|
76
|
+
};
|
|
77
|
+
current.children.push(fileEntry);
|
|
78
|
+
}
|
|
79
|
+
return root;
|
|
80
|
+
}
|
|
81
|
+
function navigateToPath(root, path) {
|
|
82
|
+
const parts = path === "." || path === "" ? [] : path.split("/").filter(Boolean);
|
|
83
|
+
let currentDir = root;
|
|
84
|
+
let file = null;
|
|
85
|
+
for (let i = 0; i < parts.length; i++) {
|
|
86
|
+
const part = parts[i];
|
|
87
|
+
const isLastPart = i === parts.length - 1;
|
|
88
|
+
if (isLastPart) {
|
|
89
|
+
const matchedFile = currentDir.children.find(
|
|
90
|
+
(child) => child.type === "file" && child.name === part
|
|
91
|
+
);
|
|
92
|
+
if (matchedFile) {
|
|
93
|
+
file = matchedFile;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const nextDir = currentDir.children.find(
|
|
98
|
+
(child) => child.type === "directory" && child.name === part
|
|
99
|
+
);
|
|
100
|
+
if (!nextDir) {
|
|
101
|
+
throw new Error(`Path not found: ${path}`);
|
|
102
|
+
}
|
|
103
|
+
currentDir = nextDir;
|
|
104
|
+
}
|
|
105
|
+
return { directory: currentDir, file };
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
buildDirectoryTree,
|
|
109
|
+
navigateToPath
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=directory-tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directory-tree.js","sources":["../../../../plugin/src/directory-tree.ts"],"sourcesContent":["import type { DirectoryEntry, FileEntry } from './lib';\n\n/**\n * Find the common directory prefix among all paths.\n * E.g., [\"/docs/a.mdx\", \"/docs/b/c.mdx\"] -> \"/docs\"\n * Only considers directory segments, not filenames.\n */\nfunction findCommonPrefix(paths: string[]): string {\n if (paths.length === 0) return '';\n\n // Extract directory parts only (exclude filename from each path)\n const dirPaths = paths.map(p => {\n const parts = p.split('/').filter(Boolean);\n // Remove the last part (filename)\n return parts.slice(0, -1);\n });\n\n if (dirPaths.length === 0 || dirPaths[0].length === 0) return '';\n\n // Find how many directory segments are common to all paths\n const firstDirParts = dirPaths[0];\n let commonLength = 0;\n\n for (let i = 0; i < firstDirParts.length; i++) {\n const segment = firstDirParts[i];\n const allMatch = dirPaths.every(parts => parts[i] === segment);\n if (allMatch) {\n commonLength = i + 1;\n } else {\n break;\n }\n }\n\n if (commonLength > 0) {\n return '/' + firstDirParts.slice(0, commonLength).join('/');\n }\n return '';\n}\n\n/**\n * Build a directory tree from glob keys.\n * Keys are paths like \"/docs/file.mdx\" (Vite-resolved from @content alias)\n * We auto-detect and strip the common prefix (content directory).\n * @param globKeys - Array of file paths from import.meta.glob\n * @param frontmatters - Optional map of paths to frontmatter objects\n */\nexport function buildDirectoryTree(\n globKeys: string[],\n frontmatters?: Record<string, FileEntry['frontmatter']>\n): DirectoryEntry {\n const root: DirectoryEntry = {\n type: 'directory',\n name: '.',\n path: '.',\n children: []\n };\n\n if (globKeys.length === 0) return root;\n\n // Auto-detect the content directory prefix\n // Vite resolves @content to the actual path, so keys look like \"/docs/file.mdx\"\n const commonPrefix = findCommonPrefix(globKeys);\n\n for (const key of globKeys) {\n // Strip the common prefix to get path relative to content root\n let relativePath = key;\n if (commonPrefix && key.startsWith(commonPrefix)) {\n relativePath = key.slice(commonPrefix.length);\n }\n // Remove leading slash if present\n if (relativePath.startsWith('/')) {\n relativePath = relativePath.slice(1);\n }\n\n // Skip hidden files and directories\n if (relativePath.split('/').some(part => part.startsWith('.'))) {\n continue;\n }\n\n const parts = relativePath.split('/').filter(Boolean);\n if (parts.length === 0) continue;\n\n let current = root;\n\n // Navigate/create directories for all but the last part\n for (let i = 0; i < parts.length - 1; i++) {\n const dirName = parts[i];\n let dir = current.children.find(\n c => c.type === 'directory' && c.name === dirName\n ) as DirectoryEntry | undefined;\n\n if (!dir) {\n dir = {\n type: 'directory',\n name: dirName,\n path: parts.slice(0, i + 1).join('/'),\n children: []\n };\n current.children.push(dir);\n }\n current = dir;\n }\n\n // Add file entry (last part)\n const filename = parts[parts.length - 1];\n\n // Don't add duplicates\n const exists = current.children.some(\n c => c.type === 'file' && c.name === filename\n );\n if (exists) continue;\n\n // Look up frontmatter using the original key (before prefix stripping)\n const frontmatter = frontmatters?.[key];\n\n const fileEntry: FileEntry = {\n type: 'file',\n name: filename,\n path: relativePath,\n size: 0, // Size not available from glob keys\n frontmatter\n };\n current.children.push(fileEntry);\n }\n\n return root;\n}\n\n/**\n * Navigate to a path within the directory tree.\n * Returns the directory and optionally a file if the path points to one.\n */\nexport function navigateToPath(\n root: DirectoryEntry,\n path: string\n): { directory: DirectoryEntry; file: FileEntry | null } {\n const parts = path === '.' || path === '' ? [] : path.split('/').filter(Boolean);\n\n let currentDir = root;\n let file: FileEntry | null = null;\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n const isLastPart = i === parts.length - 1;\n\n // Check if this part matches a file (only on last part)\n if (isLastPart) {\n const matchedFile = currentDir.children.find(\n child => child.type === 'file' && child.name === part\n ) as FileEntry | undefined;\n\n if (matchedFile) {\n file = matchedFile;\n break;\n }\n }\n\n // Otherwise, look for a directory\n const nextDir = currentDir.children.find(\n child => child.type === 'directory' && child.name === part\n ) as DirectoryEntry | undefined;\n\n if (!nextDir) {\n throw new Error(`Path not found: ${path}`);\n }\n\n currentDir = nextDir;\n }\n\n return { directory: currentDir, file };\n}\n"],"names":[],"mappings":"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"}
|
package/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/logo_dark.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>
|
|
7
|
+
<title>veslx</title>
|
|
8
8
|
<!-- Google Fonts: DM Sans + DM Mono -->
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "veslx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,16 +23,15 @@
|
|
|
23
23
|
"vite.lib.config.ts"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"dev": "bun bin/veslx.ts serve",
|
|
27
|
-
"build": "bun bin/veslx.ts build",
|
|
26
|
+
"dev": "VESLX_DEV=1 bun bin/veslx.ts serve content",
|
|
27
|
+
"build": "bun bin/veslx.ts build content",
|
|
28
28
|
"build:lib": "vite build --config vite.lib.config.ts",
|
|
29
29
|
"prepublishOnly": "npm run build:lib"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"acorn": "^8.15.0",
|
|
33
|
-
"acorn-jsx": "^5.3.2",
|
|
34
32
|
"@icons-pack/react-simple-icons": "^13.8.0",
|
|
35
33
|
"@mdx-js/react": "^3.1.1",
|
|
34
|
+
"@mdx-js/rollup": "^3.1.1",
|
|
36
35
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
37
36
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
38
37
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
@@ -44,20 +43,40 @@
|
|
|
44
43
|
"@radix-ui/react-slot": "^1.2.4",
|
|
45
44
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
46
45
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
|
46
|
+
"@tailwindcss/postcss": "^4.1.17",
|
|
47
47
|
"@tailwindcss/vite": "^4.1.17",
|
|
48
48
|
"@types/bun": "^1.3.4",
|
|
49
49
|
"@types/js-yaml": "^4.0.9",
|
|
50
|
+
"@visx/axis": "^3.12.0",
|
|
51
|
+
"@visx/curve": "^3.12.0",
|
|
52
|
+
"@visx/gradient": "^3.12.0",
|
|
53
|
+
"@visx/grid": "^3.12.0",
|
|
54
|
+
"@visx/group": "^3.12.0",
|
|
55
|
+
"@visx/responsive": "^3.12.0",
|
|
56
|
+
"@visx/scale": "^3.12.0",
|
|
57
|
+
"@visx/shape": "^3.12.0",
|
|
58
|
+
"@visx/text": "^3.12.0",
|
|
59
|
+
"@visx/tooltip": "^3.12.0",
|
|
60
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
61
|
+
"acorn": "^8.15.0",
|
|
62
|
+
"acorn-jsx": "^5.3.2",
|
|
63
|
+
"autoprefixer": "^10.4.22",
|
|
50
64
|
"bun-promptx": "^0.2.0",
|
|
51
65
|
"cac": "^6.7.14",
|
|
66
|
+
"canvas-confetti": "^1.9.4",
|
|
67
|
+
"class-variance-authority": "^0.7.1",
|
|
68
|
+
"clsx": "^2.1.1",
|
|
52
69
|
"date-fns": "^4.1.0",
|
|
53
70
|
"embla-carousel-react": "^8.6.0",
|
|
54
71
|
"gray-matter": "^4.0.3",
|
|
55
72
|
"install": "^0.13.0",
|
|
56
73
|
"js-yaml": "^4.1.1",
|
|
57
74
|
"katex": "^0.16.25",
|
|
75
|
+
"lucide-react": "^0.554.0",
|
|
58
76
|
"minimatch": "^10.1.1",
|
|
59
77
|
"next-themes": "^0.4.6",
|
|
60
78
|
"pm2": "^6.0.14",
|
|
79
|
+
"postcss": "^8.5.6",
|
|
61
80
|
"react": "^19.2.0",
|
|
62
81
|
"react-dom": "^19.2.0",
|
|
63
82
|
"react-router-dom": "^7.9.5",
|
|
@@ -67,18 +86,11 @@
|
|
|
67
86
|
"remark-math": "^6.0.0",
|
|
68
87
|
"remark-mdx-frontmatter": "^5.2.0",
|
|
69
88
|
"shiki": "^3.15.0",
|
|
70
|
-
"vite": "^6.0.3",
|
|
71
|
-
"@vitejs/plugin-react": "^5.1.0",
|
|
72
|
-
"@mdx-js/rollup": "^3.1.1",
|
|
73
|
-
"@tailwindcss/postcss": "^4.1.17",
|
|
74
|
-
"postcss": "^8.5.6",
|
|
75
|
-
"autoprefixer": "^10.4.22",
|
|
76
|
-
"lucide-react": "^0.554.0",
|
|
77
|
-
"class-variance-authority": "^0.7.1",
|
|
78
|
-
"clsx": "^2.1.1",
|
|
79
89
|
"tailwind-merge": "^3.4.0",
|
|
80
90
|
"tailwindcss": "^4.1.17",
|
|
81
|
-
"tailwindcss-animate": "^1.0.7"
|
|
91
|
+
"tailwindcss-animate": "^1.0.7",
|
|
92
|
+
"unist-util-visit": "^5.0.0",
|
|
93
|
+
"vite": "^6.0.3"
|
|
82
94
|
},
|
|
83
95
|
"devDependencies": {
|
|
84
96
|
"@eslint/js": "^9.39.1",
|
package/plugin/src/client.tsx
CHANGED
|
@@ -1,53 +1,54 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { DirectoryEntry, FileEntry } from "./lib";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Otherwise, look for a directory
|
|
27
|
-
const nextDir = currentDir.children.find(
|
|
28
|
-
(child) => child.type === "directory" && child.name === part
|
|
29
|
-
) as DirectoryEntry | undefined;
|
|
30
|
-
|
|
31
|
-
if (!nextDir) {
|
|
32
|
-
throw new Error(`Path not found: ${path}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
currentDir = nextDir;
|
|
3
|
+
import { buildDirectoryTree, navigateToPath } from "./directory-tree";
|
|
4
|
+
// @ts-ignore - virtual module
|
|
5
|
+
import { files, frontmatters } from "virtual:content-modules";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find the main content file for a directory.
|
|
9
|
+
* Supports (in order of preference):
|
|
10
|
+
* - index.mdx / index.md (modern convention)
|
|
11
|
+
* - README.mdx / README.md (traditional convention)
|
|
12
|
+
*/
|
|
13
|
+
export function findReadme(directory: DirectoryEntry): FileEntry | null {
|
|
14
|
+
const indexFiles = [
|
|
15
|
+
"index.mdx", "index.md",
|
|
16
|
+
"README.mdx", "Readme.mdx", "readme.mdx",
|
|
17
|
+
"README.md", "Readme.md", "readme.md",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const filename of indexFiles) {
|
|
21
|
+
const found = directory.children.find((child) =>
|
|
22
|
+
child.type === "file" && child.name === filename
|
|
23
|
+
) as FileEntry | undefined;
|
|
24
|
+
if (found) return found;
|
|
36
25
|
}
|
|
37
26
|
|
|
38
|
-
return
|
|
27
|
+
return null;
|
|
39
28
|
}
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Find all MDX files in a directory (excluding index/README and slides)
|
|
32
|
+
*/
|
|
33
|
+
export function findMdxFiles(directory: DirectoryEntry): FileEntry[] {
|
|
34
|
+
const indexFiles = [
|
|
35
|
+
"index.mdx", "index.md",
|
|
36
|
+
"README.mdx", "Readme.mdx", "readme.mdx",
|
|
37
|
+
"README.md", "Readme.md", "readme.md",
|
|
38
|
+
];
|
|
39
|
+
const slideFiles = [
|
|
40
|
+
"SLIDES.mdx", "Slides.mdx", "slides.mdx",
|
|
41
|
+
"SLIDES.md", "Slides.md", "slides.md",
|
|
42
|
+
];
|
|
43
|
+
const excludeFiles = [...indexFiles, ...slideFiles];
|
|
44
|
+
|
|
45
|
+
return directory.children.filter((child): child is FileEntry =>
|
|
46
|
+
child.type === "file" &&
|
|
47
|
+
(child.name.endsWith('.mdx') || child.name.endsWith('.md')) &&
|
|
48
|
+
!excludeFiles.includes(child.name) &&
|
|
49
|
+
!child.name.endsWith('.slides.mdx') &&
|
|
50
|
+
!child.name.endsWith('.slides.md')
|
|
51
|
+
);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export function findSlides(directory: DirectoryEntry): FileEntry | null {
|
|
@@ -64,83 +65,30 @@ export function findSlides(directory: DirectoryEntry): FileEntry | null {
|
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
export type DirectoryError =
|
|
67
|
-
| { type: '
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
| { type: 'path_not_found'; message: string; status: 404 };
|
|
69
|
+
|
|
70
|
+
// Build directory tree once from glob keys, with frontmatter metadata
|
|
71
|
+
const directoryTree = buildDirectoryTree(Object.keys(files), frontmatters as Record<string, FileEntry['frontmatter']>);
|
|
71
72
|
|
|
72
73
|
export function useDirectory(path: string = ".") {
|
|
73
|
-
const [directory, setDirectory] = useState<DirectoryEntry | null>(null);
|
|
74
|
-
const [file, setFile] = useState<FileEntry | null>(null);
|
|
75
|
-
const [loading, setLoading] = useState(true);
|
|
76
74
|
const [error, setError] = useState<DirectoryError | null>(null);
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!response.ok) {
|
|
87
|
-
if (response.status === 404) {
|
|
88
|
-
setError({ type: 'config_not_found', message: '.veslx.json not found' });
|
|
89
|
-
} else {
|
|
90
|
-
setError({ type: 'fetch_error', message: `Failed to fetch: ${response.status} ${response.statusText}` });
|
|
91
|
-
}
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let json;
|
|
96
|
-
try {
|
|
97
|
-
json = await response.json();
|
|
98
|
-
} catch {
|
|
99
|
-
setError({ type: 'parse_error', message: 'Failed to parse .veslx.json' });
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let parsed: { directory: DirectoryEntry; file: FileEntry | null };
|
|
104
|
-
try {
|
|
105
|
-
parsed = await parsePath(json, path);
|
|
106
|
-
} catch {
|
|
107
|
-
setError({ type: 'path_not_found', message: `Path not found: ${path}`, status: 404 });
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
parsed.directory.children.sort((a: any, b: any) => {
|
|
112
|
-
let aDate, bDate;
|
|
113
|
-
if (a.children) {
|
|
114
|
-
const readme = findReadme(a);
|
|
115
|
-
if (readme && readme.frontmatter && readme.frontmatter.date) {
|
|
116
|
-
aDate = new Date(readme.frontmatter.date as string | number | Date)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (b.children) {
|
|
120
|
-
const readme = findReadme(b);
|
|
121
|
-
if (readme && readme.frontmatter && readme.frontmatter.date) {
|
|
122
|
-
bDate = new Date(readme.frontmatter.date as string | number | Date)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (aDate && bDate) {
|
|
126
|
-
return bDate.getTime() - aDate.getTime()
|
|
127
|
-
}
|
|
128
|
-
return 0;
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
setDirectory(parsed.directory);
|
|
132
|
-
setFile(parsed.file);
|
|
133
|
-
} catch (err: any) {
|
|
134
|
-
setError({ type: 'fetch_error', message: err.message || 'Unknown error' });
|
|
135
|
-
} finally {
|
|
136
|
-
setLoading(false);
|
|
137
|
-
}
|
|
138
|
-
})();
|
|
139
|
-
|
|
140
|
-
return () => {};
|
|
76
|
+
const result = useMemo(() => {
|
|
77
|
+
try {
|
|
78
|
+
const { directory, file } = navigateToPath(directoryTree, path);
|
|
79
|
+
return { directory, file };
|
|
80
|
+
} catch {
|
|
81
|
+
setError({ type: 'path_not_found', message: `Path not found: ${path}`, status: 404 });
|
|
82
|
+
return { directory: null, file: null };
|
|
83
|
+
}
|
|
141
84
|
}, [path]);
|
|
142
85
|
|
|
143
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
directory: result.directory,
|
|
88
|
+
file: result.file,
|
|
89
|
+
loading: false, // No async loading needed
|
|
90
|
+
error
|
|
91
|
+
};
|
|
144
92
|
}
|
|
145
93
|
|
|
146
94
|
export function useFileContent(path: string) {
|