1
1
import { type FsDir , type FsFile , fsFile } from "@synstack/fs" ;
2
2
import { glob } from "@synstack/glob" ;
3
+ import { md } from "@synstack/markdown" ;
3
4
import { QueryEngine } from "@synstack/query" ;
5
+ import { t } from "@synstack/text" ;
4
6
import { z } from "zod/v4" ;
5
- import { getMarkdownEntries , NAME_SEPARATOR } from "./markdown-db.lib.ts" ;
7
+ import { getMarkdownEntries } from "./markdown-db.lib.ts" ;
6
8
7
9
type Globs = [ string , ...string [ ] ] ;
8
10
@@ -14,6 +16,8 @@ type Entry<CONFIG_SCHEMA extends BaseConfigSchema> = Awaited<
14
16
ReturnType < typeof getMarkdownEntries < CONFIG_SCHEMA > >
15
17
> [ number ] ;
16
18
19
+ export const NAME_SEPARATOR = "/" ;
20
+
17
21
export class MarkdownDb <
18
22
INPUT = unknown ,
19
23
CONFIG_SCHEMA extends BaseConfigSchema = BaseConfigSchema ,
@@ -96,6 +100,18 @@ export class MarkdownDb<
96
100
) ;
97
101
}
98
102
103
+ /**
104
+ * Filter the markdown files with glob patterns
105
+ */
106
+ public setGlobs ( ...globs : Globs ) {
107
+ return new MarkdownDb (
108
+ this . _cwd ,
109
+ this . _queryEngine ,
110
+ this . _configSchema ,
111
+ globs ,
112
+ ) ;
113
+ }
114
+
99
115
/**
100
116
* Check if the provided file is a markdown entry file in this db instance
101
117
* @param file - The file to check
@@ -113,10 +129,92 @@ export class MarkdownDb<
113
129
}
114
130
115
131
/**
116
- * Filter the markdown files with glob patterns
132
+ * Compute the entry id
133
+ * @param mdFile - The markdown file
134
+ * @returns The entry id
117
135
*/
118
- public setGlobs ( ...globs : Globs ) {
119
- return new MarkdownDb ( this . _cwd , this . _queryEngine , this . _configSchema , globs ) ;
136
+ public computeEntryId ( mdFile : FsFile ) {
137
+ const relativePath = mdFile . dir ( ) . relativePathFrom ( this . _cwd ) ;
138
+ const dirPath = relativePath . split ( "/" ) ;
139
+ const lastFolderName = dirPath . pop ( ) ;
140
+ let fileName = mdFile . fileNameWithoutExtension ( ) ;
141
+
142
+ // Remove numeric prefix (e.g., "0." from "0.buttons")
143
+ fileName = fileName . replace ( / ^ \d + \. / , "" ) ;
144
+
145
+ // Extract type suffix if present (e.g., "my-type" from "buttons.my-type")
146
+ let type : string | null = null ;
147
+ const typeMatch = fileName . match ( / ^ ( .+ ) \. ( .+ ) $ / ) ;
148
+ if ( typeMatch ) {
149
+ fileName = typeMatch [ 1 ] ;
150
+ type = typeMatch [ 2 ] ;
151
+ }
152
+
153
+ // If the last folder's name is the same as the file name, we can skip it
154
+ const nameParts =
155
+ lastFolderName === fileName
156
+ ? [ ...dirPath , fileName ]
157
+ : [ ...dirPath , lastFolderName , fileName ] ;
158
+
159
+ return {
160
+ name : nameParts . filter ( ( part ) => part !== "" ) . join ( NAME_SEPARATOR ) ,
161
+ type,
162
+ } ;
163
+ }
164
+
165
+ /**
166
+ * Parse the markdown file
167
+ * @param file - The file to parse
168
+ * @returns The parsed entry
169
+ */
170
+ public async fileToEntry ( file : FsFile | string ) {
171
+ const mdFile = fsFile ( file ) ;
172
+
173
+ // Check if the file is an entry file
174
+ if ( ! this . isEntryFile ( mdFile ) )
175
+ throw new Error ( t `
176
+ File ${ mdFile . path } is not an entry file in this MarkdownDb instance
177
+ - Cwd: ${ this . _cwd . path }
178
+ - Globs: ${ this . _globs . join ( ", " ) }
179
+ ` ) ;
180
+
181
+ // Read the file
182
+ const mdText = await mdFile . read . text ( ) ;
183
+
184
+ // Parse the header data
185
+ const headerData = await new Promise ( ( resolve ) =>
186
+ resolve ( md . getHeaderData ( mdText ) ) ,
187
+ ) . catch ( async ( err ) => {
188
+ throw new Error ( t `
189
+ Failed to read markdown file header
190
+ - File: ${ this . _cwd . relativePathTo ( mdFile ) }
191
+ - Error:
192
+ ${ err . message as string }
193
+ ` ) ;
194
+ } ) ;
195
+
196
+ // Validate the header data
197
+ const parsedData = this . _configSchema . safeParse ( headerData ) ;
198
+ if ( ! parsedData . success )
199
+ throw new Error ( t `
200
+ Failed to validate config for ${ mdFile . path } :
201
+ ${ z . prettifyError ( parsedData . error ) }
202
+ ` ) ;
203
+
204
+ // Compute the entry id
205
+ const { name, type } = this . computeEntryId ( mdFile ) ;
206
+
207
+ // Get the content
208
+ const content = md . getBody ( mdText ) . trim ( ) ;
209
+
210
+ // Return the entry
211
+ return {
212
+ $id : name ,
213
+ $type : type ,
214
+ $content : content . length > 0 ? content : null ,
215
+ $file : mdFile ,
216
+ ...parsedData . data ,
217
+ } ;
120
218
}
121
219
122
220
/**
0 commit comments