@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,453 @@
1
+ /**
2
+ * @fileoverview Schema auto-detection for content collections
3
+ *
4
+ * This module analyzes frontmatter from sample content files to automatically
5
+ * infer the schema (field types, required status, enums, etc.).
6
+ *
7
+ * ## Detection Process:
8
+ * 1. Read sample files from the collection (up to 20)
9
+ * 2. Parse frontmatter from each file
10
+ * 3. Analyze field patterns across all samples
11
+ * 4. Infer field types and constraints
12
+ * 5. Generate schema definition
13
+ *
14
+ * ## Detected Types:
15
+ * - string: Plain text values
16
+ * - number: Numeric values
17
+ * - boolean: True/false values
18
+ * - date: ISO date strings or Date objects
19
+ * - array: Arrays (with item type detection)
20
+ * - image: Paths ending with image extensions
21
+ *
22
+ * @module @writenex/astro/discovery/schema
23
+ */
24
+
25
+ import type { CollectionSchema, FieldType, SchemaField } from "@/types";
26
+ import { readCollection } from "@/filesystem/reader";
27
+
28
+ /**
29
+ * Maximum number of files to sample for schema detection
30
+ */
31
+ const MAX_SAMPLE_FILES = 20;
32
+
33
+ /**
34
+ * Minimum presence ratio to consider a field required
35
+ * (field must appear in at least 90% of files)
36
+ */
37
+ const REQUIRED_THRESHOLD = 0.9;
38
+
39
+ /**
40
+ * Maximum unique values to consider a field as enum
41
+ */
42
+ const ENUM_MAX_VALUES = 10;
43
+
44
+ /**
45
+ * Minimum ratio of unique values to total to NOT be an enum
46
+ * (if uniqueValues / total < 0.3, it's likely an enum)
47
+ */
48
+ const ENUM_RATIO_THRESHOLD = 0.3;
49
+
50
+ /**
51
+ * Image file extensions for detection
52
+ */
53
+ const IMAGE_EXTENSIONS = [
54
+ ".jpg",
55
+ ".jpeg",
56
+ ".png",
57
+ ".gif",
58
+ ".webp",
59
+ ".avif",
60
+ ".svg",
61
+ ];
62
+
63
+ /**
64
+ * Field analysis data collected from samples
65
+ */
66
+ interface FieldAnalysis {
67
+ /** Number of files where this field appears */
68
+ presentCount: number;
69
+ /** Detected types for this field across samples */
70
+ types: Set<string>;
71
+ /** Sample values for enum detection */
72
+ values: unknown[];
73
+ /** Whether values look like image paths */
74
+ hasImagePaths: boolean;
75
+ /** Whether values look like dates */
76
+ hasDateValues: boolean;
77
+ /** For arrays, analysis of item types */
78
+ arrayItemTypes: Set<string>;
79
+ }
80
+
81
+ /**
82
+ * Result of schema detection
83
+ */
84
+ export interface SchemaDetectionResult {
85
+ /** The detected schema */
86
+ schema: CollectionSchema;
87
+ /** Number of files analyzed */
88
+ samplesAnalyzed: number;
89
+ /** Confidence score (0-1) based on sample consistency */
90
+ confidence: number;
91
+ /** Fields that had inconsistent types across samples */
92
+ warnings: string[];
93
+ }
94
+
95
+ /**
96
+ * Check if a string looks like an image path
97
+ *
98
+ * @param value - Value to check
99
+ * @returns True if it looks like an image path
100
+ */
101
+ function isImagePath(value: unknown): boolean {
102
+ if (typeof value !== "string") return false;
103
+
104
+ const lowered = value.toLowerCase();
105
+ return IMAGE_EXTENSIONS.some((ext) => lowered.endsWith(ext));
106
+ }
107
+
108
+ /**
109
+ * Check if a value looks like a date
110
+ *
111
+ * @param value - Value to check
112
+ * @returns True if it looks like a date
113
+ */
114
+ function isDateValue(value: unknown): boolean {
115
+ // Already a Date object
116
+ if (value instanceof Date) return true;
117
+
118
+ // ISO date string (YYYY-MM-DD or full ISO)
119
+ if (typeof value === "string") {
120
+ // Full ISO format
121
+ if (/^\d{4}-\d{2}-\d{2}(T|\s)/.test(value)) return true;
122
+ // Simple date format
123
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return true;
124
+ }
125
+
126
+ return false;
127
+ }
128
+
129
+ /**
130
+ * Detect the JavaScript type of a value
131
+ *
132
+ * @param value - Value to analyze
133
+ * @returns Detected type string
134
+ */
135
+ function detectValueType(value: unknown): string {
136
+ if (value === null || value === undefined) return "null";
137
+ if (typeof value === "boolean") return "boolean";
138
+ if (typeof value === "number") return "number";
139
+ if (typeof value === "string") return "string";
140
+ if (Array.isArray(value)) return "array";
141
+ if (value instanceof Date) return "date";
142
+ if (typeof value === "object") return "object";
143
+ return "unknown";
144
+ }
145
+
146
+ /**
147
+ * Convert detected type to schema field type
148
+ *
149
+ * @param analysis - Field analysis data
150
+ * @returns The appropriate FieldType
151
+ */
152
+ function inferFieldType(analysis: FieldAnalysis): FieldType {
153
+ // If it has image paths, it's an image field
154
+ if (analysis.hasImagePaths) return "image";
155
+
156
+ // If it has date values, it's a date field
157
+ if (analysis.hasDateValues) return "date";
158
+
159
+ // Check detected types (excluding null)
160
+ const nonNullTypes = new Set([...analysis.types].filter((t) => t !== "null"));
161
+
162
+ // Single type is easy
163
+ if (nonNullTypes.size === 1) {
164
+ const type = [...nonNullTypes][0];
165
+ switch (type) {
166
+ case "boolean":
167
+ return "boolean";
168
+ case "number":
169
+ return "number";
170
+ case "array":
171
+ return "array";
172
+ case "object":
173
+ return "object";
174
+ case "date":
175
+ return "date";
176
+ default:
177
+ return "string";
178
+ }
179
+ }
180
+
181
+ // Mixed types - default to string (most flexible)
182
+ return "string";
183
+ }
184
+
185
+ /**
186
+ * Detect if a field should be treated as an enum
187
+ *
188
+ * @param values - All values seen for this field
189
+ * @param totalSamples - Total number of samples
190
+ * @returns Array of enum values, or undefined if not an enum
191
+ */
192
+ function detectEnum(
193
+ values: unknown[],
194
+ totalSamples: number
195
+ ): string[] | undefined {
196
+ // Filter to string values only
197
+ const stringValues = values.filter(
198
+ (v): v is string => typeof v === "string" && v.length > 0
199
+ );
200
+
201
+ if (stringValues.length === 0) return undefined;
202
+
203
+ // Get unique values
204
+ const uniqueValues = [...new Set(stringValues)];
205
+
206
+ // Check if it's a good candidate for enum
207
+ if (uniqueValues.length > ENUM_MAX_VALUES) return undefined;
208
+
209
+ // Check ratio of unique to total
210
+ const ratio = uniqueValues.length / totalSamples;
211
+ if (ratio > ENUM_RATIO_THRESHOLD) return undefined;
212
+
213
+ // Must have at least 2 unique values and appear multiple times
214
+ if (uniqueValues.length < 2) return undefined;
215
+ if (stringValues.length < totalSamples * 0.5) return undefined;
216
+
217
+ return uniqueValues.sort();
218
+ }
219
+
220
+ /**
221
+ * Detect item type for array fields
222
+ *
223
+ * @param analysis - Field analysis data
224
+ * @returns The detected item type, or undefined
225
+ */
226
+ function detectArrayItemType(analysis: FieldAnalysis): string | undefined {
227
+ if (analysis.arrayItemTypes.size === 0) return undefined;
228
+
229
+ // Filter out null
230
+ const types = [...analysis.arrayItemTypes].filter((t) => t !== "null");
231
+
232
+ if (types.length === 0) return undefined;
233
+ if (types.length === 1) return types[0];
234
+
235
+ // Mixed types - default to string
236
+ return "string";
237
+ }
238
+
239
+ /**
240
+ * Analyze frontmatter from content items to detect schema
241
+ *
242
+ * @param collectionPath - Absolute path to the collection directory
243
+ * @returns Schema detection result
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * const result = await detectSchema('/project/src/content/blog');
248
+ * console.log(result.schema);
249
+ * // {
250
+ * // title: { type: 'string', required: true },
251
+ * // pubDate: { type: 'date', required: true },
252
+ * // draft: { type: 'boolean', required: false, default: false },
253
+ * // tags: { type: 'array', required: false, items: 'string' },
254
+ * // }
255
+ * ```
256
+ */
257
+ export async function detectSchema(
258
+ collectionPath: string
259
+ ): Promise<SchemaDetectionResult> {
260
+ const warnings: string[] = [];
261
+
262
+ // Read sample content files
263
+ const items = await readCollection(collectionPath, {
264
+ includeDrafts: true,
265
+ });
266
+
267
+ // Limit to max samples
268
+ const samples = items.slice(0, MAX_SAMPLE_FILES);
269
+
270
+ if (samples.length === 0) {
271
+ return {
272
+ schema: {},
273
+ samplesAnalyzed: 0,
274
+ confidence: 0,
275
+ warnings: ["No content files found in collection"],
276
+ };
277
+ }
278
+
279
+ // Analyze each field across all samples
280
+ const fieldAnalyses = new Map<string, FieldAnalysis>();
281
+
282
+ for (const item of samples) {
283
+ for (const [fieldName, value] of Object.entries(item.frontmatter)) {
284
+ // Get or create field analysis
285
+ let analysis = fieldAnalyses.get(fieldName);
286
+ if (!analysis) {
287
+ analysis = {
288
+ presentCount: 0,
289
+ types: new Set(),
290
+ values: [],
291
+ hasImagePaths: false,
292
+ hasDateValues: false,
293
+ arrayItemTypes: new Set(),
294
+ };
295
+ fieldAnalyses.set(fieldName, analysis);
296
+ }
297
+
298
+ // Update analysis
299
+ analysis.presentCount++;
300
+ analysis.types.add(detectValueType(value));
301
+ analysis.values.push(value);
302
+
303
+ // Check for special types
304
+ if (isImagePath(value)) {
305
+ analysis.hasImagePaths = true;
306
+ }
307
+ if (isDateValue(value)) {
308
+ analysis.hasDateValues = true;
309
+ }
310
+
311
+ // Analyze array items
312
+ if (Array.isArray(value)) {
313
+ for (const item of value) {
314
+ analysis.arrayItemTypes.add(detectValueType(item));
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ // Generate schema from analysis
321
+ const schema: CollectionSchema = {};
322
+ const totalSamples = samples.length;
323
+
324
+ for (const [fieldName, analysis] of fieldAnalyses) {
325
+ const fieldType = inferFieldType(analysis);
326
+ const isRequired =
327
+ analysis.presentCount / totalSamples >= REQUIRED_THRESHOLD;
328
+
329
+ const field: SchemaField = {
330
+ type: fieldType,
331
+ required: isRequired,
332
+ };
333
+
334
+ // Add array item type if applicable
335
+ if (fieldType === "array") {
336
+ const itemType = detectArrayItemType(analysis);
337
+ if (itemType) {
338
+ field.items = itemType;
339
+ }
340
+ }
341
+
342
+ // Detect enum for string fields
343
+ if (fieldType === "string") {
344
+ const enumValues = detectEnum(analysis.values, totalSamples);
345
+ if (enumValues) {
346
+ // Store enum values in the field
347
+ // Note: We use 'default' to store enum options since SchemaField
348
+ // doesn't have an 'enum' property - this can be enhanced later
349
+ field.description = `Options: ${enumValues.join(", ")}`;
350
+ }
351
+ }
352
+
353
+ // Detect default value for boolean fields
354
+ if (fieldType === "boolean") {
355
+ const boolValues = analysis.values.filter(
356
+ (v): v is boolean => typeof v === "boolean"
357
+ );
358
+ if (boolValues.length > 0) {
359
+ // Use most common value as default
360
+ const trueCount = boolValues.filter((v) => v === true).length;
361
+ const falseCount = boolValues.filter((v) => v === false).length;
362
+ field.default = trueCount > falseCount ? true : false;
363
+ }
364
+ }
365
+
366
+ // Check for type inconsistencies
367
+ const nonNullTypes = [...analysis.types].filter((t) => t !== "null");
368
+ if (nonNullTypes.length > 1) {
369
+ warnings.push(
370
+ `Field "${fieldName}" has inconsistent types: ${nonNullTypes.join(", ")}`
371
+ );
372
+ }
373
+
374
+ schema[fieldName] = field;
375
+ }
376
+
377
+ // Calculate confidence based on consistency
378
+ const inconsistentFields = warnings.filter((w) =>
379
+ w.includes("inconsistent")
380
+ ).length;
381
+ const confidence = Math.max(
382
+ 0,
383
+ 1 - inconsistentFields / Math.max(1, fieldAnalyses.size)
384
+ );
385
+
386
+ return {
387
+ schema,
388
+ samplesAnalyzed: totalSamples,
389
+ confidence,
390
+ warnings,
391
+ };
392
+ }
393
+
394
+ /**
395
+ * Merge detected schema with user-provided schema
396
+ *
397
+ * User schema takes precedence over detected schema.
398
+ *
399
+ * @param detected - Auto-detected schema
400
+ * @param userSchema - User-provided schema overrides
401
+ * @returns Merged schema
402
+ */
403
+ export function mergeSchema(
404
+ detected: CollectionSchema,
405
+ userSchema?: CollectionSchema
406
+ ): CollectionSchema {
407
+ if (!userSchema) return detected;
408
+
409
+ const merged: CollectionSchema = { ...detected };
410
+
411
+ for (const [fieldName, userField] of Object.entries(userSchema)) {
412
+ merged[fieldName] = {
413
+ ...detected[fieldName],
414
+ ...userField,
415
+ };
416
+ }
417
+
418
+ return merged;
419
+ }
420
+
421
+ /**
422
+ * Convert schema to a human-readable description
423
+ *
424
+ * @param schema - The schema to describe
425
+ * @returns Human-readable description
426
+ */
427
+ export function describeSchema(schema: CollectionSchema): string {
428
+ const lines: string[] = [];
429
+
430
+ for (const [fieldName, field] of Object.entries(schema)) {
431
+ let desc = `- ${fieldName}: ${field.type}`;
432
+
433
+ if (field.required) {
434
+ desc += " (required)";
435
+ }
436
+
437
+ if (field.items) {
438
+ desc += ` of ${field.items}`;
439
+ }
440
+
441
+ if (field.default !== undefined) {
442
+ desc += ` [default: ${JSON.stringify(field.default)}]`;
443
+ }
444
+
445
+ if (field.description) {
446
+ desc += ` - ${field.description}`;
447
+ }
448
+
449
+ lines.push(desc);
450
+ }
451
+
452
+ return lines.join("\n");
453
+ }