@imjp/writenex-astro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- package/src/types/version.ts +117 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Filesystem module exports for @writenex/astro
|
|
3
|
+
*
|
|
4
|
+
* This module provides the public API for filesystem operations,
|
|
5
|
+
* including reading, writing, watching content files, version history,
|
|
6
|
+
* and image handling.
|
|
7
|
+
*
|
|
8
|
+
* @module @writenex/astro/filesystem
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Reader functions and types
|
|
12
|
+
export {
|
|
13
|
+
readContentFile,
|
|
14
|
+
readCollection,
|
|
15
|
+
getCollectionSummaries,
|
|
16
|
+
getCollectionCount,
|
|
17
|
+
checkCollection,
|
|
18
|
+
getFileStats,
|
|
19
|
+
isContentFile,
|
|
20
|
+
extractSlug,
|
|
21
|
+
generateExcerpt,
|
|
22
|
+
toContentSummary,
|
|
23
|
+
getContentFilePath,
|
|
24
|
+
} from "./reader";
|
|
25
|
+
export type { ReadContentOptions, ReadFileResult } from "./reader";
|
|
26
|
+
|
|
27
|
+
// Writer functions and types
|
|
28
|
+
export {
|
|
29
|
+
createContent,
|
|
30
|
+
updateContent,
|
|
31
|
+
deleteContent,
|
|
32
|
+
generateSlug,
|
|
33
|
+
generateUniqueSlug,
|
|
34
|
+
} from "./writer";
|
|
35
|
+
export type {
|
|
36
|
+
CreateContentOptions,
|
|
37
|
+
UpdateContentOptions,
|
|
38
|
+
WriteResult,
|
|
39
|
+
} from "./writer";
|
|
40
|
+
|
|
41
|
+
// Watcher functions and types
|
|
42
|
+
export {
|
|
43
|
+
ContentWatcher,
|
|
44
|
+
FileModificationTracker,
|
|
45
|
+
createContentWatcher,
|
|
46
|
+
} from "./watcher";
|
|
47
|
+
export type {
|
|
48
|
+
FileChangeType,
|
|
49
|
+
FileChangeEvent,
|
|
50
|
+
WatcherOptions,
|
|
51
|
+
} from "./watcher";
|
|
52
|
+
|
|
53
|
+
// Version history functions
|
|
54
|
+
export {
|
|
55
|
+
saveVersion,
|
|
56
|
+
getVersions,
|
|
57
|
+
getVersion,
|
|
58
|
+
deleteVersion,
|
|
59
|
+
clearVersions,
|
|
60
|
+
pruneVersions,
|
|
61
|
+
restoreVersion,
|
|
62
|
+
generateVersionId,
|
|
63
|
+
parseVersionId,
|
|
64
|
+
getVersionStoragePath,
|
|
65
|
+
getVersionFilePath,
|
|
66
|
+
getManifestPath,
|
|
67
|
+
generatePreview,
|
|
68
|
+
readManifest,
|
|
69
|
+
writeManifest,
|
|
70
|
+
createEmptyManifest,
|
|
71
|
+
recoverManifest,
|
|
72
|
+
getOrRecoverManifest,
|
|
73
|
+
ensureGitignore,
|
|
74
|
+
ensureStorageDirectory,
|
|
75
|
+
} from "./versions";
|
|
76
|
+
|
|
77
|
+
// Version config helpers
|
|
78
|
+
export {
|
|
79
|
+
resolveVersionConfig,
|
|
80
|
+
isVersionHistoryEnabled,
|
|
81
|
+
saveVersionWithConfig,
|
|
82
|
+
getVersionsWithConfig,
|
|
83
|
+
getVersionWithConfig,
|
|
84
|
+
deleteVersionWithConfig,
|
|
85
|
+
clearVersionsWithConfig,
|
|
86
|
+
pruneVersionsWithConfig,
|
|
87
|
+
restoreVersionWithConfig,
|
|
88
|
+
} from "./version-config";
|
|
89
|
+
|
|
90
|
+
// Image functions and types
|
|
91
|
+
export {
|
|
92
|
+
uploadImage,
|
|
93
|
+
parseMultipartFormData,
|
|
94
|
+
isValidImageFile,
|
|
95
|
+
discoverContentImages,
|
|
96
|
+
getContentImageFolder,
|
|
97
|
+
detectContentStructure,
|
|
98
|
+
calculateRelativePath,
|
|
99
|
+
scanDirectoryForImages,
|
|
100
|
+
DEFAULT_IMAGE_CONFIG,
|
|
101
|
+
} from "./images";
|
|
102
|
+
export type {
|
|
103
|
+
ImageUploadResult,
|
|
104
|
+
ImageUploadOptions,
|
|
105
|
+
ContentStructure,
|
|
106
|
+
ContentStructureResult,
|
|
107
|
+
} from "./images";
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Filesystem reader for content collections
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions for reading content files from the filesystem,
|
|
5
|
+
* parsing frontmatter, and extracting content metadata.
|
|
6
|
+
*
|
|
7
|
+
* ## Features:
|
|
8
|
+
* - Read individual content files with frontmatter parsing
|
|
9
|
+
* - List all content files in a collection
|
|
10
|
+
* - Generate content summaries for listing
|
|
11
|
+
* - Support for .md and .mdx files
|
|
12
|
+
*
|
|
13
|
+
* @module @writenex/astro/filesystem/reader
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { join, basename, extname, relative } from "node:path";
|
|
19
|
+
import matter from "gray-matter";
|
|
20
|
+
import type { ContentItem, ContentSummary } from "@/types";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Supported content file extensions
|
|
24
|
+
*/
|
|
25
|
+
const CONTENT_EXTENSIONS = [".md", ".mdx"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maximum excerpt length in characters
|
|
29
|
+
*/
|
|
30
|
+
const EXCERPT_LENGTH = 150;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for reading content
|
|
34
|
+
*/
|
|
35
|
+
export interface ReadContentOptions {
|
|
36
|
+
/** Include draft content in listings */
|
|
37
|
+
includeDrafts?: boolean;
|
|
38
|
+
/** Sort field for listings */
|
|
39
|
+
sortBy?: string;
|
|
40
|
+
/** Sort order */
|
|
41
|
+
sortOrder?: "asc" | "desc";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Result of reading a content file
|
|
46
|
+
*/
|
|
47
|
+
export interface ReadFileResult {
|
|
48
|
+
/** Whether the read was successful */
|
|
49
|
+
success: boolean;
|
|
50
|
+
/** The content item (if successful) */
|
|
51
|
+
content?: ContentItem;
|
|
52
|
+
/** Error message (if failed) */
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a file is a content file based on extension
|
|
58
|
+
*
|
|
59
|
+
* @param filename - The filename to check
|
|
60
|
+
* @returns True if the file is a content file
|
|
61
|
+
*/
|
|
62
|
+
export function isContentFile(filename: string): boolean {
|
|
63
|
+
const ext = extname(filename).toLowerCase();
|
|
64
|
+
return CONTENT_EXTENSIONS.includes(ext);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract slug from a content file path
|
|
69
|
+
*
|
|
70
|
+
* Handles various file patterns:
|
|
71
|
+
* - `my-post.md` -> `my-post`
|
|
72
|
+
* - `2024-01-15-my-post.md` -> `2024-01-15-my-post`
|
|
73
|
+
* - `my-post/index.md` -> `my-post`
|
|
74
|
+
*
|
|
75
|
+
* @param filePath - Path to the content file
|
|
76
|
+
* @param collectionPath - Path to the collection directory
|
|
77
|
+
* @returns The extracted slug
|
|
78
|
+
*/
|
|
79
|
+
export function extractSlug(filePath: string, collectionPath: string): string {
|
|
80
|
+
const relativePath = relative(collectionPath, filePath);
|
|
81
|
+
const filename = basename(relativePath);
|
|
82
|
+
const ext = extname(filename);
|
|
83
|
+
|
|
84
|
+
// Handle index files (folder-based content)
|
|
85
|
+
if (filename === "index.md" || filename === "index.mdx") {
|
|
86
|
+
const parts = relativePath.split("/");
|
|
87
|
+
if (parts.length >= 2) {
|
|
88
|
+
const slug = parts[parts.length - 2];
|
|
89
|
+
if (slug) return slug;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Remove extension to get slug
|
|
94
|
+
return filename.slice(0, -ext.length);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate an excerpt from markdown content
|
|
99
|
+
*
|
|
100
|
+
* @param body - The markdown body content
|
|
101
|
+
* @param maxLength - Maximum excerpt length
|
|
102
|
+
* @returns The generated excerpt
|
|
103
|
+
*/
|
|
104
|
+
export function generateExcerpt(
|
|
105
|
+
body: string,
|
|
106
|
+
maxLength: number = EXCERPT_LENGTH
|
|
107
|
+
): string {
|
|
108
|
+
// Remove markdown formatting for cleaner excerpt
|
|
109
|
+
const cleaned = body
|
|
110
|
+
// Remove headers
|
|
111
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
112
|
+
// Remove bold/italic
|
|
113
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
114
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
115
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
116
|
+
.replace(/_([^_]+)_/g, "$1")
|
|
117
|
+
// Remove links but keep text
|
|
118
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
119
|
+
// Remove images
|
|
120
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "")
|
|
121
|
+
// Remove code blocks
|
|
122
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
123
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
124
|
+
// Remove blockquotes
|
|
125
|
+
.replace(/^>\s+/gm, "")
|
|
126
|
+
// Remove horizontal rules
|
|
127
|
+
.replace(/^[-*_]{3,}$/gm, "")
|
|
128
|
+
// Collapse whitespace
|
|
129
|
+
.replace(/\s+/g, " ")
|
|
130
|
+
.trim();
|
|
131
|
+
|
|
132
|
+
if (cleaned.length <= maxLength) {
|
|
133
|
+
return cleaned;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Truncate at word boundary
|
|
137
|
+
const truncated = cleaned.slice(0, maxLength);
|
|
138
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
139
|
+
|
|
140
|
+
if (lastSpace > maxLength * 0.7) {
|
|
141
|
+
return truncated.slice(0, lastSpace) + "...";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return truncated + "...";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read and parse a single content file
|
|
149
|
+
*
|
|
150
|
+
* @param filePath - Absolute path to the content file
|
|
151
|
+
* @param collectionPath - Path to the collection directory
|
|
152
|
+
* @returns ReadFileResult with the parsed content or error
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* const result = await readContentFile(
|
|
157
|
+
* '/project/src/content/blog/my-post.md',
|
|
158
|
+
* '/project/src/content/blog'
|
|
159
|
+
* );
|
|
160
|
+
*
|
|
161
|
+
* if (result.success) {
|
|
162
|
+
* console.log(result.content.frontmatter.title);
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export async function readContentFile(
|
|
167
|
+
filePath: string,
|
|
168
|
+
collectionPath: string
|
|
169
|
+
): Promise<ReadFileResult> {
|
|
170
|
+
try {
|
|
171
|
+
// Check if file exists
|
|
172
|
+
if (!existsSync(filePath)) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
error: `File not found: ${filePath}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Read file content and stats in parallel
|
|
180
|
+
const [raw, stats] = await Promise.all([
|
|
181
|
+
readFile(filePath, "utf-8"),
|
|
182
|
+
stat(filePath),
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
// Parse frontmatter
|
|
186
|
+
const { data: frontmatter, content: body } = matter(raw);
|
|
187
|
+
|
|
188
|
+
// Extract slug
|
|
189
|
+
const id = extractSlug(filePath, collectionPath);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
content: {
|
|
194
|
+
id,
|
|
195
|
+
path: filePath,
|
|
196
|
+
frontmatter,
|
|
197
|
+
body: body.trim(),
|
|
198
|
+
raw,
|
|
199
|
+
mtime: stats.mtimeMs,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
error: `Failed to read content file: ${message}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* List all content files in a directory recursively
|
|
213
|
+
*
|
|
214
|
+
* @param dirPath - Path to the directory to scan
|
|
215
|
+
* @returns Array of absolute file paths
|
|
216
|
+
*/
|
|
217
|
+
async function listFilesRecursive(dirPath: string): Promise<string[]> {
|
|
218
|
+
const files: string[] = [];
|
|
219
|
+
|
|
220
|
+
if (!existsSync(dirPath)) {
|
|
221
|
+
return files;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
225
|
+
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const fullPath = join(dirPath, entry.name);
|
|
228
|
+
|
|
229
|
+
if (entry.isDirectory()) {
|
|
230
|
+
// Recursively scan subdirectories
|
|
231
|
+
const subFiles = await listFilesRecursive(fullPath);
|
|
232
|
+
files.push(...subFiles);
|
|
233
|
+
} else if (entry.isFile() && isContentFile(entry.name)) {
|
|
234
|
+
files.push(fullPath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return files;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Read all content files in a collection
|
|
243
|
+
*
|
|
244
|
+
* @param collectionPath - Absolute path to the collection directory
|
|
245
|
+
* @param options - Read options
|
|
246
|
+
* @returns Array of content items
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const items = await readCollection('/project/src/content/blog', {
|
|
251
|
+
* includeDrafts: false,
|
|
252
|
+
* sortBy: 'pubDate',
|
|
253
|
+
* sortOrder: 'desc',
|
|
254
|
+
* });
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export async function readCollection(
|
|
258
|
+
collectionPath: string,
|
|
259
|
+
options: ReadContentOptions = {}
|
|
260
|
+
): Promise<ContentItem[]> {
|
|
261
|
+
const { includeDrafts = true, sortBy, sortOrder = "desc" } = options;
|
|
262
|
+
|
|
263
|
+
// Get all content files
|
|
264
|
+
const filePaths = await listFilesRecursive(collectionPath);
|
|
265
|
+
|
|
266
|
+
// Read and parse all files
|
|
267
|
+
const results = await Promise.all(
|
|
268
|
+
filePaths.map((fp) => readContentFile(fp, collectionPath))
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Filter successful reads and optionally filter drafts
|
|
272
|
+
let items = results
|
|
273
|
+
.filter(
|
|
274
|
+
(r): r is { success: true; content: ContentItem } =>
|
|
275
|
+
r.success && !!r.content
|
|
276
|
+
)
|
|
277
|
+
.map((r) => r.content)
|
|
278
|
+
.filter((item) => {
|
|
279
|
+
if (!includeDrafts && item.frontmatter.draft === true) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Sort if requested
|
|
286
|
+
if (sortBy) {
|
|
287
|
+
items = items.sort((a, b) => {
|
|
288
|
+
const aVal = a.frontmatter[sortBy];
|
|
289
|
+
const bVal = b.frontmatter[sortBy];
|
|
290
|
+
|
|
291
|
+
// Handle undefined values
|
|
292
|
+
if (aVal === undefined && bVal === undefined) return 0;
|
|
293
|
+
if (aVal === undefined) return sortOrder === "asc" ? -1 : 1;
|
|
294
|
+
if (bVal === undefined) return sortOrder === "asc" ? 1 : -1;
|
|
295
|
+
|
|
296
|
+
// Compare values (convert to string for comparison)
|
|
297
|
+
const aStr = String(aVal);
|
|
298
|
+
const bStr = String(bVal);
|
|
299
|
+
if (aStr < bStr) return sortOrder === "asc" ? -1 : 1;
|
|
300
|
+
if (aStr > bStr) return sortOrder === "asc" ? 1 : -1;
|
|
301
|
+
return 0;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return items;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Convert a content item to a summary for listing
|
|
310
|
+
*
|
|
311
|
+
* @param item - The full content item
|
|
312
|
+
* @returns Content summary with essential fields
|
|
313
|
+
*/
|
|
314
|
+
export function toContentSummary(item: ContentItem): ContentSummary {
|
|
315
|
+
const { id, path, frontmatter, body } = item;
|
|
316
|
+
|
|
317
|
+
// Support both pubDate and publishDate naming conventions
|
|
318
|
+
const dateValue =
|
|
319
|
+
frontmatter.pubDate ?? frontmatter.publishDate ?? frontmatter.date;
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
id,
|
|
323
|
+
path,
|
|
324
|
+
title: String(frontmatter.title ?? id),
|
|
325
|
+
pubDate: dateValue ? String(dateValue) : undefined,
|
|
326
|
+
draft: frontmatter.draft === true,
|
|
327
|
+
excerpt: generateExcerpt(body),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get content summaries for a collection
|
|
333
|
+
*
|
|
334
|
+
* @param collectionPath - Absolute path to the collection directory
|
|
335
|
+
* @param options - Read options
|
|
336
|
+
* @returns Array of content summaries
|
|
337
|
+
*/
|
|
338
|
+
export async function getCollectionSummaries(
|
|
339
|
+
collectionPath: string,
|
|
340
|
+
options: ReadContentOptions = {}
|
|
341
|
+
): Promise<ContentSummary[]> {
|
|
342
|
+
const items = await readCollection(collectionPath, options);
|
|
343
|
+
return items.map(toContentSummary);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get the count of content files in a collection
|
|
348
|
+
*
|
|
349
|
+
* @param collectionPath - Absolute path to the collection directory
|
|
350
|
+
* @returns Number of content files
|
|
351
|
+
*/
|
|
352
|
+
export async function getCollectionCount(
|
|
353
|
+
collectionPath: string
|
|
354
|
+
): Promise<number> {
|
|
355
|
+
const filePaths = await listFilesRecursive(collectionPath);
|
|
356
|
+
return filePaths.length;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Check if a collection directory exists and contains content
|
|
361
|
+
*
|
|
362
|
+
* @param collectionPath - Absolute path to the collection directory
|
|
363
|
+
* @returns Object with exists and hasContent flags
|
|
364
|
+
*/
|
|
365
|
+
export async function checkCollection(collectionPath: string): Promise<{
|
|
366
|
+
exists: boolean;
|
|
367
|
+
hasContent: boolean;
|
|
368
|
+
count: number;
|
|
369
|
+
}> {
|
|
370
|
+
if (!existsSync(collectionPath)) {
|
|
371
|
+
return { exists: false, hasContent: false, count: 0 };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const count = await getCollectionCount(collectionPath);
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
exists: true,
|
|
378
|
+
hasContent: count > 0,
|
|
379
|
+
count,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get file stats for a content file
|
|
385
|
+
*
|
|
386
|
+
* @param filePath - Path to the content file
|
|
387
|
+
* @returns File stats or null if file doesn't exist
|
|
388
|
+
*/
|
|
389
|
+
export async function getFileStats(filePath: string): Promise<{
|
|
390
|
+
size: number;
|
|
391
|
+
mtime: Date;
|
|
392
|
+
ctime: Date;
|
|
393
|
+
} | null> {
|
|
394
|
+
try {
|
|
395
|
+
const stats = await stat(filePath);
|
|
396
|
+
return {
|
|
397
|
+
size: stats.size,
|
|
398
|
+
mtime: stats.mtime,
|
|
399
|
+
ctime: stats.birthtime,
|
|
400
|
+
};
|
|
401
|
+
} catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get the file path for a content item by ID
|
|
408
|
+
*
|
|
409
|
+
* Searches for the content file in the collection directory,
|
|
410
|
+
* handling different content structures:
|
|
411
|
+
* - Folder-based: `slug/index.md` or `slug/index.mdx`
|
|
412
|
+
* - Flat file: `slug.md` or `slug.mdx`
|
|
413
|
+
*
|
|
414
|
+
* @param collectionPath - Path to the collection directory
|
|
415
|
+
* @param contentId - Content ID (slug)
|
|
416
|
+
* @returns File path if found, null otherwise
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```typescript
|
|
420
|
+
* const filePath = getContentFilePath('/project/src/content/blog', 'my-post');
|
|
421
|
+
* // Returns: '/project/src/content/blog/my-post.md' or
|
|
422
|
+
* // '/project/src/content/blog/my-post/index.md'
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
export function getContentFilePath(
|
|
426
|
+
collectionPath: string,
|
|
427
|
+
contentId: string
|
|
428
|
+
): string | null {
|
|
429
|
+
// Try folder-based structure first (slug/index.md or slug/index.mdx)
|
|
430
|
+
const indexMdPath = join(collectionPath, contentId, "index.md");
|
|
431
|
+
if (existsSync(indexMdPath)) {
|
|
432
|
+
return indexMdPath;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const indexMdxPath = join(collectionPath, contentId, "index.mdx");
|
|
436
|
+
if (existsSync(indexMdxPath)) {
|
|
437
|
+
return indexMdxPath;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Try flat file structure (slug.md or slug.mdx)
|
|
441
|
+
const flatMdPath = join(collectionPath, `${contentId}.md`);
|
|
442
|
+
if (existsSync(flatMdPath)) {
|
|
443
|
+
return flatMdPath;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const flatMdxPath = join(collectionPath, `${contentId}.mdx`);
|
|
447
|
+
if (existsSync(flatMdxPath)) {
|
|
448
|
+
return flatMdxPath;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return null;
|
|
452
|
+
}
|