froggy-docs 1.1.1 → 1.1.3
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/.claude/settings.local.json +21 -0
- package/.github/workflows/froggy-docs.yml +59 -0
- package/ABOUT.md +99 -0
- package/MILESTONE.md +29 -17
- package/bin/froggy_docs.dart +3 -45
- package/frontend/lib/app.dart +487 -280
- package/frontend/lib/components/file_field.dart +75 -0
- package/frontend/web/app.js +224 -67
- package/frontend/web/froggy_docs.json +288 -4
- package/lib/froggy_docs.dart +1 -0
- package/lib/src/cli_runner.dart +123 -0
- package/lib/src/parser_engine.dart +351 -22
- package/lib/src/watcher_engine.dart +46 -14
- package/lib/src/web_server.dart +40 -17
- package/package.js +0 -0
- package/package.json +4 -4
- package/bin/froggy-docs +0 -0
|
@@ -36,6 +36,14 @@ class ParserEngine {
|
|
|
36
36
|
'options',
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
String _outputPath = 'frontend/web/froggy_docs.json';
|
|
40
|
+
|
|
41
|
+
void setOutputPath(String path) {
|
|
42
|
+
_outputPath = path;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
String get outputPath => _outputPath;
|
|
46
|
+
|
|
39
47
|
final Map<String, dynamic> _baseSpec = {
|
|
40
48
|
"openapi": "3.0.0",
|
|
41
49
|
"info": {
|
|
@@ -104,6 +112,37 @@ class ParserEngine {
|
|
|
104
112
|
}
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
dynamic _parseDefaultValue(String value, String? type) {
|
|
116
|
+
switch (type?.toString().toLowerCase()) {
|
|
117
|
+
case 'number':
|
|
118
|
+
case 'integer':
|
|
119
|
+
case 'int':
|
|
120
|
+
return num.tryParse(value) ?? value;
|
|
121
|
+
case 'boolean':
|
|
122
|
+
return value.toLowerCase() == 'true';
|
|
123
|
+
default:
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
void _attachResponseExample(
|
|
129
|
+
String code,
|
|
130
|
+
dynamic jsonData,
|
|
131
|
+
Map<String, Map<String, dynamic>> responseSchemas,
|
|
132
|
+
) {
|
|
133
|
+
final response = responseSchemas[code]!;
|
|
134
|
+
if (!response.containsKey('content')) {
|
|
135
|
+
response['content'] = <String, dynamic>{
|
|
136
|
+
"application/json": <String, dynamic>{
|
|
137
|
+
"schema": <String, dynamic>{"type": "object"},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
final contentMap = response['content'] as Map;
|
|
142
|
+
final appJsonMap = contentMap['application/json'] as Map;
|
|
143
|
+
appJsonMap['example'] = jsonData;
|
|
144
|
+
}
|
|
145
|
+
|
|
107
146
|
void parseFile(String filePath) {
|
|
108
147
|
_errors.clear();
|
|
109
148
|
|
|
@@ -132,8 +171,15 @@ class ParserEngine {
|
|
|
132
171
|
bool hasAuth = false;
|
|
133
172
|
List<String> currentTags = [];
|
|
134
173
|
List<Map<String, String>> requestBodyFields = [];
|
|
174
|
+
List<Map<String, String>> requestFileFields = [];
|
|
175
|
+
List<Map<String, dynamic>> queryParams = [];
|
|
176
|
+
List<Map<String, dynamic>> headerParams = [];
|
|
177
|
+
Map<String, Map<String, dynamic>> responseSchemas = {};
|
|
178
|
+
String? pendingResponseCode;
|
|
135
179
|
bool isReadingBodyJson = false;
|
|
180
|
+
bool isReadingResponseJson = false;
|
|
136
181
|
StringBuffer bodyJsonBuffer = StringBuffer();
|
|
182
|
+
StringBuffer responseJsonBuffer = StringBuffer();
|
|
137
183
|
int lineNumber = 0;
|
|
138
184
|
|
|
139
185
|
String inferType(dynamic value) {
|
|
@@ -146,6 +192,44 @@ class ParserEngine {
|
|
|
146
192
|
}
|
|
147
193
|
|
|
148
194
|
void flushEndpoint() {
|
|
195
|
+
// Defensively flush any pending JSON buffers (handles end-of-file cases).
|
|
196
|
+
if (isReadingBodyJson && bodyJsonBuffer.isNotEmpty) {
|
|
197
|
+
isReadingBodyJson = false;
|
|
198
|
+
try {
|
|
199
|
+
final jsonData = jsonDecode(bodyJsonBuffer.toString());
|
|
200
|
+
if (jsonData is Map) {
|
|
201
|
+
for (final entry in jsonData.entries) {
|
|
202
|
+
requestBodyFields.add({
|
|
203
|
+
'name': entry.key.toString(),
|
|
204
|
+
'type': inferType(entry.value),
|
|
205
|
+
'desc': 'Inferred from JSON',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
_errors.add('Invalid JSON in @body-json at line $lineNumber: $e');
|
|
211
|
+
}
|
|
212
|
+
bodyJsonBuffer.clear();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isReadingResponseJson &&
|
|
216
|
+
responseJsonBuffer.isNotEmpty &&
|
|
217
|
+
pendingResponseCode != null) {
|
|
218
|
+
isReadingResponseJson = false;
|
|
219
|
+
try {
|
|
220
|
+
final jsonData = jsonDecode(responseJsonBuffer.toString());
|
|
221
|
+
_attachResponseExample(
|
|
222
|
+
pendingResponseCode!,
|
|
223
|
+
jsonData,
|
|
224
|
+
responseSchemas,
|
|
225
|
+
);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
_errors
|
|
228
|
+
.add('Invalid JSON in @response-json at line $lineNumber: $e');
|
|
229
|
+
}
|
|
230
|
+
responseJsonBuffer.clear();
|
|
231
|
+
}
|
|
232
|
+
|
|
149
233
|
if (currentMethod != null) {
|
|
150
234
|
if (currentPath == null) {
|
|
151
235
|
_errors.add('Endpoint defined without path at line $lineNumber');
|
|
@@ -153,8 +237,15 @@ class ParserEngine {
|
|
|
153
237
|
currentPath = null;
|
|
154
238
|
description = '';
|
|
155
239
|
hasAuth = false;
|
|
240
|
+
currentTags.clear();
|
|
156
241
|
requestBodyFields.clear();
|
|
242
|
+
requestFileFields.clear();
|
|
243
|
+
queryParams.clear();
|
|
244
|
+
headerParams.clear();
|
|
245
|
+
responseSchemas.clear();
|
|
246
|
+
pendingResponseCode = null;
|
|
157
247
|
bodyJsonBuffer.clear();
|
|
248
|
+
responseJsonBuffer.clear();
|
|
158
249
|
return;
|
|
159
250
|
}
|
|
160
251
|
|
|
@@ -166,8 +257,15 @@ class ParserEngine {
|
|
|
166
257
|
currentPath = null;
|
|
167
258
|
description = '';
|
|
168
259
|
hasAuth = false;
|
|
260
|
+
currentTags.clear();
|
|
169
261
|
requestBodyFields.clear();
|
|
262
|
+
requestFileFields.clear();
|
|
263
|
+
queryParams.clear();
|
|
264
|
+
headerParams.clear();
|
|
265
|
+
responseSchemas.clear();
|
|
266
|
+
pendingResponseCode = null;
|
|
170
267
|
bodyJsonBuffer.clear();
|
|
268
|
+
responseJsonBuffer.clear();
|
|
171
269
|
return;
|
|
172
270
|
}
|
|
173
271
|
|
|
@@ -178,11 +276,16 @@ class ParserEngine {
|
|
|
178
276
|
fileEndpoints[path] = {};
|
|
179
277
|
}
|
|
180
278
|
|
|
279
|
+
final responses = <String, dynamic>{};
|
|
280
|
+
if (responseSchemas.isEmpty) {
|
|
281
|
+
responses['200'] = {"description": "Successful response"};
|
|
282
|
+
} else {
|
|
283
|
+
responses.addAll(responseSchemas);
|
|
284
|
+
}
|
|
285
|
+
|
|
181
286
|
Map<String, dynamic> endpointData = {
|
|
182
287
|
"summary": description.isNotEmpty ? description : "No description",
|
|
183
|
-
"responses":
|
|
184
|
-
"200": {"description": "Successful response"},
|
|
185
|
-
},
|
|
288
|
+
"responses": responses,
|
|
186
289
|
};
|
|
187
290
|
|
|
188
291
|
if (currentTags.isNotEmpty) {
|
|
@@ -195,25 +298,79 @@ class ParserEngine {
|
|
|
195
298
|
];
|
|
196
299
|
}
|
|
197
300
|
|
|
198
|
-
|
|
199
|
-
|
|
301
|
+
final parameters = <Map<String, dynamic>>[];
|
|
302
|
+
for (var qp in queryParams) {
|
|
303
|
+
final param = <String, dynamic>{
|
|
304
|
+
"name": qp['name'],
|
|
305
|
+
"in": "query",
|
|
306
|
+
"description": qp['desc'] ?? '',
|
|
307
|
+
"schema": <String, dynamic>{
|
|
308
|
+
"type": (qp['type'] ?? 'string').toString().toLowerCase(),
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
if (qp['default'] != null && (qp['default'] as String).isNotEmpty) {
|
|
312
|
+
param['schema']['default'] = _parseDefaultValue(
|
|
313
|
+
qp['default'] as String,
|
|
314
|
+
qp['type'],
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
parameters.add(param);
|
|
318
|
+
}
|
|
319
|
+
for (var hp in headerParams) {
|
|
320
|
+
parameters.add({
|
|
321
|
+
"name": hp['name'],
|
|
322
|
+
"in": "header",
|
|
323
|
+
"description": hp['desc'] ?? '',
|
|
324
|
+
"schema": {
|
|
325
|
+
"type": (hp['type'] ?? 'string').toString().toLowerCase(),
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (parameters.isNotEmpty) {
|
|
330
|
+
endpointData['parameters'] = parameters;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (requestBodyFields.isNotEmpty || requestFileFields.isNotEmpty) {
|
|
334
|
+
final properties = <String, dynamic>{};
|
|
200
335
|
for (var field in requestBodyFields) {
|
|
201
|
-
final fieldName = field['name']
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
336
|
+
final fieldName = field['name']!;
|
|
337
|
+
properties[fieldName] = {
|
|
338
|
+
"type": (field['type'] ?? 'string').toString().toLowerCase(),
|
|
339
|
+
"description": field['desc'] ?? '',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
for (var field in requestFileFields) {
|
|
343
|
+
final fieldName = field['name']!;
|
|
344
|
+
properties[fieldName] = {
|
|
345
|
+
"type": "string",
|
|
346
|
+
"format": "binary",
|
|
347
|
+
"description": field['desc'] ?? '',
|
|
348
|
+
};
|
|
208
349
|
}
|
|
209
350
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"
|
|
213
|
-
"
|
|
351
|
+
if (requestFileFields.isNotEmpty) {
|
|
352
|
+
endpointData['requestBody'] = {
|
|
353
|
+
"content": <String, dynamic>{
|
|
354
|
+
"multipart/form-data": <String, dynamic>{
|
|
355
|
+
"schema": <String, dynamic>{
|
|
356
|
+
"type": "object",
|
|
357
|
+
"properties": properties,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
214
360
|
},
|
|
215
|
-
}
|
|
216
|
-
}
|
|
361
|
+
};
|
|
362
|
+
} else {
|
|
363
|
+
endpointData['requestBody'] = {
|
|
364
|
+
"content": <String, dynamic>{
|
|
365
|
+
"application/json": <String, dynamic>{
|
|
366
|
+
"schema": <String, dynamic>{
|
|
367
|
+
"type": "object",
|
|
368
|
+
"properties": properties,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
217
374
|
}
|
|
218
375
|
|
|
219
376
|
fileEndpoints[path][method] = endpointData;
|
|
@@ -225,7 +382,13 @@ class ParserEngine {
|
|
|
225
382
|
hasAuth = false;
|
|
226
383
|
currentTags.clear();
|
|
227
384
|
requestBodyFields.clear();
|
|
385
|
+
requestFileFields.clear();
|
|
386
|
+
queryParams.clear();
|
|
387
|
+
headerParams.clear();
|
|
388
|
+
responseSchemas.clear();
|
|
389
|
+
pendingResponseCode = null;
|
|
228
390
|
bodyJsonBuffer.clear();
|
|
391
|
+
responseJsonBuffer.clear();
|
|
229
392
|
}
|
|
230
393
|
|
|
231
394
|
for (final l in lines) {
|
|
@@ -259,6 +422,29 @@ class ParserEngine {
|
|
|
259
422
|
}
|
|
260
423
|
}
|
|
261
424
|
|
|
425
|
+
if (isReadingResponseJson) {
|
|
426
|
+
if (!_isCommentLine(trimmed, commentPrefix)) {
|
|
427
|
+
isReadingResponseJson = false;
|
|
428
|
+
if (responseJsonBuffer.isNotEmpty && pendingResponseCode != null) {
|
|
429
|
+
try {
|
|
430
|
+
final jsonData = jsonDecode(responseJsonBuffer.toString());
|
|
431
|
+
_attachResponseExample(
|
|
432
|
+
pendingResponseCode!,
|
|
433
|
+
jsonData,
|
|
434
|
+
responseSchemas,
|
|
435
|
+
);
|
|
436
|
+
} catch (e) {
|
|
437
|
+
_errors.add('Invalid response JSON at line $lineNumber: $e');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
responseJsonBuffer.clear();
|
|
441
|
+
} else {
|
|
442
|
+
final commentText = _extractCommentText(trimmed, commentPrefix);
|
|
443
|
+
responseJsonBuffer.write(commentText);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
262
448
|
if (!_isCommentLine(trimmed, commentPrefix)) {
|
|
263
449
|
if (trimmed.isNotEmpty) {
|
|
264
450
|
flushEndpoint();
|
|
@@ -309,7 +495,24 @@ class ParserEngine {
|
|
|
309
495
|
'desc': fieldDesc,
|
|
310
496
|
});
|
|
311
497
|
} else {
|
|
312
|
-
_errors.add(
|
|
498
|
+
_errors.add(
|
|
499
|
+
'@body requires field name and type at line $lineNumber',
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
} else if (tag == '@file') {
|
|
503
|
+
if (parts.length >= 3) {
|
|
504
|
+
final fieldName = parts[1];
|
|
505
|
+
final fileType = parts[2];
|
|
506
|
+
final fieldDesc = parts.length > 3 ? parts.sublist(3).join(' ') : '';
|
|
507
|
+
requestFileFields.add({
|
|
508
|
+
'name': fieldName,
|
|
509
|
+
'type': fileType,
|
|
510
|
+
'desc': fieldDesc,
|
|
511
|
+
});
|
|
512
|
+
} else {
|
|
513
|
+
_errors.add(
|
|
514
|
+
'@file requires field name and type at line $lineNumber',
|
|
515
|
+
);
|
|
313
516
|
}
|
|
314
517
|
} else if (tag == '@body-file') {
|
|
315
518
|
if (parts.length >= 2) {
|
|
@@ -334,6 +537,92 @@ class ParserEngine {
|
|
|
334
537
|
} else if (tag == '@body-json') {
|
|
335
538
|
isReadingBodyJson = true;
|
|
336
539
|
bodyJsonBuffer.clear();
|
|
540
|
+
} else if (tag == '@response') {
|
|
541
|
+
if (parts.length >= 3) {
|
|
542
|
+
pendingResponseCode = parts[1];
|
|
543
|
+
final responseType = parts[2];
|
|
544
|
+
final responseDesc =
|
|
545
|
+
parts.length > 3 ? parts.sublist(3).join(' ') : '';
|
|
546
|
+
final lowerType = responseType.toLowerCase();
|
|
547
|
+
final schemaType = (lowerType == 'array') ? 'array' : 'object';
|
|
548
|
+
responseSchemas[pendingResponseCode!] = {
|
|
549
|
+
"description": responseDesc,
|
|
550
|
+
"content": <String, dynamic>{
|
|
551
|
+
"application/json": <String, dynamic>{
|
|
552
|
+
"schema": <String, dynamic>{"type": schemaType},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
} else {
|
|
557
|
+
_errors.add(
|
|
558
|
+
'@response requires status code and type at line $lineNumber',
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
} else if (tag == '@response-json') {
|
|
562
|
+
if (pendingResponseCode != null &&
|
|
563
|
+
responseSchemas.containsKey(pendingResponseCode)) {
|
|
564
|
+
final rest = commentText.replaceFirst('@response-json', '').trim();
|
|
565
|
+
if (rest.isNotEmpty) {
|
|
566
|
+
try {
|
|
567
|
+
final jsonData = jsonDecode(rest);
|
|
568
|
+
_attachResponseExample(
|
|
569
|
+
pendingResponseCode!,
|
|
570
|
+
jsonData,
|
|
571
|
+
responseSchemas,
|
|
572
|
+
);
|
|
573
|
+
} catch (_) {
|
|
574
|
+
isReadingResponseJson = true;
|
|
575
|
+
responseJsonBuffer.clear();
|
|
576
|
+
responseJsonBuffer.write(rest);
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
isReadingResponseJson = true;
|
|
580
|
+
responseJsonBuffer.clear();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} else if (tag == '@query') {
|
|
584
|
+
if (parts.length >= 3) {
|
|
585
|
+
final name = parts[1];
|
|
586
|
+
final type = parts[2];
|
|
587
|
+
String desc = '';
|
|
588
|
+
String? defaultValue;
|
|
589
|
+
|
|
590
|
+
final rest = parts.length > 3 ? parts.sublist(3).join(' ') : '';
|
|
591
|
+
final defaultMatch =
|
|
592
|
+
RegExp(r'\(default:\s*(.+?)\)').firstMatch(rest);
|
|
593
|
+
if (defaultMatch != null) {
|
|
594
|
+
defaultValue = defaultMatch.group(1);
|
|
595
|
+
desc = rest.replaceFirst(defaultMatch.group(0)!, '').trim();
|
|
596
|
+
} else {
|
|
597
|
+
desc = rest;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
queryParams.add({
|
|
601
|
+
'name': name,
|
|
602
|
+
'type': type,
|
|
603
|
+
'desc': desc,
|
|
604
|
+
'default': defaultValue,
|
|
605
|
+
});
|
|
606
|
+
} else {
|
|
607
|
+
_errors.add(
|
|
608
|
+
'@query requires name and type at line $lineNumber',
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
} else if (tag == '@header') {
|
|
612
|
+
if (parts.length >= 3) {
|
|
613
|
+
final name = parts[1];
|
|
614
|
+
final type = parts[2];
|
|
615
|
+
final desc = parts.length > 3 ? parts.sublist(3).join(' ') : '';
|
|
616
|
+
headerParams.add({
|
|
617
|
+
'name': name,
|
|
618
|
+
'type': type,
|
|
619
|
+
'desc': desc,
|
|
620
|
+
});
|
|
621
|
+
} else {
|
|
622
|
+
_errors.add(
|
|
623
|
+
'@header requires name and type at line $lineNumber',
|
|
624
|
+
);
|
|
625
|
+
}
|
|
337
626
|
}
|
|
338
627
|
}
|
|
339
628
|
|
|
@@ -341,7 +630,7 @@ class ParserEngine {
|
|
|
341
630
|
|
|
342
631
|
if (_errors.isNotEmpty) {
|
|
343
632
|
for (final error in _errors) {
|
|
344
|
-
print('⚠️
|
|
633
|
+
print('⚠️ $error');
|
|
345
634
|
}
|
|
346
635
|
}
|
|
347
636
|
|
|
@@ -370,12 +659,11 @@ class ParserEngine {
|
|
|
370
659
|
|
|
371
660
|
_baseSpec['paths'] = fullPaths;
|
|
372
661
|
|
|
373
|
-
// Add tags if any
|
|
374
662
|
if (_tags.isNotEmpty) {
|
|
375
663
|
_baseSpec['tags'] = _tags.map((t) => {"name": t}).toList();
|
|
376
664
|
}
|
|
377
665
|
|
|
378
|
-
final swaggerFile = File(
|
|
666
|
+
final swaggerFile = File(_outputPath);
|
|
379
667
|
if (!swaggerFile.parent.existsSync()) {
|
|
380
668
|
swaggerFile.parent.createSync(recursive: true);
|
|
381
669
|
}
|
|
@@ -387,4 +675,45 @@ class ParserEngine {
|
|
|
387
675
|
'🐸 FroggyDocs: Updated documentation. Total paths: ${fullPaths.length}',
|
|
388
676
|
);
|
|
389
677
|
}
|
|
678
|
+
|
|
679
|
+
Map<String, dynamic> generateSpec() {
|
|
680
|
+
final Map<String, dynamic> fullPaths = {};
|
|
681
|
+
for (final fileMap in _fileRegistry.values) {
|
|
682
|
+
for (final entry in fileMap.entries) {
|
|
683
|
+
final path = entry.key;
|
|
684
|
+
final methods = entry.value;
|
|
685
|
+
if (fullPaths[path] == null) {
|
|
686
|
+
fullPaths[path] = {};
|
|
687
|
+
}
|
|
688
|
+
(fullPaths[path] as Map).addAll(methods);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
final spec = Map<String, dynamic>.from(_baseSpec);
|
|
692
|
+
spec['paths'] = fullPaths;
|
|
693
|
+
if (_tags.isNotEmpty) {
|
|
694
|
+
spec['tags'] = _tags.map((t) => {"name": t}).toList();
|
|
695
|
+
}
|
|
696
|
+
return spec;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
void clearRegistry() {
|
|
700
|
+
_fileRegistry.clear();
|
|
701
|
+
_tags.clear();
|
|
702
|
+
_errors.clear();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
int get pathCount {
|
|
706
|
+
final Map<String, dynamic> fullPaths = {};
|
|
707
|
+
for (final fileMap in _fileRegistry.values) {
|
|
708
|
+
for (final entry in fileMap.entries) {
|
|
709
|
+
final path = entry.key;
|
|
710
|
+
final methods = entry.value;
|
|
711
|
+
if (fullPaths[path] == null) {
|
|
712
|
+
fullPaths[path] = {};
|
|
713
|
+
}
|
|
714
|
+
(fullPaths[path] as Map).addAll(methods);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return fullPaths.length;
|
|
718
|
+
}
|
|
390
719
|
}
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import 'dart:io';
|
|
2
|
+
import 'dart:async';
|
|
2
3
|
import 'package:watcher/watcher.dart';
|
|
3
4
|
import 'package:path/path.dart' as p;
|
|
4
5
|
import 'parser_engine.dart';
|
|
5
6
|
|
|
6
7
|
class WatcherEngine {
|
|
7
|
-
final ParserEngine _parser
|
|
8
|
+
final ParserEngine _parser;
|
|
9
|
+
Timer? _debounceTimer;
|
|
10
|
+
final Duration _debounceDuration;
|
|
11
|
+
final String? _ignorePattern;
|
|
12
|
+
|
|
13
|
+
WatcherEngine({
|
|
14
|
+
String outputPath = 'frontend/web/froggy_docs.json',
|
|
15
|
+
String ignorePattern = '',
|
|
16
|
+
Duration debounceDuration = const Duration(milliseconds: 300),
|
|
17
|
+
}) : _parser = ParserEngine(),
|
|
18
|
+
_debounceDuration = debounceDuration,
|
|
19
|
+
_ignorePattern = ignorePattern.isEmpty ? null : ignorePattern {
|
|
20
|
+
_parser.setOutputPath(outputPath);
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
void startWatching(String directoryPath) {
|
|
10
24
|
print('🚀 Initializing FroggyDocs scan...');
|
|
@@ -28,31 +42,49 @@ class WatcherEngine {
|
|
|
28
42
|
}
|
|
29
43
|
});
|
|
30
44
|
} catch (e) {
|
|
31
|
-
print('⚠️
|
|
45
|
+
print('⚠️ Warning during initial scan: $e');
|
|
32
46
|
}
|
|
33
47
|
print('✅ Initial scan complete.\n');
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
bool _shouldIgnore(String path) {
|
|
37
51
|
final normalizedPath = path.replaceAll('\\', '/');
|
|
38
|
-
|
|
52
|
+
if (normalizedPath.contains('/.dart_tool/') ||
|
|
39
53
|
normalizedPath.contains('/frontend/') ||
|
|
40
54
|
normalizedPath.contains('/node_modules/') ||
|
|
41
55
|
normalizedPath.contains('/.git/') ||
|
|
42
56
|
normalizedPath.endsWith('.json') ||
|
|
43
|
-
normalizedPath.endsWith('.md')
|
|
57
|
+
normalizedPath.endsWith('.md')) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (_ignorePattern != null) {
|
|
62
|
+
final regexPattern = _ignorePattern
|
|
63
|
+
.replaceAll('.', r'\.')
|
|
64
|
+
.replaceAll('*', '.*')
|
|
65
|
+
.replaceAll('?', '.');
|
|
66
|
+
if (RegExp(regexPattern).hasMatch(normalizedPath)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false;
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
void _handleFileChange(WatchEvent event) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
_debounceTimer?.cancel();
|
|
76
|
+
|
|
77
|
+
_debounceTimer = Timer(_debounceDuration, () {
|
|
78
|
+
switch (event.type) {
|
|
79
|
+
case ChangeType.ADD:
|
|
80
|
+
case ChangeType.MODIFY:
|
|
81
|
+
_parser.parseFile(event.path);
|
|
82
|
+
break;
|
|
83
|
+
case ChangeType.REMOVE:
|
|
84
|
+
print('🗑️ Removing documentation for: ${p.basename(event.path)}');
|
|
85
|
+
_parser.removeFile(event.path);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
57
89
|
}
|
|
58
90
|
}
|