@nuasite/cms-marker 0.0.67 → 0.0.70
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/dist/types/collection-scanner.d.ts +6 -0
- package/dist/types/collection-scanner.d.ts.map +1 -0
- package/dist/types/dev-middleware.d.ts.map +1 -1
- package/dist/types/html-processor.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/manifest-writer.d.ts +10 -1
- package/dist/types/manifest-writer.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/types.d.ts +40 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/collection-scanner.ts +328 -0
- package/src/dev-middleware.ts +11 -9
- package/src/html-processor.ts +77 -54
- package/src/index.ts +12 -0
- package/src/manifest-writer.ts +24 -1
- package/src/types.ts +54 -0
package/dist/types/types.d.ts
CHANGED
|
@@ -184,6 +184,44 @@ export interface CollectionEntry {
|
|
|
184
184
|
/** ID of the wrapper element containing the rendered markdown */
|
|
185
185
|
wrapperId?: string;
|
|
186
186
|
}
|
|
187
|
+
/** Field types for collection schema inference */
|
|
188
|
+
export type FieldType = 'text' | 'textarea' | 'date' | 'boolean' | 'number' | 'image' | 'url' | 'select' | 'array' | 'object' | 'reference';
|
|
189
|
+
/** Definition of a single field in a collection's schema */
|
|
190
|
+
export interface FieldDefinition {
|
|
191
|
+
/** Field name as it appears in frontmatter */
|
|
192
|
+
name: string;
|
|
193
|
+
/** Inferred or specified field type */
|
|
194
|
+
type: FieldType;
|
|
195
|
+
/** Whether the field is required (present in all entries) */
|
|
196
|
+
required: boolean;
|
|
197
|
+
/** Default value for the field */
|
|
198
|
+
defaultValue?: unknown;
|
|
199
|
+
/** Options for 'select' type fields */
|
|
200
|
+
options?: string[];
|
|
201
|
+
/** Item type for 'array' fields */
|
|
202
|
+
itemType?: FieldType;
|
|
203
|
+
/** Nested fields for 'object' type */
|
|
204
|
+
fields?: FieldDefinition[];
|
|
205
|
+
/** Sample values seen across entries */
|
|
206
|
+
examples?: unknown[];
|
|
207
|
+
}
|
|
208
|
+
/** Definition of a content collection with inferred schema */
|
|
209
|
+
export interface CollectionDefinition {
|
|
210
|
+
/** Collection identifier (directory name) */
|
|
211
|
+
name: string;
|
|
212
|
+
/** Human-readable label for the collection */
|
|
213
|
+
label: string;
|
|
214
|
+
/** Path to the collection directory */
|
|
215
|
+
path: string;
|
|
216
|
+
/** Number of entries in the collection */
|
|
217
|
+
entryCount: number;
|
|
218
|
+
/** Inferred field definitions */
|
|
219
|
+
fields: FieldDefinition[];
|
|
220
|
+
/** Whether the collection has draft support */
|
|
221
|
+
supportsDraft?: boolean;
|
|
222
|
+
/** File extension used by entries */
|
|
223
|
+
fileExtension: 'md' | 'mdx';
|
|
224
|
+
}
|
|
187
225
|
/** Manifest metadata for versioning and conflict detection */
|
|
188
226
|
export interface ManifestMetadata {
|
|
189
227
|
/** Manifest schema version */
|
|
@@ -207,6 +245,8 @@ export interface CmsManifest {
|
|
|
207
245
|
componentDefinitions: Record<string, ComponentDefinition>;
|
|
208
246
|
/** Content collection entries indexed by "collectionName/slug" */
|
|
209
247
|
collections?: Record<string, CollectionEntry>;
|
|
248
|
+
/** Collection definitions with inferred schemas */
|
|
249
|
+
collectionDefinitions?: Record<string, CollectionDefinition>;
|
|
210
250
|
/** Available Tailwind colors from the project's config */
|
|
211
251
|
availableColors?: AvailableColors;
|
|
212
252
|
/** Available text styles from the project's Tailwind config */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAChC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC7B,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,uCAAuC;IACvC,aAAa,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,uDAAuD;AACvD,MAAM,WAAW,aAAa;IAC7B,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,eAAe;IACf,GAAG,EAAE,MAAM,CAAA;IACX,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uBAAuB;IACvB,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9C,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;AAED,yCAAyC;AACzC,MAAM,WAAW,kBAAkB;IAClC,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB;AAED,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC7B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,oDAAoD;AACpD,MAAM,WAAW,YAAY;IAC5B,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC1B;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC/B,6CAA6C;IAC7C,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB,mCAAmC;IACnC,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,EAAE,CAAA;CACtB;AAED,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC9B,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC3B;AAED,iDAAiD;AACjD,MAAM,WAAW,mBAAmB;IACnC,yDAAyD;IACzD,UAAU,EAAE,cAAc,EAAE,CAAA;IAC5B,4DAA4D;IAC5D,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,8DAA8D;IAC9D,cAAc,EAAE,cAAc,EAAE,CAAA;IAChC,8CAA8C;IAC9C,SAAS,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,CAAA;IACjF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAA;IAIpB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wDAAwD;IACxD,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,mEAAmE;IACnE,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,qCAAqC;IACrC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IAChC,gEAAgE;IAChE,YAAY,CAAC,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC/B,iDAAiD;IACjD,cAAc,EAAE,MAAM,CAAA;IACtB,mCAAmC;IACnC,cAAc,EAAE,MAAM,CAAA;IACtB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5D,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAA;IACrB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAChC,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzC;AAED,MAAM,WAAW,WAAW;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAC7C,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IACzD,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC7C,0DAA0D;IAC1D,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;CACzC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getProjectRoot } from './config'
|
|
4
|
+
import type { CollectionDefinition, FieldDefinition, FieldType } from './types'
|
|
5
|
+
|
|
6
|
+
/** Regex patterns for type inference */
|
|
7
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}/
|
|
8
|
+
const URL_PATTERN = /^(https?:\/\/|\/)/
|
|
9
|
+
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|avif)$/i
|
|
10
|
+
|
|
11
|
+
/** Maximum unique values before treating as free-form text instead of select */
|
|
12
|
+
const MAX_SELECT_OPTIONS = 10
|
|
13
|
+
|
|
14
|
+
/** Minimum length for textarea detection */
|
|
15
|
+
const TEXTAREA_MIN_LENGTH = 200
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Observed values for a single field across multiple files
|
|
19
|
+
*/
|
|
20
|
+
interface FieldObservation {
|
|
21
|
+
name: string
|
|
22
|
+
values: unknown[]
|
|
23
|
+
presentCount: number
|
|
24
|
+
totalEntries: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse YAML frontmatter from markdown content
|
|
29
|
+
*/
|
|
30
|
+
function parseFrontmatter(content: string): Record<string, unknown> | null {
|
|
31
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
32
|
+
if (!match?.[1]) return null
|
|
33
|
+
|
|
34
|
+
const frontmatter: Record<string, unknown> = {}
|
|
35
|
+
const lines = match[1].split('\n')
|
|
36
|
+
|
|
37
|
+
let currentKey: string | null = null
|
|
38
|
+
let currentArrayItems: unknown[] = []
|
|
39
|
+
let isInArray = false
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
// Check for array item (starts with "- ")
|
|
43
|
+
const arrayMatch = line.match(/^(\s*)- (.*)$/)
|
|
44
|
+
if (arrayMatch && isInArray && arrayMatch[2] !== undefined) {
|
|
45
|
+
const value = parseYamlValue(arrayMatch[2].trim())
|
|
46
|
+
currentArrayItems.push(value)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for key-value pair
|
|
51
|
+
const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)
|
|
52
|
+
if (kvMatch?.[1] && kvMatch[2] !== undefined) {
|
|
53
|
+
// Save previous array if any
|
|
54
|
+
if (isInArray && currentKey) {
|
|
55
|
+
frontmatter[currentKey] = currentArrayItems
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
currentKey = kvMatch[1]
|
|
59
|
+
const rawValue = kvMatch[2].trim()
|
|
60
|
+
|
|
61
|
+
// Check if starting an array
|
|
62
|
+
if (rawValue === '' || rawValue === '[]') {
|
|
63
|
+
isInArray = true
|
|
64
|
+
currentArrayItems = []
|
|
65
|
+
} else if (rawValue.startsWith('[') && rawValue.endsWith(']')) {
|
|
66
|
+
// Inline array like [a, b, c]
|
|
67
|
+
const items = rawValue.slice(1, -1).split(',').map(s => parseYamlValue(s.trim()))
|
|
68
|
+
frontmatter[currentKey] = items
|
|
69
|
+
isInArray = false
|
|
70
|
+
} else {
|
|
71
|
+
frontmatter[currentKey] = parseYamlValue(rawValue)
|
|
72
|
+
isInArray = false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Save final array if any
|
|
78
|
+
if (isInArray && currentKey) {
|
|
79
|
+
frontmatter[currentKey] = currentArrayItems
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return frontmatter
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse a YAML value into appropriate JS type
|
|
87
|
+
*/
|
|
88
|
+
function parseYamlValue(value: string): unknown {
|
|
89
|
+
// Handle quoted strings
|
|
90
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
91
|
+
return value.slice(1, -1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle booleans
|
|
95
|
+
if (value === 'true') return true
|
|
96
|
+
if (value === 'false') return false
|
|
97
|
+
|
|
98
|
+
// Handle null/empty
|
|
99
|
+
if (value === '' || value === 'null' || value === '~') return null
|
|
100
|
+
|
|
101
|
+
// Handle numbers
|
|
102
|
+
const num = Number(value)
|
|
103
|
+
if (!Number.isNaN(num) && value !== '') return num
|
|
104
|
+
|
|
105
|
+
return value
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Infer the field type from a value
|
|
110
|
+
*/
|
|
111
|
+
function inferFieldType(value: unknown, key: string): FieldType {
|
|
112
|
+
if (value === null || value === undefined) {
|
|
113
|
+
return 'text'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (typeof value === 'boolean') {
|
|
117
|
+
return 'boolean'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof value === 'number') {
|
|
121
|
+
return 'number'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
return 'array'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof value === 'object') {
|
|
129
|
+
return 'object'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof value === 'string') {
|
|
133
|
+
// Check for date pattern
|
|
134
|
+
if (DATE_PATTERN.test(value)) {
|
|
135
|
+
return 'date'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for image paths
|
|
139
|
+
if (
|
|
140
|
+
IMAGE_EXTENSIONS.test(value) || key.toLowerCase().includes('image') || key.toLowerCase().includes('thumbnail')
|
|
141
|
+
|| key.toLowerCase().includes('cover')
|
|
142
|
+
) {
|
|
143
|
+
return 'image'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check for URLs
|
|
147
|
+
if (URL_PATTERN.test(value)) {
|
|
148
|
+
return 'url'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for textarea (long text or contains newlines)
|
|
152
|
+
if (value.includes('\n') || value.length > TEXTAREA_MIN_LENGTH) {
|
|
153
|
+
return 'textarea'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return 'text'
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return 'text'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Merge field observations from multiple files to determine final field definition
|
|
164
|
+
*/
|
|
165
|
+
function mergeFieldObservations(observations: FieldObservation[]): FieldDefinition[] {
|
|
166
|
+
const fields: FieldDefinition[] = []
|
|
167
|
+
|
|
168
|
+
for (const obs of observations) {
|
|
169
|
+
const nonNullValues = obs.values.filter(v => v !== null && v !== undefined)
|
|
170
|
+
if (nonNullValues.length === 0) continue
|
|
171
|
+
|
|
172
|
+
// Determine type by consensus (most common inferred type)
|
|
173
|
+
const typeCounts = new Map<FieldType, number>()
|
|
174
|
+
for (const value of nonNullValues) {
|
|
175
|
+
const type = inferFieldType(value, obs.name)
|
|
176
|
+
typeCounts.set(type, (typeCounts.get(type) || 0) + 1)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Get most common type
|
|
180
|
+
let fieldType: FieldType = 'text'
|
|
181
|
+
let maxCount = 0
|
|
182
|
+
for (const [type, count] of typeCounts) {
|
|
183
|
+
if (count > maxCount) {
|
|
184
|
+
maxCount = count
|
|
185
|
+
fieldType = type
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const field: FieldDefinition = {
|
|
190
|
+
name: obs.name,
|
|
191
|
+
type: fieldType,
|
|
192
|
+
required: obs.presentCount === obs.totalEntries,
|
|
193
|
+
examples: nonNullValues.slice(0, 3),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// For text fields, check if we should treat as select (limited unique values)
|
|
197
|
+
if (fieldType === 'text') {
|
|
198
|
+
const uniqueValues = [...new Set(nonNullValues.map(v => String(v)))]
|
|
199
|
+
if (uniqueValues.length > 0 && uniqueValues.length <= MAX_SELECT_OPTIONS && nonNullValues.length >= 2) {
|
|
200
|
+
field.type = 'select'
|
|
201
|
+
field.options = uniqueValues.sort()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// For arrays, try to infer item type
|
|
206
|
+
if (fieldType === 'array') {
|
|
207
|
+
const allItems = nonNullValues.flatMap(v => (Array.isArray(v) ? v : []))
|
|
208
|
+
if (allItems.length > 0) {
|
|
209
|
+
const itemType = inferFieldType(allItems[0], obs.name)
|
|
210
|
+
field.itemType = itemType
|
|
211
|
+
|
|
212
|
+
// Check if array items should be select
|
|
213
|
+
if (itemType === 'text') {
|
|
214
|
+
const uniqueItems = [...new Set(allItems.map(v => String(v)))]
|
|
215
|
+
if (uniqueItems.length <= MAX_SELECT_OPTIONS * 2) {
|
|
216
|
+
field.options = uniqueItems.sort()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fields.push(field)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return fields
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Scan a single collection directory and infer its schema
|
|
230
|
+
*/
|
|
231
|
+
async function scanCollection(collectionPath: string, collectionName: string, contentDir: string): Promise<CollectionDefinition | null> {
|
|
232
|
+
try {
|
|
233
|
+
const entries = await fs.readdir(collectionPath, { withFileTypes: true })
|
|
234
|
+
const markdownFiles = entries.filter(e => e.isFile() && (e.name.endsWith('.md') || e.name.endsWith('.mdx')))
|
|
235
|
+
|
|
236
|
+
if (markdownFiles.length === 0) return null
|
|
237
|
+
|
|
238
|
+
// Determine file extension (prefer md, use mdx if that's all we have)
|
|
239
|
+
const hasMd = markdownFiles.some(f => f.name.endsWith('.md'))
|
|
240
|
+
const fileExtension: 'md' | 'mdx' = hasMd ? 'md' : 'mdx'
|
|
241
|
+
|
|
242
|
+
// Collect field observations across all files
|
|
243
|
+
const fieldMap = new Map<string, FieldObservation>()
|
|
244
|
+
let hasDraft = false
|
|
245
|
+
|
|
246
|
+
for (const file of markdownFiles) {
|
|
247
|
+
const filePath = path.join(collectionPath, file.name)
|
|
248
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
249
|
+
const frontmatter = parseFrontmatter(content)
|
|
250
|
+
|
|
251
|
+
if (!frontmatter) continue
|
|
252
|
+
|
|
253
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
254
|
+
if (key === 'draft' && typeof value === 'boolean') {
|
|
255
|
+
hasDraft = true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let obs = fieldMap.get(key)
|
|
259
|
+
if (!obs) {
|
|
260
|
+
obs = {
|
|
261
|
+
name: key,
|
|
262
|
+
values: [],
|
|
263
|
+
presentCount: 0,
|
|
264
|
+
totalEntries: markdownFiles.length,
|
|
265
|
+
}
|
|
266
|
+
fieldMap.set(key, obs)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
obs.values.push(value)
|
|
270
|
+
obs.presentCount++
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Update totalEntries for all observations
|
|
275
|
+
for (const obs of fieldMap.values()) {
|
|
276
|
+
obs.totalEntries = markdownFiles.length
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const fields = mergeFieldObservations(Array.from(fieldMap.values()))
|
|
280
|
+
|
|
281
|
+
// Generate a human-readable label
|
|
282
|
+
const label = collectionName
|
|
283
|
+
.replace(/[-_]/g, ' ')
|
|
284
|
+
.replace(/\b\w/g, c => c.toUpperCase())
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
name: collectionName,
|
|
288
|
+
label,
|
|
289
|
+
path: path.join(contentDir, collectionName),
|
|
290
|
+
entryCount: markdownFiles.length,
|
|
291
|
+
fields,
|
|
292
|
+
supportsDraft: hasDraft,
|
|
293
|
+
fileExtension,
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Scan all collections in the content directory
|
|
302
|
+
*/
|
|
303
|
+
export async function scanCollections(contentDir: string = 'src/content'): Promise<Record<string, CollectionDefinition>> {
|
|
304
|
+
const projectRoot = getProjectRoot()
|
|
305
|
+
const fullContentDir = path.isAbsolute(contentDir) ? contentDir : path.join(projectRoot, contentDir)
|
|
306
|
+
|
|
307
|
+
const collections: Record<string, CollectionDefinition> = {}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const entries = await fs.readdir(fullContentDir, { withFileTypes: true })
|
|
311
|
+
|
|
312
|
+
const scanPromises = entries
|
|
313
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('_') && !entry.name.startsWith('.'))
|
|
314
|
+
.map(async entry => {
|
|
315
|
+
const collectionPath = path.join(fullContentDir, entry.name)
|
|
316
|
+
const definition = await scanCollection(collectionPath, entry.name, contentDir)
|
|
317
|
+
if (definition) {
|
|
318
|
+
collections[entry.name] = definition
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
await Promise.all(scanPromises)
|
|
323
|
+
} catch {
|
|
324
|
+
// Content directory doesn't exist or isn't readable
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return collections
|
|
328
|
+
}
|
package/src/dev-middleware.ts
CHANGED
|
@@ -35,19 +35,21 @@ export function createDevMiddleware(
|
|
|
35
35
|
componentDefinitions: Record<string, ComponentDefinition>,
|
|
36
36
|
idCounter: { value: number },
|
|
37
37
|
) {
|
|
38
|
-
// Serve global CMS manifest (component definitions, available colors, and settings)
|
|
38
|
+
// Serve global CMS manifest (component definitions, available colors, collection definitions, and settings)
|
|
39
39
|
server.middlewares.use((req, res, next) => {
|
|
40
40
|
if (req.url === '/cms-manifest.json') {
|
|
41
41
|
res.setHeader('Content-Type', 'application/json')
|
|
42
42
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
const manifest: Record<string, unknown> = {
|
|
44
|
+
componentDefinitions,
|
|
45
|
+
availableColors: manifestWriter.getAvailableColors(),
|
|
46
|
+
availableTextStyles: manifestWriter.getAvailableTextStyles(),
|
|
47
|
+
}
|
|
48
|
+
const collectionDefs = manifestWriter.getCollectionDefinitions()
|
|
49
|
+
if (Object.keys(collectionDefs).length > 0) {
|
|
50
|
+
manifest.collectionDefinitions = collectionDefs
|
|
51
|
+
}
|
|
52
|
+
res.end(JSON.stringify(manifest, null, 2))
|
|
51
53
|
return
|
|
52
54
|
}
|
|
53
55
|
next()
|
package/src/html-processor.ts
CHANGED
|
@@ -280,61 +280,10 @@ export async function processHtml(
|
|
|
280
280
|
})
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
// Image detection pass: mark img elements for CMS image replacement
|
|
284
|
-
// Store image entries separately to add to manifest later
|
|
285
|
-
interface ImageEntry {
|
|
286
|
-
metadata: ImageMetadata
|
|
287
|
-
sourceFile?: string
|
|
288
|
-
sourceLine?: number
|
|
289
|
-
}
|
|
290
|
-
const imageEntries = new Map<string, ImageEntry>()
|
|
291
|
-
root.querySelectorAll('img').forEach((node) => {
|
|
292
|
-
// Skip if already marked
|
|
293
|
-
if (node.getAttribute(attributeName)) return
|
|
294
|
-
|
|
295
|
-
const src = node.getAttribute('src')
|
|
296
|
-
if (!src) return // Skip images without src
|
|
297
|
-
|
|
298
|
-
const id = getNextId()
|
|
299
|
-
node.setAttribute(attributeName, id)
|
|
300
|
-
node.setAttribute('data-cms-img', 'true')
|
|
301
|
-
|
|
302
|
-
// Try to get source location from the image itself or ancestors
|
|
303
|
-
let sourceFile: string | undefined
|
|
304
|
-
let sourceLine: number | undefined
|
|
305
|
-
let current: HTMLNode | null = node
|
|
306
|
-
while (current && !sourceFile) {
|
|
307
|
-
const file = current.getAttribute?.('data-astro-source-file')
|
|
308
|
-
const line = current.getAttribute?.('data-astro-source-loc') || current.getAttribute?.('data-astro-source-line')
|
|
309
|
-
if (file) {
|
|
310
|
-
sourceFile = file
|
|
311
|
-
if (line) {
|
|
312
|
-
const lineNum = parseInt(line.split(':')[0] ?? '1', 10)
|
|
313
|
-
if (!Number.isNaN(lineNum)) {
|
|
314
|
-
sourceLine = lineNum
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
current = current.parentNode as HTMLNode | null
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Build image metadata
|
|
322
|
-
const metadata: ImageMetadata = {
|
|
323
|
-
src,
|
|
324
|
-
alt: node.getAttribute('alt') || '',
|
|
325
|
-
srcSet: node.getAttribute('srcset') || undefined,
|
|
326
|
-
sizes: node.getAttribute('sizes') || undefined,
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Store image info for manifest
|
|
330
|
-
imageEntries.set(id, {
|
|
331
|
-
metadata,
|
|
332
|
-
sourceFile,
|
|
333
|
-
sourceLine,
|
|
334
|
-
})
|
|
335
|
-
})
|
|
336
|
-
|
|
337
283
|
// Collection wrapper detection pass: find the element that wraps markdown content
|
|
284
|
+
// This needs to run BEFORE image marking so we can skip images inside markdown
|
|
285
|
+
let markdownWrapperNode: HTMLNode | null = null
|
|
286
|
+
|
|
338
287
|
// Two strategies:
|
|
339
288
|
// 1. Dev mode: look for elements with data-astro-source-file containing children without it
|
|
340
289
|
// 2. Build mode: find element whose first child content matches the start of markdown body
|
|
@@ -375,6 +324,7 @@ export async function processHtml(
|
|
|
375
324
|
node.setAttribute(attributeName, id)
|
|
376
325
|
node.setAttribute('data-cms-markdown', 'true')
|
|
377
326
|
collectionWrapperId = id
|
|
327
|
+
markdownWrapperNode = node
|
|
378
328
|
foundWrapper = true
|
|
379
329
|
// Don't break - we want the deepest wrapper, so we'll overwrite
|
|
380
330
|
}
|
|
@@ -436,6 +386,7 @@ export async function processHtml(
|
|
|
436
386
|
best.node.setAttribute(attributeName, id)
|
|
437
387
|
best.node.setAttribute('data-cms-markdown', 'true')
|
|
438
388
|
collectionWrapperId = id
|
|
389
|
+
markdownWrapperNode = best.node
|
|
439
390
|
foundWrapper = true
|
|
440
391
|
}
|
|
441
392
|
}
|
|
@@ -443,6 +394,75 @@ export async function processHtml(
|
|
|
443
394
|
}
|
|
444
395
|
}
|
|
445
396
|
|
|
397
|
+
// Helper function to check if a node is inside the markdown wrapper
|
|
398
|
+
const isInsideMarkdownWrapper = (node: HTMLNode): boolean => {
|
|
399
|
+
if (!markdownWrapperNode) return false
|
|
400
|
+
let current = node.parentNode as HTMLNode | null
|
|
401
|
+
while (current) {
|
|
402
|
+
if (current === markdownWrapperNode) return true
|
|
403
|
+
current = current.parentNode as HTMLNode | null
|
|
404
|
+
}
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Image detection pass: mark img elements for CMS image replacement
|
|
409
|
+
// Store image entries separately to add to manifest later
|
|
410
|
+
// NOTE: Skip images inside markdown wrapper - they are edited via the markdown editor
|
|
411
|
+
interface ImageEntry {
|
|
412
|
+
metadata: ImageMetadata
|
|
413
|
+
sourceFile?: string
|
|
414
|
+
sourceLine?: number
|
|
415
|
+
}
|
|
416
|
+
const imageEntries = new Map<string, ImageEntry>()
|
|
417
|
+
root.querySelectorAll('img').forEach((node) => {
|
|
418
|
+
// Skip if already marked
|
|
419
|
+
if (node.getAttribute(attributeName)) return
|
|
420
|
+
|
|
421
|
+
// Skip images inside markdown wrapper - they are edited via the markdown editor
|
|
422
|
+
if (isInsideMarkdownWrapper(node)) return
|
|
423
|
+
|
|
424
|
+
const src = node.getAttribute('src')
|
|
425
|
+
if (!src) return // Skip images without src
|
|
426
|
+
|
|
427
|
+
const id = getNextId()
|
|
428
|
+
node.setAttribute(attributeName, id)
|
|
429
|
+
node.setAttribute('data-cms-img', 'true')
|
|
430
|
+
|
|
431
|
+
// Try to get source location from the image itself or ancestors
|
|
432
|
+
let sourceFile: string | undefined
|
|
433
|
+
let sourceLine: number | undefined
|
|
434
|
+
let current: HTMLNode | null = node
|
|
435
|
+
while (current && !sourceFile) {
|
|
436
|
+
const file = current.getAttribute?.('data-astro-source-file')
|
|
437
|
+
const line = current.getAttribute?.('data-astro-source-loc') || current.getAttribute?.('data-astro-source-line')
|
|
438
|
+
if (file) {
|
|
439
|
+
sourceFile = file
|
|
440
|
+
if (line) {
|
|
441
|
+
const lineNum = parseInt(line.split(':')[0] ?? '1', 10)
|
|
442
|
+
if (!Number.isNaN(lineNum)) {
|
|
443
|
+
sourceLine = lineNum
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
current = current.parentNode as HTMLNode | null
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Build image metadata
|
|
451
|
+
const metadata: ImageMetadata = {
|
|
452
|
+
src,
|
|
453
|
+
alt: node.getAttribute('alt') || '',
|
|
454
|
+
srcSet: node.getAttribute('srcset') || undefined,
|
|
455
|
+
sizes: node.getAttribute('sizes') || undefined,
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Store image info for manifest
|
|
459
|
+
imageEntries.set(id, {
|
|
460
|
+
metadata,
|
|
461
|
+
sourceFile,
|
|
462
|
+
sourceLine,
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
|
|
446
466
|
// Third pass: assign IDs to all qualifying text elements and extract source locations
|
|
447
467
|
root.querySelectorAll('*').forEach((node) => {
|
|
448
468
|
const tag = node.tagName?.toLowerCase?.() ?? ''
|
|
@@ -451,6 +471,9 @@ export async function processHtml(
|
|
|
451
471
|
if (includeTags && !includeTags.includes(tag)) return
|
|
452
472
|
if (node.getAttribute(attributeName)) return // Already marked
|
|
453
473
|
|
|
474
|
+
// Skip elements inside markdown wrapper - they are edited via the markdown editor
|
|
475
|
+
if (isInsideMarkdownWrapper(node)) return
|
|
476
|
+
|
|
454
477
|
// Skip inline text styling elements (strong, b, em, i, etc.)
|
|
455
478
|
// These should be part of their parent's text content, not separately editable
|
|
456
479
|
// Only apply when includeTags is null (all tags) - if specific tags are listed, respect them
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro'
|
|
2
2
|
import { processBuildOutput } from './build-processor'
|
|
3
|
+
import { scanCollections } from './collection-scanner'
|
|
3
4
|
import { ComponentRegistry } from './component-registry'
|
|
4
5
|
import { resetProjectRoot } from './config'
|
|
5
6
|
import { createDevMiddleware } from './dev-middleware'
|
|
@@ -67,6 +68,15 @@ export default function cmsMarker(options: CmsMarkerOptions = {}): AstroIntegrat
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// Scan content collections for schema inference
|
|
72
|
+
const collectionDefinitions = await scanCollections(contentDir)
|
|
73
|
+
manifestWriter.setCollectionDefinitions(collectionDefinitions)
|
|
74
|
+
|
|
75
|
+
const collectionCount = Object.keys(collectionDefinitions).length
|
|
76
|
+
if (collectionCount > 0) {
|
|
77
|
+
logger.info(`Found ${collectionCount} content collection(s)`)
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
// Create Vite plugin context
|
|
71
81
|
const pluginContext = {
|
|
72
82
|
manifestWriter,
|
|
@@ -110,6 +120,8 @@ export default function cmsMarker(options: CmsMarkerOptions = {}): AstroIntegrat
|
|
|
110
120
|
|
|
111
121
|
// Re-export config functions for testing
|
|
112
122
|
export { getProjectRoot, resetProjectRoot, setProjectRoot } from './config'
|
|
123
|
+
// Re-export collection scanner
|
|
124
|
+
export { scanCollections } from './collection-scanner'
|
|
113
125
|
// Re-export types for consumers
|
|
114
126
|
export type { CollectionInfo, MarkdownContent, SourceLocation, VariableReference } from './source-finder'
|
|
115
127
|
export { findCollectionSource, parseMarkdownContent } from './source-finder'
|