@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.
Files changed (141) hide show
  1. package/README.md +539 -0
  2. package/dist/chunk-5PM6EQE5.js +151 -0
  3. package/dist/chunk-5PM6EQE5.js.map +1 -0
  4. package/dist/chunk-7XU5X6CW.js +1331 -0
  5. package/dist/chunk-7XU5X6CW.js.map +1 -0
  6. package/dist/chunk-AAOQHQPU.js +574 -0
  7. package/dist/chunk-AAOQHQPU.js.map +1 -0
  8. package/dist/chunk-CF2XXJFF.js +1410 -0
  9. package/dist/chunk-CF2XXJFF.js.map +1 -0
  10. package/dist/chunk-CRPZUUDU.js +52 -0
  11. package/dist/chunk-CRPZUUDU.js.map +1 -0
  12. package/dist/chunk-CYLDJ3HZ.js +310 -0
  13. package/dist/chunk-CYLDJ3HZ.js.map +1 -0
  14. package/dist/chunk-KIKIPIFA.js +1 -0
  15. package/dist/chunk-KIKIPIFA.js.map +1 -0
  16. package/dist/chunk-XNTQTTJU.js +145 -0
  17. package/dist/chunk-XNTQTTJU.js.map +1 -0
  18. package/dist/client/index.css +2 -0
  19. package/dist/client/index.css.map +1 -0
  20. package/dist/client/index.js +375 -0
  21. package/dist/client/index.js.map +1 -0
  22. package/dist/client/styles.css +584 -0
  23. package/dist/client/variables.css +304 -0
  24. package/dist/config/index.d.ts +54 -0
  25. package/dist/config/index.js +38 -0
  26. package/dist/config/index.js.map +1 -0
  27. package/dist/config-BmEdBDo_.d.ts +220 -0
  28. package/dist/content-BWR52vD-.d.ts +64 -0
  29. package/dist/discovery/index.d.ts +310 -0
  30. package/dist/discovery/index.js +38 -0
  31. package/dist/discovery/index.js.map +1 -0
  32. package/dist/errors-C0iYiDTv.d.ts +107 -0
  33. package/dist/filesystem/index.d.ts +1292 -0
  34. package/dist/filesystem/index.js +203 -0
  35. package/dist/filesystem/index.js.map +1 -0
  36. package/dist/image-FP7w5ZIs.d.ts +47 -0
  37. package/dist/index.d.ts +64 -0
  38. package/dist/index.js +151 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/loader-55LWCXHA.js +12 -0
  41. package/dist/loader-55LWCXHA.js.map +1 -0
  42. package/dist/loader-CrdnaAWR.d.ts +327 -0
  43. package/dist/server/index.d.ts +357 -0
  44. package/dist/server/index.js +37 -0
  45. package/dist/server/index.js.map +1 -0
  46. package/package.json +94 -0
  47. package/src/client/App.tsx +900 -0
  48. package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
  49. package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
  50. package/src/client/components/ConfigPanel/index.ts +6 -0
  51. package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
  52. package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
  53. package/src/client/components/CreateContentModal/index.ts +7 -0
  54. package/src/client/components/Editor/Editor.css +885 -0
  55. package/src/client/components/Editor/Editor.tsx +484 -0
  56. package/src/client/components/Editor/ImageDialog.css +344 -0
  57. package/src/client/components/Editor/ImageDialog.tsx +367 -0
  58. package/src/client/components/Editor/LinkDialog.css +326 -0
  59. package/src/client/components/Editor/LinkDialog.tsx +332 -0
  60. package/src/client/components/Editor/index.ts +6 -0
  61. package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
  62. package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
  63. package/src/client/components/FrontmatterForm/index.ts +7 -0
  64. package/src/client/components/Header/Header.css +300 -0
  65. package/src/client/components/Header/Header.tsx +300 -0
  66. package/src/client/components/Header/index.ts +7 -0
  67. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
  68. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
  69. package/src/client/components/KeyboardShortcuts/index.ts +6 -0
  70. package/src/client/components/LazyEditor.tsx +75 -0
  71. package/src/client/components/LiveRegion/LiveRegion.css +19 -0
  72. package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
  73. package/src/client/components/LiveRegion/index.ts +7 -0
  74. package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
  75. package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
  76. package/src/client/components/SearchReplace/index.ts +7 -0
  77. package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
  78. package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
  79. package/src/client/components/SelectCollectionModal/index.ts +7 -0
  80. package/src/client/components/Sidebar/Sidebar.css +570 -0
  81. package/src/client/components/Sidebar/Sidebar.tsx +617 -0
  82. package/src/client/components/Sidebar/index.ts +7 -0
  83. package/src/client/components/SkipLink/SkipLink.css +51 -0
  84. package/src/client/components/SkipLink/SkipLink.tsx +67 -0
  85. package/src/client/components/SkipLink/index.ts +7 -0
  86. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
  87. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
  88. package/src/client/components/UnsavedChangesModal/index.ts +1 -0
  89. package/src/client/components/VersionHistory/DiffViewer.css +430 -0
  90. package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
  91. package/src/client/components/VersionHistory/VersionActions.css +318 -0
  92. package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
  93. package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
  94. package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
  95. package/src/client/components/VersionHistory/index.ts +9 -0
  96. package/src/client/context/ApiContext.tsx +154 -0
  97. package/src/client/context/ThemeContext.tsx +172 -0
  98. package/src/client/hooks/useAnnounce.ts +201 -0
  99. package/src/client/hooks/useApi.ts +374 -0
  100. package/src/client/hooks/useArrowNavigation.ts +286 -0
  101. package/src/client/hooks/useAutosave.ts +241 -0
  102. package/src/client/hooks/useFocusTrap.ts +178 -0
  103. package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
  104. package/src/client/hooks/useSearch.ts +206 -0
  105. package/src/client/hooks/useVersionHistory.ts +451 -0
  106. package/src/client/index.tsx +70 -0
  107. package/src/client/styles.css +584 -0
  108. package/src/client/utils/focus.ts +57 -0
  109. package/src/client/utils/openInEditor.ts +130 -0
  110. package/src/client/variables.css +304 -0
  111. package/src/config/defaults.ts +109 -0
  112. package/src/config/index.ts +32 -0
  113. package/src/config/loader.ts +174 -0
  114. package/src/config/schema.ts +161 -0
  115. package/src/core/constants.ts +39 -0
  116. package/src/core/errors.ts +739 -0
  117. package/src/core/index.ts +11 -0
  118. package/src/discovery/collections.ts +216 -0
  119. package/src/discovery/index.ts +33 -0
  120. package/src/discovery/patterns.ts +702 -0
  121. package/src/discovery/schema.ts +453 -0
  122. package/src/filesystem/images.ts +798 -0
  123. package/src/filesystem/index.ts +107 -0
  124. package/src/filesystem/reader.ts +452 -0
  125. package/src/filesystem/version-config.ts +390 -0
  126. package/src/filesystem/versions.ts +1339 -0
  127. package/src/filesystem/watcher.ts +226 -0
  128. package/src/filesystem/writer.ts +540 -0
  129. package/src/index.ts +61 -0
  130. package/src/integration.ts +228 -0
  131. package/src/server/assets.ts +254 -0
  132. package/src/server/cache.ts +355 -0
  133. package/src/server/index.ts +33 -0
  134. package/src/server/middleware.ts +209 -0
  135. package/src/server/routes.ts +1428 -0
  136. package/src/types/api.ts +61 -0
  137. package/src/types/config.ts +134 -0
  138. package/src/types/content.ts +64 -0
  139. package/src/types/image.ts +48 -0
  140. package/src/types/index.ts +58 -0
  141. package/src/types/version.ts +117 -0
@@ -0,0 +1,574 @@
1
+ // src/filesystem/reader.ts
2
+ import { readFile, readdir, stat } from "fs/promises";
3
+ import { existsSync } from "fs";
4
+ import { join, basename, extname, relative } from "path";
5
+ import matter from "gray-matter";
6
+ var CONTENT_EXTENSIONS = [".md", ".mdx"];
7
+ var EXCERPT_LENGTH = 150;
8
+ function isContentFile(filename) {
9
+ const ext = extname(filename).toLowerCase();
10
+ return CONTENT_EXTENSIONS.includes(ext);
11
+ }
12
+ function extractSlug(filePath, collectionPath) {
13
+ const relativePath = relative(collectionPath, filePath);
14
+ const filename = basename(relativePath);
15
+ const ext = extname(filename);
16
+ if (filename === "index.md" || filename === "index.mdx") {
17
+ const parts = relativePath.split("/");
18
+ if (parts.length >= 2) {
19
+ const slug = parts[parts.length - 2];
20
+ if (slug) return slug;
21
+ }
22
+ }
23
+ return filename.slice(0, -ext.length);
24
+ }
25
+ function generateExcerpt(body, maxLength = EXCERPT_LENGTH) {
26
+ const cleaned = body.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}$/gm, "").replace(/\s+/g, " ").trim();
27
+ if (cleaned.length <= maxLength) {
28
+ return cleaned;
29
+ }
30
+ const truncated = cleaned.slice(0, maxLength);
31
+ const lastSpace = truncated.lastIndexOf(" ");
32
+ if (lastSpace > maxLength * 0.7) {
33
+ return truncated.slice(0, lastSpace) + "...";
34
+ }
35
+ return truncated + "...";
36
+ }
37
+ async function readContentFile(filePath, collectionPath) {
38
+ try {
39
+ if (!existsSync(filePath)) {
40
+ return {
41
+ success: false,
42
+ error: `File not found: ${filePath}`
43
+ };
44
+ }
45
+ const [raw, stats] = await Promise.all([
46
+ readFile(filePath, "utf-8"),
47
+ stat(filePath)
48
+ ]);
49
+ const { data: frontmatter, content: body } = matter(raw);
50
+ const id = extractSlug(filePath, collectionPath);
51
+ return {
52
+ success: true,
53
+ content: {
54
+ id,
55
+ path: filePath,
56
+ frontmatter,
57
+ body: body.trim(),
58
+ raw,
59
+ mtime: stats.mtimeMs
60
+ }
61
+ };
62
+ } catch (error) {
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ return {
65
+ success: false,
66
+ error: `Failed to read content file: ${message}`
67
+ };
68
+ }
69
+ }
70
+ async function listFilesRecursive(dirPath) {
71
+ const files = [];
72
+ if (!existsSync(dirPath)) {
73
+ return files;
74
+ }
75
+ const entries = await readdir(dirPath, { withFileTypes: true });
76
+ for (const entry of entries) {
77
+ const fullPath = join(dirPath, entry.name);
78
+ if (entry.isDirectory()) {
79
+ const subFiles = await listFilesRecursive(fullPath);
80
+ files.push(...subFiles);
81
+ } else if (entry.isFile() && isContentFile(entry.name)) {
82
+ files.push(fullPath);
83
+ }
84
+ }
85
+ return files;
86
+ }
87
+ async function readCollection(collectionPath, options = {}) {
88
+ const { includeDrafts = true, sortBy, sortOrder = "desc" } = options;
89
+ const filePaths = await listFilesRecursive(collectionPath);
90
+ const results = await Promise.all(
91
+ filePaths.map((fp) => readContentFile(fp, collectionPath))
92
+ );
93
+ let items = results.filter(
94
+ (r) => r.success && !!r.content
95
+ ).map((r) => r.content).filter((item) => {
96
+ if (!includeDrafts && item.frontmatter.draft === true) {
97
+ return false;
98
+ }
99
+ return true;
100
+ });
101
+ if (sortBy) {
102
+ items = items.sort((a, b) => {
103
+ const aVal = a.frontmatter[sortBy];
104
+ const bVal = b.frontmatter[sortBy];
105
+ if (aVal === void 0 && bVal === void 0) return 0;
106
+ if (aVal === void 0) return sortOrder === "asc" ? -1 : 1;
107
+ if (bVal === void 0) return sortOrder === "asc" ? 1 : -1;
108
+ const aStr = String(aVal);
109
+ const bStr = String(bVal);
110
+ if (aStr < bStr) return sortOrder === "asc" ? -1 : 1;
111
+ if (aStr > bStr) return sortOrder === "asc" ? 1 : -1;
112
+ return 0;
113
+ });
114
+ }
115
+ return items;
116
+ }
117
+ function toContentSummary(item) {
118
+ const { id, path, frontmatter, body } = item;
119
+ const dateValue = frontmatter.pubDate ?? frontmatter.publishDate ?? frontmatter.date;
120
+ return {
121
+ id,
122
+ path,
123
+ title: String(frontmatter.title ?? id),
124
+ pubDate: dateValue ? String(dateValue) : void 0,
125
+ draft: frontmatter.draft === true,
126
+ excerpt: generateExcerpt(body)
127
+ };
128
+ }
129
+ async function getCollectionSummaries(collectionPath, options = {}) {
130
+ const items = await readCollection(collectionPath, options);
131
+ return items.map(toContentSummary);
132
+ }
133
+ async function getCollectionCount(collectionPath) {
134
+ const filePaths = await listFilesRecursive(collectionPath);
135
+ return filePaths.length;
136
+ }
137
+ async function checkCollection(collectionPath) {
138
+ if (!existsSync(collectionPath)) {
139
+ return { exists: false, hasContent: false, count: 0 };
140
+ }
141
+ const count = await getCollectionCount(collectionPath);
142
+ return {
143
+ exists: true,
144
+ hasContent: count > 0,
145
+ count
146
+ };
147
+ }
148
+ async function getFileStats(filePath) {
149
+ try {
150
+ const stats = await stat(filePath);
151
+ return {
152
+ size: stats.size,
153
+ mtime: stats.mtime,
154
+ ctime: stats.birthtime
155
+ };
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+ function getContentFilePath(collectionPath, contentId) {
161
+ const indexMdPath = join(collectionPath, contentId, "index.md");
162
+ if (existsSync(indexMdPath)) {
163
+ return indexMdPath;
164
+ }
165
+ const indexMdxPath = join(collectionPath, contentId, "index.mdx");
166
+ if (existsSync(indexMdxPath)) {
167
+ return indexMdxPath;
168
+ }
169
+ const flatMdPath = join(collectionPath, `${contentId}.md`);
170
+ if (existsSync(flatMdPath)) {
171
+ return flatMdPath;
172
+ }
173
+ const flatMdxPath = join(collectionPath, `${contentId}.mdx`);
174
+ if (existsSync(flatMdxPath)) {
175
+ return flatMdxPath;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ // src/discovery/patterns.ts
181
+ import { readdir as readdir2 } from "fs/promises";
182
+ import { existsSync as existsSync2 } from "fs";
183
+ import { join as join2, extname as extname2, relative as relative2 } from "path";
184
+ var PATTERN_DEFINITIONS = [
185
+ // {year}/{month}/{day}/{slug}.md - Full date folder structure
186
+ {
187
+ name: "year-month-day-slug",
188
+ template: "{year}/{month}/{day}/{slug}.md",
189
+ regex: /^(\d{4})\/(\d{2})\/(\d{2})\/([^/]+)\.(md|mdx)$/,
190
+ extract: (match, ext) => ({
191
+ year: match[1] ?? "",
192
+ month: match[2] ?? "",
193
+ day: match[3] ?? "",
194
+ slug: match[4] ?? "",
195
+ extension: ext
196
+ }),
197
+ priority: 95
198
+ },
199
+ // {year}/{month}/{slug}.md - Year/month nested date structure
200
+ {
201
+ name: "year-month-slug",
202
+ template: "{year}/{month}/{slug}.md",
203
+ regex: /^(\d{4})\/(\d{2})\/([^/]+)\.(md|mdx)$/,
204
+ extract: (match, ext) => ({
205
+ year: match[1] ?? "",
206
+ month: match[2] ?? "",
207
+ slug: match[3] ?? "",
208
+ extension: ext
209
+ }),
210
+ priority: 90
211
+ },
212
+ // {year}/{slug}.md - Year folder structure
213
+ {
214
+ name: "year-slug",
215
+ template: "{year}/{slug}.md",
216
+ regex: /^(\d{4})\/([^/]+)\.(md|mdx)$/,
217
+ extract: (match, ext) => ({
218
+ year: match[1] ?? "",
219
+ slug: match[2] ?? "",
220
+ extension: ext
221
+ }),
222
+ priority: 85
223
+ },
224
+ // {lang}/{slug}/index.md - Language with folder-based content (i18n)
225
+ {
226
+ name: "lang-folder-index",
227
+ template: "{lang}/{slug}/index.md",
228
+ regex: /^([a-z]{2}(?:-[A-Z]{2})?)\/([^/]+)\/index\.(md|mdx)$/,
229
+ extract: (match, ext) => ({
230
+ lang: match[1] ?? "",
231
+ slug: match[2] ?? "",
232
+ extension: ext
233
+ }),
234
+ priority: 82
235
+ },
236
+ // {category}/{slug}/index.md - Category with folder-based content
237
+ {
238
+ name: "category-folder-index",
239
+ template: "{category}/{slug}/index.md",
240
+ regex: /^([^/]+)\/([^/]+)\/index\.(md|mdx)$/,
241
+ extract: (match, ext) => ({
242
+ category: match[1] ?? "",
243
+ slug: match[2] ?? "",
244
+ extension: ext
245
+ }),
246
+ priority: 80
247
+ },
248
+ // {slug}/index.md - Folder-based content
249
+ {
250
+ name: "folder-index",
251
+ template: "{slug}/index.md",
252
+ regex: /^([^/]+)\/index\.(md|mdx)$/,
253
+ extract: (match, ext) => ({
254
+ slug: match[1] ?? "",
255
+ extension: ext
256
+ }),
257
+ priority: 75
258
+ },
259
+ // {date}-{slug}.md - Date-prefixed (ISO format)
260
+ {
261
+ name: "date-slug",
262
+ template: "{date}-{slug}.md",
263
+ regex: /^(\d{4}-\d{2}-\d{2})-(.+)\.(md|mdx)$/,
264
+ extract: (match, ext) => ({
265
+ date: match[1] ?? "",
266
+ slug: match[2] ?? "",
267
+ extension: ext
268
+ }),
269
+ priority: 70
270
+ },
271
+ // {lang}/{slug}.md - Language-prefixed content (i18n)
272
+ // Matches: en/my-post.md, pt-BR/my-post.md
273
+ {
274
+ name: "lang-slug",
275
+ template: "{lang}/{slug}.md",
276
+ regex: /^([a-z]{2}(?:-[A-Z]{2})?)\/([^/]+)\.(md|mdx)$/,
277
+ extract: (match, ext) => ({
278
+ lang: match[1] ?? "",
279
+ slug: match[2] ?? "",
280
+ extension: ext
281
+ }),
282
+ priority: 60
283
+ },
284
+ // {category}/{slug}.md - Category folder (catch-all for non-date/non-lang folders)
285
+ {
286
+ name: "category-slug",
287
+ template: "{category}/{slug}.md",
288
+ regex: /^([^/]+)\/([^/]+)\.(md|mdx)$/,
289
+ extract: (match, ext) => ({
290
+ category: match[1] ?? "",
291
+ slug: match[2] ?? "",
292
+ extension: ext
293
+ }),
294
+ priority: 50
295
+ },
296
+ // {slug}.md - Simple flat structure (default fallback)
297
+ {
298
+ name: "simple-slug",
299
+ template: "{slug}.md",
300
+ regex: /^([^/]+)\.(md|mdx)$/,
301
+ extract: (match, ext) => ({
302
+ slug: match[1] ?? "",
303
+ extension: ext
304
+ }),
305
+ priority: 10
306
+ }
307
+ ];
308
+ async function listContentFiles(dirPath) {
309
+ const files = [];
310
+ if (!existsSync2(dirPath)) {
311
+ return files;
312
+ }
313
+ async function scan(currentPath, relativeTo) {
314
+ const entries = await readdir2(currentPath, { withFileTypes: true });
315
+ for (const entry of entries) {
316
+ const fullPath = join2(currentPath, entry.name);
317
+ const relativePath = relative2(relativeTo, fullPath);
318
+ if (entry.isDirectory()) {
319
+ if (!entry.name.startsWith(".") && !entry.name.startsWith("_")) {
320
+ await scan(fullPath, relativeTo);
321
+ }
322
+ } else if (entry.isFile() && isContentFile(entry.name)) {
323
+ files.push(relativePath);
324
+ }
325
+ }
326
+ }
327
+ await scan(dirPath, dirPath);
328
+ return files;
329
+ }
330
+ function matchPattern(relativePath) {
331
+ const normalizedPath = relativePath.replace(/\\/g, "/");
332
+ for (const pattern of PATTERN_DEFINITIONS) {
333
+ const match = normalizedPath.match(pattern.regex);
334
+ if (match) {
335
+ return { pattern, match };
336
+ }
337
+ }
338
+ return null;
339
+ }
340
+ async function detectFilePattern(collectionPath) {
341
+ const files = await listContentFiles(collectionPath);
342
+ if (files.length === 0) {
343
+ return {
344
+ pattern: "{slug}.md",
345
+ confidence: 0,
346
+ matchCount: 0,
347
+ totalFiles: 0,
348
+ samples: []
349
+ };
350
+ }
351
+ const patternCounts = /* @__PURE__ */ new Map();
352
+ for (const pattern of PATTERN_DEFINITIONS) {
353
+ patternCounts.set(pattern.name, {
354
+ pattern,
355
+ count: 0,
356
+ samples: [],
357
+ extension: ".md"
358
+ });
359
+ }
360
+ for (const filePath of files) {
361
+ const result = matchPattern(filePath);
362
+ if (result) {
363
+ const { pattern, match } = result;
364
+ const entry = patternCounts.get(pattern.name);
365
+ if (entry) {
366
+ const ext = extname2(filePath);
367
+ const extracted = pattern.extract(match, ext);
368
+ entry.count++;
369
+ entry.extension = ext;
370
+ if (entry.samples.length < 3) {
371
+ entry.samples.push({ filePath, extracted });
372
+ }
373
+ }
374
+ }
375
+ }
376
+ let bestPattern = null;
377
+ let bestScore = -1;
378
+ for (const [, entry] of patternCounts) {
379
+ if (entry.count === 0) continue;
380
+ const matchRatio = entry.count / files.length;
381
+ const score = matchRatio * 100 + entry.pattern.priority;
382
+ if (score > bestScore) {
383
+ bestScore = score;
384
+ let template = entry.pattern.template;
385
+ if (entry.extension === ".mdx") {
386
+ template = template.replace(".md", ".mdx");
387
+ }
388
+ bestPattern = {
389
+ pattern: template,
390
+ confidence: matchRatio,
391
+ matchCount: entry.count,
392
+ totalFiles: files.length,
393
+ samples: entry.samples
394
+ };
395
+ }
396
+ }
397
+ return bestPattern ?? {
398
+ pattern: "{slug}.md",
399
+ confidence: 0,
400
+ matchCount: 0,
401
+ totalFiles: files.length,
402
+ samples: []
403
+ };
404
+ }
405
+ function generatePathFromPattern(pattern, tokens) {
406
+ let result = pattern;
407
+ for (const [key, value] of Object.entries(tokens)) {
408
+ result = result.replace(`{${key}}`, value);
409
+ }
410
+ return result;
411
+ }
412
+ function parsePatternTokens(pattern) {
413
+ const tokenRegex = /\{([^}]+)\}/g;
414
+ const tokens = [];
415
+ let match;
416
+ while ((match = tokenRegex.exec(pattern)) !== null) {
417
+ if (match[1]) {
418
+ tokens.push(match[1]);
419
+ }
420
+ }
421
+ return tokens;
422
+ }
423
+ function validatePattern(pattern, requiredTokens = ["slug"]) {
424
+ const tokens = parsePatternTokens(pattern);
425
+ return requiredTokens.every((req) => tokens.includes(req));
426
+ }
427
+ function getPatternExtension(pattern) {
428
+ if (pattern.endsWith(".mdx")) {
429
+ return ".mdx";
430
+ }
431
+ return ".md";
432
+ }
433
+ var TOKEN_RESOLVERS = {
434
+ // Core tokens
435
+ slug: (_fm, slug) => slug,
436
+ // Date tokens - from pubDate or current date
437
+ date: (fm) => {
438
+ const pubDate = resolveDateFromFrontmatter(fm);
439
+ return pubDate.toISOString().split("T")[0] ?? "";
440
+ },
441
+ year: (fm) => {
442
+ const pubDate = resolveDateFromFrontmatter(fm);
443
+ return pubDate.getFullYear().toString();
444
+ },
445
+ month: (fm) => {
446
+ const pubDate = resolveDateFromFrontmatter(fm);
447
+ return (pubDate.getMonth() + 1).toString().padStart(2, "0");
448
+ },
449
+ day: (fm) => {
450
+ const pubDate = resolveDateFromFrontmatter(fm);
451
+ return pubDate.getDate().toString().padStart(2, "0");
452
+ },
453
+ // i18n tokens
454
+ lang: (fm) => {
455
+ if (typeof fm.lang === "string") return fm.lang;
456
+ if (typeof fm.language === "string") return fm.language;
457
+ if (typeof fm.locale === "string") return fm.locale;
458
+ return "en";
459
+ },
460
+ // Organization tokens
461
+ category: (fm) => {
462
+ if (typeof fm.category === "string") return fm.category;
463
+ if (Array.isArray(fm.categories) && typeof fm.categories[0] === "string") {
464
+ return fm.categories[0];
465
+ }
466
+ return "uncategorized";
467
+ },
468
+ author: (fm) => {
469
+ if (typeof fm.author === "string") return slugifyValue(fm.author);
470
+ if (typeof fm.author === "object" && fm.author !== null && "name" in fm.author) {
471
+ return slugifyValue(String(fm.author.name));
472
+ }
473
+ return "anonymous";
474
+ },
475
+ type: (fm) => {
476
+ if (typeof fm.type === "string") return fm.type;
477
+ if (typeof fm.contentType === "string") return fm.contentType;
478
+ return "post";
479
+ },
480
+ status: (fm) => {
481
+ if (typeof fm.status === "string") return fm.status;
482
+ if (fm.draft === true) return "draft";
483
+ return "published";
484
+ },
485
+ series: (fm) => {
486
+ if (typeof fm.series === "string") return slugifyValue(fm.series);
487
+ return "";
488
+ },
489
+ collection: (fm) => {
490
+ if (typeof fm.collection === "string") return fm.collection;
491
+ return "";
492
+ }
493
+ };
494
+ function resolveDateFromFrontmatter(frontmatter) {
495
+ const dateFields = ["pubDate", "date", "publishDate", "createdAt", "created"];
496
+ for (const field of dateFields) {
497
+ const value = frontmatter[field];
498
+ if (value instanceof Date) return value;
499
+ if (typeof value === "string") {
500
+ const parsed = new Date(value);
501
+ if (!isNaN(parsed.getTime())) return parsed;
502
+ }
503
+ }
504
+ return /* @__PURE__ */ new Date();
505
+ }
506
+ function slugifyValue(value) {
507
+ return value.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
508
+ }
509
+ function resolvePatternTokens(pattern, options) {
510
+ const { slug, frontmatter = {}, customTokens = {} } = options;
511
+ const tokenNames = parsePatternTokens(pattern);
512
+ const resolved = {};
513
+ for (const tokenName of tokenNames) {
514
+ if (tokenName in customTokens) {
515
+ resolved[tokenName] = customTokens[tokenName] ?? "";
516
+ continue;
517
+ }
518
+ const resolver = TOKEN_RESOLVERS[tokenName];
519
+ if (resolver) {
520
+ resolved[tokenName] = resolver(frontmatter, slug);
521
+ continue;
522
+ }
523
+ const fmValue = frontmatter[tokenName];
524
+ if (typeof fmValue === "string") {
525
+ resolved[tokenName] = slugifyValue(fmValue);
526
+ continue;
527
+ }
528
+ if (typeof fmValue === "number") {
529
+ resolved[tokenName] = fmValue.toString();
530
+ continue;
531
+ }
532
+ resolved[tokenName] = "";
533
+ }
534
+ return resolved;
535
+ }
536
+ function isValidPattern(pattern) {
537
+ if (!pattern.includes("{slug}")) {
538
+ return { valid: false, error: "Pattern must contain {slug} token" };
539
+ }
540
+ if (!pattern.endsWith(".md") && !pattern.endsWith(".mdx")) {
541
+ return { valid: false, error: "Pattern must end with .md or .mdx" };
542
+ }
543
+ const unclosed = pattern.match(/\{[^}]*$/);
544
+ if (unclosed) {
545
+ return { valid: false, error: "Pattern contains unclosed token" };
546
+ }
547
+ return { valid: true };
548
+ }
549
+ function getSupportedTokens() {
550
+ return Object.keys(TOKEN_RESOLVERS);
551
+ }
552
+
553
+ export {
554
+ isContentFile,
555
+ extractSlug,
556
+ generateExcerpt,
557
+ readContentFile,
558
+ readCollection,
559
+ toContentSummary,
560
+ getCollectionSummaries,
561
+ getCollectionCount,
562
+ checkCollection,
563
+ getFileStats,
564
+ getContentFilePath,
565
+ detectFilePattern,
566
+ generatePathFromPattern,
567
+ parsePatternTokens,
568
+ validatePattern,
569
+ getPatternExtension,
570
+ resolvePatternTokens,
571
+ isValidPattern,
572
+ getSupportedTokens
573
+ };
574
+ //# sourceMappingURL=chunk-AAOQHQPU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/filesystem/reader.ts","../src/discovery/patterns.ts"],"sourcesContent":["/**\n * @fileoverview Filesystem reader for content collections\n *\n * This module provides functions for reading content files from the filesystem,\n * parsing frontmatter, and extracting content metadata.\n *\n * ## Features:\n * - Read individual content files with frontmatter parsing\n * - List all content files in a collection\n * - Generate content summaries for listing\n * - Support for .md and .mdx files\n *\n * @module @writenex/astro/filesystem/reader\n */\n\nimport { readFile, readdir, stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, basename, extname, relative } from \"node:path\";\nimport matter from \"gray-matter\";\nimport type { ContentItem, ContentSummary } from \"@/types\";\n\n/**\n * Supported content file extensions\n */\nconst CONTENT_EXTENSIONS = [\".md\", \".mdx\"];\n\n/**\n * Maximum excerpt length in characters\n */\nconst EXCERPT_LENGTH = 150;\n\n/**\n * Options for reading content\n */\nexport interface ReadContentOptions {\n /** Include draft content in listings */\n includeDrafts?: boolean;\n /** Sort field for listings */\n sortBy?: string;\n /** Sort order */\n sortOrder?: \"asc\" | \"desc\";\n}\n\n/**\n * Result of reading a content file\n */\nexport interface ReadFileResult {\n /** Whether the read was successful */\n success: boolean;\n /** The content item (if successful) */\n content?: ContentItem;\n /** Error message (if failed) */\n error?: string;\n}\n\n/**\n * Check if a file is a content file based on extension\n *\n * @param filename - The filename to check\n * @returns True if the file is a content file\n */\nexport function isContentFile(filename: string): boolean {\n const ext = extname(filename).toLowerCase();\n return CONTENT_EXTENSIONS.includes(ext);\n}\n\n/**\n * Extract slug from a content file path\n *\n * Handles various file patterns:\n * - `my-post.md` -> `my-post`\n * - `2024-01-15-my-post.md` -> `2024-01-15-my-post`\n * - `my-post/index.md` -> `my-post`\n *\n * @param filePath - Path to the content file\n * @param collectionPath - Path to the collection directory\n * @returns The extracted slug\n */\nexport function extractSlug(filePath: string, collectionPath: string): string {\n const relativePath = relative(collectionPath, filePath);\n const filename = basename(relativePath);\n const ext = extname(filename);\n\n // Handle index files (folder-based content)\n if (filename === \"index.md\" || filename === \"index.mdx\") {\n const parts = relativePath.split(\"/\");\n if (parts.length >= 2) {\n const slug = parts[parts.length - 2];\n if (slug) return slug;\n }\n }\n\n // Remove extension to get slug\n return filename.slice(0, -ext.length);\n}\n\n/**\n * Generate an excerpt from markdown content\n *\n * @param body - The markdown body content\n * @param maxLength - Maximum excerpt length\n * @returns The generated excerpt\n */\nexport function generateExcerpt(\n body: string,\n maxLength: number = EXCERPT_LENGTH\n): string {\n // Remove markdown formatting for cleaner excerpt\n const cleaned = body\n // Remove headers\n .replace(/^#{1,6}\\s+/gm, \"\")\n // Remove bold/italic\n .replace(/\\*\\*([^*]+)\\*\\*/g, \"$1\")\n .replace(/\\*([^*]+)\\*/g, \"$1\")\n .replace(/__([^_]+)__/g, \"$1\")\n .replace(/_([^_]+)_/g, \"$1\")\n // Remove links but keep text\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\")\n // Remove images\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, \"\")\n // Remove code blocks\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`([^`]+)`/g, \"$1\")\n // Remove blockquotes\n .replace(/^>\\s+/gm, \"\")\n // Remove horizontal rules\n .replace(/^[-*_]{3,}$/gm, \"\")\n // Collapse whitespace\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (cleaned.length <= maxLength) {\n return cleaned;\n }\n\n // Truncate at word boundary\n const truncated = cleaned.slice(0, maxLength);\n const lastSpace = truncated.lastIndexOf(\" \");\n\n if (lastSpace > maxLength * 0.7) {\n return truncated.slice(0, lastSpace) + \"...\";\n }\n\n return truncated + \"...\";\n}\n\n/**\n * Read and parse a single content file\n *\n * @param filePath - Absolute path to the content file\n * @param collectionPath - Path to the collection directory\n * @returns ReadFileResult with the parsed content or error\n *\n * @example\n * ```typescript\n * const result = await readContentFile(\n * '/project/src/content/blog/my-post.md',\n * '/project/src/content/blog'\n * );\n *\n * if (result.success) {\n * console.log(result.content.frontmatter.title);\n * }\n * ```\n */\nexport async function readContentFile(\n filePath: string,\n collectionPath: string\n): Promise<ReadFileResult> {\n try {\n // Check if file exists\n if (!existsSync(filePath)) {\n return {\n success: false,\n error: `File not found: ${filePath}`,\n };\n }\n\n // Read file content and stats in parallel\n const [raw, stats] = await Promise.all([\n readFile(filePath, \"utf-8\"),\n stat(filePath),\n ]);\n\n // Parse frontmatter\n const { data: frontmatter, content: body } = matter(raw);\n\n // Extract slug\n const id = extractSlug(filePath, collectionPath);\n\n return {\n success: true,\n content: {\n id,\n path: filePath,\n frontmatter,\n body: body.trim(),\n raw,\n mtime: stats.mtimeMs,\n },\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n error: `Failed to read content file: ${message}`,\n };\n }\n}\n\n/**\n * List all content files in a directory recursively\n *\n * @param dirPath - Path to the directory to scan\n * @returns Array of absolute file paths\n */\nasync function listFilesRecursive(dirPath: string): Promise<string[]> {\n const files: string[] = [];\n\n if (!existsSync(dirPath)) {\n return files;\n }\n\n const entries = await readdir(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry.name);\n\n if (entry.isDirectory()) {\n // Recursively scan subdirectories\n const subFiles = await listFilesRecursive(fullPath);\n files.push(...subFiles);\n } else if (entry.isFile() && isContentFile(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\n/**\n * Read all content files in a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Read options\n * @returns Array of content items\n *\n * @example\n * ```typescript\n * const items = await readCollection('/project/src/content/blog', {\n * includeDrafts: false,\n * sortBy: 'pubDate',\n * sortOrder: 'desc',\n * });\n * ```\n */\nexport async function readCollection(\n collectionPath: string,\n options: ReadContentOptions = {}\n): Promise<ContentItem[]> {\n const { includeDrafts = true, sortBy, sortOrder = \"desc\" } = options;\n\n // Get all content files\n const filePaths = await listFilesRecursive(collectionPath);\n\n // Read and parse all files\n const results = await Promise.all(\n filePaths.map((fp) => readContentFile(fp, collectionPath))\n );\n\n // Filter successful reads and optionally filter drafts\n let items = results\n .filter(\n (r): r is { success: true; content: ContentItem } =>\n r.success && !!r.content\n )\n .map((r) => r.content)\n .filter((item) => {\n if (!includeDrafts && item.frontmatter.draft === true) {\n return false;\n }\n return true;\n });\n\n // Sort if requested\n if (sortBy) {\n items = items.sort((a, b) => {\n const aVal = a.frontmatter[sortBy];\n const bVal = b.frontmatter[sortBy];\n\n // Handle undefined values\n if (aVal === undefined && bVal === undefined) return 0;\n if (aVal === undefined) return sortOrder === \"asc\" ? -1 : 1;\n if (bVal === undefined) return sortOrder === \"asc\" ? 1 : -1;\n\n // Compare values (convert to string for comparison)\n const aStr = String(aVal);\n const bStr = String(bVal);\n if (aStr < bStr) return sortOrder === \"asc\" ? -1 : 1;\n if (aStr > bStr) return sortOrder === \"asc\" ? 1 : -1;\n return 0;\n });\n }\n\n return items;\n}\n\n/**\n * Convert a content item to a summary for listing\n *\n * @param item - The full content item\n * @returns Content summary with essential fields\n */\nexport function toContentSummary(item: ContentItem): ContentSummary {\n const { id, path, frontmatter, body } = item;\n\n // Support both pubDate and publishDate naming conventions\n const dateValue =\n frontmatter.pubDate ?? frontmatter.publishDate ?? frontmatter.date;\n\n return {\n id,\n path,\n title: String(frontmatter.title ?? id),\n pubDate: dateValue ? String(dateValue) : undefined,\n draft: frontmatter.draft === true,\n excerpt: generateExcerpt(body),\n };\n}\n\n/**\n * Get content summaries for a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Read options\n * @returns Array of content summaries\n */\nexport async function getCollectionSummaries(\n collectionPath: string,\n options: ReadContentOptions = {}\n): Promise<ContentSummary[]> {\n const items = await readCollection(collectionPath, options);\n return items.map(toContentSummary);\n}\n\n/**\n * Get the count of content files in a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Number of content files\n */\nexport async function getCollectionCount(\n collectionPath: string\n): Promise<number> {\n const filePaths = await listFilesRecursive(collectionPath);\n return filePaths.length;\n}\n\n/**\n * Check if a collection directory exists and contains content\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Object with exists and hasContent flags\n */\nexport async function checkCollection(collectionPath: string): Promise<{\n exists: boolean;\n hasContent: boolean;\n count: number;\n}> {\n if (!existsSync(collectionPath)) {\n return { exists: false, hasContent: false, count: 0 };\n }\n\n const count = await getCollectionCount(collectionPath);\n\n return {\n exists: true,\n hasContent: count > 0,\n count,\n };\n}\n\n/**\n * Get file stats for a content file\n *\n * @param filePath - Path to the content file\n * @returns File stats or null if file doesn't exist\n */\nexport async function getFileStats(filePath: string): Promise<{\n size: number;\n mtime: Date;\n ctime: Date;\n} | null> {\n try {\n const stats = await stat(filePath);\n return {\n size: stats.size,\n mtime: stats.mtime,\n ctime: stats.birthtime,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Get the file path for a content item by ID\n *\n * Searches for the content file in the collection directory,\n * handling different content structures:\n * - Folder-based: `slug/index.md` or `slug/index.mdx`\n * - Flat file: `slug.md` or `slug.mdx`\n *\n * @param collectionPath - Path to the collection directory\n * @param contentId - Content ID (slug)\n * @returns File path if found, null otherwise\n *\n * @example\n * ```typescript\n * const filePath = getContentFilePath('/project/src/content/blog', 'my-post');\n * // Returns: '/project/src/content/blog/my-post.md' or\n * // '/project/src/content/blog/my-post/index.md'\n * ```\n */\nexport function getContentFilePath(\n collectionPath: string,\n contentId: string\n): string | null {\n // Try folder-based structure first (slug/index.md or slug/index.mdx)\n const indexMdPath = join(collectionPath, contentId, \"index.md\");\n if (existsSync(indexMdPath)) {\n return indexMdPath;\n }\n\n const indexMdxPath = join(collectionPath, contentId, \"index.mdx\");\n if (existsSync(indexMdxPath)) {\n return indexMdxPath;\n }\n\n // Try flat file structure (slug.md or slug.mdx)\n const flatMdPath = join(collectionPath, `${contentId}.md`);\n if (existsSync(flatMdPath)) {\n return flatMdPath;\n }\n\n const flatMdxPath = join(collectionPath, `${contentId}.mdx`);\n if (existsSync(flatMdxPath)) {\n return flatMdxPath;\n }\n\n return null;\n}\n","/**\n * @fileoverview File pattern detection for content collections\n *\n * This module provides functions to detect and work with file naming patterns\n * in Astro content collections.\n *\n * ## Supported Patterns:\n * - `{slug}.md` - Simple slug-based naming\n * - `{date}-{slug}.md` - Date-prefixed naming (2024-01-15-my-post.md)\n * - `{year}/{slug}.md` - Year folder structure\n * - `{year}/{month}/{slug}.md` - Year/month folder structure\n * - `{year}/{month}/{day}/{slug}.md` - Full date folder structure\n * - `{slug}/index.md` - Folder-based with index file\n * - `{category}/{slug}.md` - Category folder structure\n * - `{category}/{slug}/index.md` - Category with folder-based content\n * - `{lang}/{slug}.md` - Language-prefixed content (i18n)\n * - `{lang}/{slug}/index.md` - Language with folder-based content\n *\n * ## Custom Patterns:\n * Developers can configure custom patterns in their collection config.\n * Custom tokens are resolved from frontmatter data or use default values.\n *\n * ## Detection Process:\n * 1. Scan collection directory for all content files\n * 2. Analyze file paths and names for common patterns\n * 3. Score each pattern based on match frequency\n * 4. Return the best matching pattern\n *\n * @module @writenex/astro/discovery/patterns\n */\n\nimport { readdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, extname, relative } from \"node:path\";\nimport { isContentFile } from \"@/filesystem/reader\";\n\n/**\n * Pattern definition with regex and template\n */\ninterface PatternDefinition {\n /** Pattern name for identification */\n name: string;\n /** Template string with tokens */\n template: string;\n /** Regex to match against file paths */\n regex: RegExp;\n /** Function to extract tokens from a match */\n extract: (match: RegExpMatchArray, ext: string) => Record<string, string>;\n /** Priority when multiple patterns match (higher = preferred) */\n priority: number;\n}\n\n/**\n * Result of pattern detection\n */\nexport interface PatternDetectionResult {\n /** The detected pattern template */\n pattern: string;\n /** Confidence score (0-1) */\n confidence: number;\n /** Number of files that matched this pattern */\n matchCount: number;\n /** Total files analyzed */\n totalFiles: number;\n /** Sample matches for debugging */\n samples: Array<{\n filePath: string;\n extracted: Record<string, string>;\n }>;\n}\n\n/**\n * All supported pattern definitions\n *\n * Order matters - more specific patterns should come first.\n * Higher priority patterns are preferred when multiple patterns match.\n */\nconst PATTERN_DEFINITIONS: PatternDefinition[] = [\n // {year}/{month}/{day}/{slug}.md - Full date folder structure\n {\n name: \"year-month-day-slug\",\n template: \"{year}/{month}/{day}/{slug}.md\",\n regex: /^(\\d{4})\\/(\\d{2})\\/(\\d{2})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n month: match[2] ?? \"\",\n day: match[3] ?? \"\",\n slug: match[4] ?? \"\",\n extension: ext,\n }),\n priority: 95,\n },\n\n // {year}/{month}/{slug}.md - Year/month nested date structure\n {\n name: \"year-month-slug\",\n template: \"{year}/{month}/{slug}.md\",\n regex: /^(\\d{4})\\/(\\d{2})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n month: match[2] ?? \"\",\n slug: match[3] ?? \"\",\n extension: ext,\n }),\n priority: 90,\n },\n\n // {year}/{slug}.md - Year folder structure\n {\n name: \"year-slug\",\n template: \"{year}/{slug}.md\",\n regex: /^(\\d{4})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 85,\n },\n\n // {lang}/{slug}/index.md - Language with folder-based content (i18n)\n {\n name: \"lang-folder-index\",\n template: \"{lang}/{slug}/index.md\",\n regex: /^([a-z]{2}(?:-[A-Z]{2})?)\\/([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n lang: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 82,\n },\n\n // {category}/{slug}/index.md - Category with folder-based content\n {\n name: \"category-folder-index\",\n template: \"{category}/{slug}/index.md\",\n regex: /^([^/]+)\\/([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n category: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 80,\n },\n\n // {slug}/index.md - Folder-based content\n {\n name: \"folder-index\",\n template: \"{slug}/index.md\",\n regex: /^([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n slug: match[1] ?? \"\",\n extension: ext,\n }),\n priority: 75,\n },\n\n // {date}-{slug}.md - Date-prefixed (ISO format)\n {\n name: \"date-slug\",\n template: \"{date}-{slug}.md\",\n regex: /^(\\d{4}-\\d{2}-\\d{2})-(.+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n date: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 70,\n },\n\n // {lang}/{slug}.md - Language-prefixed content (i18n)\n // Matches: en/my-post.md, pt-BR/my-post.md\n {\n name: \"lang-slug\",\n template: \"{lang}/{slug}.md\",\n regex: /^([a-z]{2}(?:-[A-Z]{2})?)\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n lang: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 60,\n },\n\n // {category}/{slug}.md - Category folder (catch-all for non-date/non-lang folders)\n {\n name: \"category-slug\",\n template: \"{category}/{slug}.md\",\n regex: /^([^/]+)\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n category: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 50,\n },\n\n // {slug}.md - Simple flat structure (default fallback)\n {\n name: \"simple-slug\",\n template: \"{slug}.md\",\n regex: /^([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n slug: match[1] ?? \"\",\n extension: ext,\n }),\n priority: 10,\n },\n];\n\n/**\n * List all content files in a directory recursively\n *\n * @param dirPath - Directory to scan\n * @returns Array of relative file paths\n */\nasync function listContentFiles(dirPath: string): Promise<string[]> {\n const files: string[] = [];\n\n if (!existsSync(dirPath)) {\n return files;\n }\n\n async function scan(currentPath: string, relativeTo: string): Promise<void> {\n const entries = await readdir(currentPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(currentPath, entry.name);\n const relativePath = relative(relativeTo, fullPath);\n\n if (entry.isDirectory()) {\n // Skip hidden and special directories\n if (!entry.name.startsWith(\".\") && !entry.name.startsWith(\"_\")) {\n await scan(fullPath, relativeTo);\n }\n } else if (entry.isFile() && isContentFile(entry.name)) {\n files.push(relativePath);\n }\n }\n }\n\n await scan(dirPath, dirPath);\n return files;\n}\n\n/**\n * Try to match a file path against all pattern definitions\n *\n * @param relativePath - Relative path to the content file\n * @returns Matched pattern and extracted tokens, or null\n */\nfunction matchPattern(\n relativePath: string\n): { pattern: PatternDefinition; match: RegExpMatchArray } | null {\n // Normalize path separators\n const normalizedPath = relativePath.replace(/\\\\/g, \"/\");\n\n for (const pattern of PATTERN_DEFINITIONS) {\n const match = normalizedPath.match(pattern.regex);\n if (match) {\n return { pattern, match };\n }\n }\n\n return null;\n}\n\n/**\n * Detect the file naming pattern used in a collection\n *\n * Analyzes all content files in the collection directory and determines\n * the most likely pattern based on file names and structure.\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Pattern detection result with confidence score\n *\n * @example\n * ```typescript\n * const result = await detectFilePattern('/project/src/content/blog');\n * console.log(result.pattern); // \"{date}-{slug}.md\"\n * console.log(result.confidence); // 0.95\n * ```\n */\nexport async function detectFilePattern(\n collectionPath: string\n): Promise<PatternDetectionResult> {\n const files = await listContentFiles(collectionPath);\n\n if (files.length === 0) {\n return {\n pattern: \"{slug}.md\",\n confidence: 0,\n matchCount: 0,\n totalFiles: 0,\n samples: [],\n };\n }\n\n // Count matches for each pattern\n const patternCounts = new Map<\n string,\n {\n pattern: PatternDefinition;\n count: number;\n samples: Array<{ filePath: string; extracted: Record<string, string> }>;\n extension: string;\n }\n >();\n\n for (const pattern of PATTERN_DEFINITIONS) {\n patternCounts.set(pattern.name, {\n pattern,\n count: 0,\n samples: [],\n extension: \".md\",\n });\n }\n\n // Analyze each file\n for (const filePath of files) {\n const result = matchPattern(filePath);\n\n if (result) {\n const { pattern, match } = result;\n const entry = patternCounts.get(pattern.name);\n\n if (entry) {\n const ext = extname(filePath);\n const extracted = pattern.extract(match, ext);\n\n entry.count++;\n entry.extension = ext;\n\n // Keep up to 3 samples\n if (entry.samples.length < 3) {\n entry.samples.push({ filePath, extracted });\n }\n }\n }\n }\n\n // Find the best matching pattern\n // Consider both match count and pattern priority\n let bestPattern: PatternDetectionResult | null = null;\n let bestScore = -1;\n\n for (const [, entry] of patternCounts) {\n if (entry.count === 0) continue;\n\n // Score = (match ratio * 100) + priority\n // This ensures high match ratio wins, but priority breaks ties\n const matchRatio = entry.count / files.length;\n const score = matchRatio * 100 + entry.pattern.priority;\n\n if (score > bestScore) {\n bestScore = score;\n\n // Adjust template for actual extension used\n let template = entry.pattern.template;\n if (entry.extension === \".mdx\") {\n template = template.replace(\".md\", \".mdx\");\n }\n\n bestPattern = {\n pattern: template,\n confidence: matchRatio,\n matchCount: entry.count,\n totalFiles: files.length,\n samples: entry.samples,\n };\n }\n }\n\n // Return best pattern or default\n return (\n bestPattern ?? {\n pattern: \"{slug}.md\",\n confidence: 0,\n matchCount: 0,\n totalFiles: files.length,\n samples: [],\n }\n );\n}\n\n/**\n * Generate a file path from a pattern and tokens\n *\n * @param pattern - Pattern template (e.g., \"{date}-{slug}.md\")\n * @param tokens - Token values to substitute\n * @returns Generated file path\n *\n * @example\n * ```typescript\n * const path = generatePathFromPattern(\n * \"{date}-{slug}.md\",\n * { date: \"2024-01-15\", slug: \"my-post\" }\n * );\n * // Returns: \"2024-01-15-my-post.md\"\n * ```\n */\nexport function generatePathFromPattern(\n pattern: string,\n tokens: Record<string, string>\n): string {\n let result = pattern;\n\n for (const [key, value] of Object.entries(tokens)) {\n result = result.replace(`{${key}}`, value);\n }\n\n return result;\n}\n\n/**\n * Parse a pattern template to extract token names\n *\n * @param pattern - Pattern template\n * @returns Array of token names\n *\n * @example\n * ```typescript\n * const tokens = parsePatternTokens(\"{year}/{month}/{slug}.md\");\n * // Returns: [\"year\", \"month\", \"slug\"]\n * ```\n */\nexport function parsePatternTokens(pattern: string): string[] {\n const tokenRegex = /\\{([^}]+)\\}/g;\n const tokens: string[] = [];\n let match;\n\n while ((match = tokenRegex.exec(pattern)) !== null) {\n if (match[1]) {\n tokens.push(match[1]);\n }\n }\n\n return tokens;\n}\n\n/**\n * Validate that a pattern has all required tokens\n *\n * @param pattern - Pattern template\n * @param requiredTokens - Required token names\n * @returns True if all required tokens are present\n */\nexport function validatePattern(\n pattern: string,\n requiredTokens: string[] = [\"slug\"]\n): boolean {\n const tokens = parsePatternTokens(pattern);\n return requiredTokens.every((req) => tokens.includes(req));\n}\n\n/**\n * Get the default extension for a pattern\n *\n * @param pattern - Pattern template\n * @returns The file extension (.md or .mdx)\n */\nexport function getPatternExtension(pattern: string): string {\n if (pattern.endsWith(\".mdx\")) {\n return \".mdx\";\n }\n return \".md\";\n}\n\n/**\n * Known token types and their default value generators\n */\ntype TokenResolver = (\n frontmatter: Record<string, unknown>,\n slug: string\n) => string;\n\nconst TOKEN_RESOLVERS: Record<string, TokenResolver> = {\n // Core tokens\n slug: (_fm, slug) => slug,\n\n // Date tokens - from pubDate or current date\n date: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.toISOString().split(\"T\")[0] ?? \"\";\n },\n year: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.getFullYear().toString();\n },\n month: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return (pubDate.getMonth() + 1).toString().padStart(2, \"0\");\n },\n day: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.getDate().toString().padStart(2, \"0\");\n },\n\n // i18n tokens\n lang: (fm) => {\n if (typeof fm.lang === \"string\") return fm.lang;\n if (typeof fm.language === \"string\") return fm.language;\n if (typeof fm.locale === \"string\") return fm.locale;\n return \"en\"; // Default to English\n },\n\n // Organization tokens\n category: (fm) => {\n if (typeof fm.category === \"string\") return fm.category;\n if (Array.isArray(fm.categories) && typeof fm.categories[0] === \"string\") {\n return fm.categories[0];\n }\n return \"uncategorized\";\n },\n author: (fm) => {\n if (typeof fm.author === \"string\") return slugifyValue(fm.author);\n if (\n typeof fm.author === \"object\" &&\n fm.author !== null &&\n \"name\" in fm.author\n ) {\n return slugifyValue(String(fm.author.name));\n }\n return \"anonymous\";\n },\n type: (fm) => {\n if (typeof fm.type === \"string\") return fm.type;\n if (typeof fm.contentType === \"string\") return fm.contentType;\n return \"post\";\n },\n status: (fm) => {\n if (typeof fm.status === \"string\") return fm.status;\n if (fm.draft === true) return \"draft\";\n return \"published\";\n },\n series: (fm) => {\n if (typeof fm.series === \"string\") return slugifyValue(fm.series);\n return \"\";\n },\n collection: (fm) => {\n if (typeof fm.collection === \"string\") return fm.collection;\n return \"\";\n },\n};\n\n/**\n * Resolve a date from frontmatter\n *\n * Checks common date field names: pubDate, date, publishDate, createdAt\n *\n * @param frontmatter - Frontmatter data\n * @returns Resolved Date object\n */\nfunction resolveDateFromFrontmatter(\n frontmatter: Record<string, unknown>\n): Date {\n const dateFields = [\"pubDate\", \"date\", \"publishDate\", \"createdAt\", \"created\"];\n\n for (const field of dateFields) {\n const value = frontmatter[field];\n if (value instanceof Date) return value;\n if (typeof value === \"string\") {\n const parsed = new Date(value);\n if (!isNaN(parsed.getTime())) return parsed;\n }\n }\n\n return new Date();\n}\n\n/**\n * Convert a string to a URL-safe slug\n *\n * @param value - String to slugify\n * @returns URL-safe slug\n */\nfunction slugifyValue(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/[\\s_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * Options for resolving pattern tokens\n */\nexport interface ResolveTokensOptions {\n /** The content slug */\n slug: string;\n /** Frontmatter data for resolving dynamic tokens */\n frontmatter?: Record<string, unknown>;\n /** Custom token values (override automatic resolution) */\n customTokens?: Record<string, string>;\n}\n\n/**\n * Resolve all tokens in a pattern to their values\n *\n * Token resolution priority:\n * 1. Custom tokens (explicitly provided)\n * 2. Known token resolvers (date, year, month, etc.)\n * 3. Frontmatter values (for custom tokens)\n * 4. Empty string (fallback)\n *\n * @param pattern - Pattern template with tokens\n * @param options - Resolution options\n * @returns Record of token names to resolved values\n *\n * @example\n * ```typescript\n * const tokens = resolvePatternTokens(\"{year}/{month}/{slug}.md\", {\n * slug: \"my-post\",\n * frontmatter: { pubDate: new Date(\"2024-06-15\") }\n * });\n * // Returns: { year: \"2024\", month: \"06\", slug: \"my-post\" }\n * ```\n */\nexport function resolvePatternTokens(\n pattern: string,\n options: ResolveTokensOptions\n): Record<string, string> {\n const { slug, frontmatter = {}, customTokens = {} } = options;\n const tokenNames = parsePatternTokens(pattern);\n const resolved: Record<string, string> = {};\n\n for (const tokenName of tokenNames) {\n // Priority 1: Custom tokens\n if (tokenName in customTokens) {\n resolved[tokenName] = customTokens[tokenName] ?? \"\";\n continue;\n }\n\n // Priority 2: Known token resolvers\n const resolver = TOKEN_RESOLVERS[tokenName];\n if (resolver) {\n resolved[tokenName] = resolver(frontmatter, slug);\n continue;\n }\n\n // Priority 3: Direct frontmatter value\n const fmValue = frontmatter[tokenName];\n if (typeof fmValue === \"string\") {\n resolved[tokenName] = slugifyValue(fmValue);\n continue;\n }\n if (typeof fmValue === \"number\") {\n resolved[tokenName] = fmValue.toString();\n continue;\n }\n\n // Priority 4: Fallback to empty string\n resolved[tokenName] = \"\";\n }\n\n return resolved;\n}\n\n/**\n * Check if a pattern is valid for content creation\n *\n * A pattern is valid if:\n * - It contains the {slug} token (required)\n * - It ends with .md or .mdx\n * - All tokens can be resolved\n *\n * @param pattern - Pattern template to validate\n * @returns Validation result with error message if invalid\n */\nexport function isValidPattern(pattern: string): {\n valid: boolean;\n error?: string;\n} {\n // Must contain slug token\n if (!pattern.includes(\"{slug}\")) {\n return { valid: false, error: \"Pattern must contain {slug} token\" };\n }\n\n // Must end with .md or .mdx\n if (!pattern.endsWith(\".md\") && !pattern.endsWith(\".mdx\")) {\n return { valid: false, error: \"Pattern must end with .md or .mdx\" };\n }\n\n // Check for unclosed tokens\n const unclosed = pattern.match(/\\{[^}]*$/);\n if (unclosed) {\n return { valid: false, error: \"Pattern contains unclosed token\" };\n }\n\n return { valid: true };\n}\n\n/**\n * Get list of all supported token names\n *\n * @returns Array of supported token names\n */\nexport function getSupportedTokens(): string[] {\n return Object.keys(TOKEN_RESOLVERS);\n}\n"],"mappings":"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","names":["readdir","existsSync","join","extname","relative","existsSync","readdir","join","relative","extname"]}