apidocly 1.0.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.
@@ -0,0 +1,334 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { minifyCSS, minifyAndObfuscateJSAsync, minifyJSON } = require('../utils/minifier');
5
+
6
+ const TEMPLATE_DIR = path.join(__dirname, '../../template');
7
+
8
+ // Encryption settings
9
+ const ENCRYPTION_ALGORITHM = 'aes-256-gcm';
10
+ const PBKDF2_ITERATIONS = 100000;
11
+ const SALT_LENGTH = 16;
12
+ const IV_LENGTH = 12;
13
+ const KEY_LENGTH = 32;
14
+ const AUTH_TAG_LENGTH = 16;
15
+
16
+ async function generateDocs(apiData, config, outputPath, options = {}) {
17
+ const { singleFile, verbose } = options;
18
+
19
+ if (!fs.existsSync(outputPath)) {
20
+ fs.mkdirSync(outputPath, { recursive: true });
21
+ }
22
+
23
+ const password = config.password || null;
24
+ let authData = null;
25
+
26
+ if (password) {
27
+ const isEncrypted = !password.startsWith('sha256:');
28
+ authData = {
29
+ hash: hashPassword(password),
30
+ message: config.passwordMessage || 'Enter password to view API documentation',
31
+ encrypted: isEncrypted,
32
+ // Encryption params for client-side decryption
33
+ pbkdf2Iterations: PBKDF2_ITERATIONS,
34
+ saltLength: SALT_LENGTH,
35
+ ivLength: IV_LENGTH,
36
+ authTagLength: AUTH_TAG_LENGTH
37
+ };
38
+ }
39
+
40
+ // Always save version history for version switching feature
41
+ saveVersionHistory(apiData, config, outputPath, verbose);
42
+
43
+ generateApiDataFile(apiData, outputPath, password, authData);
44
+
45
+ if (singleFile) {
46
+ await generateSingleFile(apiData, config, outputPath);
47
+ } else {
48
+ await generateMultipleFiles(apiData, config, outputPath);
49
+ }
50
+
51
+ if (verbose) {
52
+ console.log(` Generated documentation at: ${outputPath}`);
53
+ }
54
+ }
55
+
56
+ function hashPassword(password) {
57
+ if (password.startsWith('sha256:')) {
58
+ return password.slice(7);
59
+ }
60
+
61
+ return crypto.createHash('sha256').update(password).digest('hex');
62
+ }
63
+
64
+ function getPlainPassword(password) {
65
+ // If password starts with sha256:, we can't encrypt (need plain password)
66
+ if (password.startsWith('sha256:')) {
67
+ return null;
68
+ }
69
+ return password;
70
+ }
71
+
72
+ function encryptData(data, password) {
73
+ const salt = crypto.randomBytes(SALT_LENGTH);
74
+ const iv = crypto.randomBytes(IV_LENGTH);
75
+
76
+ // Derive key from password using PBKDF2
77
+ const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
78
+
79
+ // Encrypt with AES-256-GCM
80
+ const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
81
+
82
+ const jsonData = JSON.stringify(data);
83
+ let encrypted = cipher.update(jsonData, 'utf8');
84
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
85
+
86
+ const authTag = cipher.getAuthTag();
87
+
88
+ // Combine: salt + iv + authTag + encrypted data
89
+ const combined = Buffer.concat([salt, iv, authTag, encrypted]);
90
+
91
+ return combined.toString('base64');
92
+ }
93
+
94
+ function generateApiDataFile(apiData, outputPath, password = null, authData = null) {
95
+ let content;
96
+
97
+ if (password) {
98
+ const plainPassword = getPlainPassword(password);
99
+
100
+ if (plainPassword) {
101
+ // Encrypt the API data
102
+ const encryptedData = encryptData(apiData, plainPassword);
103
+ content = `var apiDataEncrypted="${encryptedData}";var apiDataAuth=${minifyJSON(authData)};var apiData=null;`;
104
+ } else {
105
+ // Can't encrypt with hash-only password, warn and use unencrypted
106
+ console.warn('Warning: Cannot encrypt API data with pre-hashed password. Use plain password for encryption.');
107
+ content = `var apiDataEncrypted=null;var apiDataAuth=${minifyJSON(authData)};var apiData=${minifyJSON(apiData)};`;
108
+ }
109
+ } else {
110
+ // No password, no encryption
111
+ content = `var apiDataEncrypted=null;var apiDataAuth=null;var apiData=${minifyJSON(apiData)};`;
112
+ }
113
+
114
+ fs.writeFileSync(path.join(outputPath, 'api_data.js'), content, 'utf8');
115
+ }
116
+
117
+ async function generateMultipleFiles(apiData, config, outputPath) {
118
+ let htmlTemplate = fs.readFileSync(path.join(TEMPLATE_DIR, 'index.html'), 'utf8');
119
+
120
+ // Generate cache version from current timestamp
121
+ const cacheVersion = Date.now().toString(36);
122
+
123
+ htmlTemplate = htmlTemplate
124
+ .replace(/\{\{PROJECT_TITLE\}\}/g, config.title || config.name)
125
+ .replace(/\{\{PROJECT_NAME\}\}/g, config.name)
126
+ .replace(/\{\{PROJECT_VERSION\}\}/g, config.version)
127
+ .replace(/\{\{CACHE_VERSION\}\}/g, cacheVersion);
128
+
129
+ fs.writeFileSync(path.join(outputPath, 'index.html'), htmlTemplate, 'utf8');
130
+
131
+ // Minify and write CSS
132
+ const cssDir = path.join(outputPath, 'css');
133
+ if (!fs.existsSync(cssDir)) {
134
+ fs.mkdirSync(cssDir, { recursive: true });
135
+ }
136
+ const cssContent = fs.readFileSync(path.join(TEMPLATE_DIR, 'css/style.css'), 'utf8');
137
+ fs.writeFileSync(path.join(cssDir, 'style.css'), minifyCSS(cssContent), 'utf8');
138
+
139
+ // Minify and write JS using Terser
140
+ const jsDir = path.join(outputPath, 'js');
141
+ if (!fs.existsSync(jsDir)) {
142
+ fs.mkdirSync(jsDir, { recursive: true });
143
+ }
144
+ const mainJsContent = fs.readFileSync(path.join(TEMPLATE_DIR, 'js/main.js'), 'utf8');
145
+ const mainJsMinified = await minifyAndObfuscateJSAsync(mainJsContent);
146
+ fs.writeFileSync(path.join(jsDir, 'main.js'), mainJsMinified, 'utf8');
147
+
148
+ const authJsContent = fs.readFileSync(path.join(TEMPLATE_DIR, 'js/auth.js'), 'utf8');
149
+ const authJsMinified = await minifyAndObfuscateJSAsync(authJsContent);
150
+ fs.writeFileSync(path.join(jsDir, 'auth.js'), authJsMinified, 'utf8');
151
+ }
152
+
153
+ async function generateSingleFile(apiData, config, outputPath) {
154
+ const css = minifyCSS(fs.readFileSync(path.join(TEMPLATE_DIR, 'css/style.css'), 'utf8'));
155
+ const mainJs = await minifyAndObfuscateJSAsync(fs.readFileSync(path.join(TEMPLATE_DIR, 'js/main.js'), 'utf8'));
156
+ const authJs = await minifyAndObfuscateJSAsync(fs.readFileSync(path.join(TEMPLATE_DIR, 'js/auth.js'), 'utf8'));
157
+ const apiDataJs = `var apiData=${minifyJSON(apiData)};`;
158
+
159
+ const html = `<!DOCTYPE html>
160
+ <html lang="en">
161
+ <head>
162
+ <meta charset="UTF-8">
163
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
164
+ <meta name="theme-color" content="#09090b">
165
+ <title>${escapeHtml(config.title || config.name)}</title>
166
+ <style>
167
+ ${css}
168
+ </style>
169
+ </head>
170
+ <body>
171
+ <div id="login-container"></div>
172
+
173
+ <div id="app" class="app hidden">
174
+ <aside class="sidebar">
175
+ <div class="sidebar-header">
176
+ <h1>${escapeHtml(config.name)}</h1>
177
+ <div class="version">v${escapeHtml(config.version)}</div>
178
+ </div>
179
+
180
+ <div class="sidebar-search">
181
+ <input type="text" class="search-input" placeholder="Search endpoints..." id="search-input">
182
+ </div>
183
+
184
+ <nav class="sidebar-nav" id="nav-container">
185
+ </nav>
186
+ </aside>
187
+
188
+ <main class="main">
189
+ <header class="main-header">
190
+ <h2 id="current-section">API Documentation</h2>
191
+ <button class="logout-btn hidden" id="logout-btn">Logout</button>
192
+ </header>
193
+
194
+ <div class="content" id="content-container">
195
+ </div>
196
+ </main>
197
+
198
+ <button class="mobile-toggle" id="mobile-toggle">
199
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
200
+ <line x1="3" y1="12" x2="21" y2="12"></line>
201
+ <line x1="3" y1="6" x2="21" y2="6"></line>
202
+ <line x1="3" y1="18" x2="21" y2="18"></line>
203
+ </svg>
204
+ </button>
205
+ </div>
206
+
207
+ <script>
208
+ ${apiDataJs}
209
+ </script>
210
+ <script>
211
+ ${authJs}
212
+ </script>
213
+ <script>
214
+ ${mainJs}
215
+ </script>
216
+ </body>
217
+ </html>`;
218
+
219
+ fs.writeFileSync(path.join(outputPath, 'index.html'), html, 'utf8');
220
+ }
221
+
222
+ function escapeHtml(str) {
223
+ if (!str) return '';
224
+ return String(str)
225
+ .replace(/&/g, '&amp;')
226
+ .replace(/</g, '&lt;')
227
+ .replace(/>/g, '&gt;')
228
+ .replace(/"/g, '&quot;')
229
+ .replace(/'/g, '&#39;');
230
+ }
231
+
232
+ function saveVersionHistory(apiData, config, outputPath, verbose) {
233
+ const versionsDir = path.join(outputPath, 'versions');
234
+
235
+ if (!fs.existsSync(versionsDir)) {
236
+ fs.mkdirSync(versionsDir, { recursive: true });
237
+ }
238
+
239
+ const version = apiData.project.version || '1.0.0';
240
+ const timestamp = new Date().toISOString();
241
+ const versionFilename = `v${version.replace(/\./g, '_')}.json`;
242
+ const versionPath = path.join(versionsDir, versionFilename);
243
+
244
+ // Load existing versions index
245
+ const versionsIndexPath = path.join(versionsDir, 'index.json');
246
+ let versionsIndex = [];
247
+
248
+ if (fs.existsSync(versionsIndexPath)) {
249
+ try {
250
+ versionsIndex = JSON.parse(fs.readFileSync(versionsIndexPath, 'utf8'));
251
+ } catch (e) {
252
+ versionsIndex = [];
253
+ }
254
+ }
255
+
256
+ // Check if this version already exists
257
+ const existingIndex = versionsIndex.findIndex(v => v.version === version);
258
+
259
+ // Create version entry
260
+ const versionEntry = {
261
+ version: version,
262
+ filename: versionFilename,
263
+ timestamp: timestamp,
264
+ endpointCount: countEndpoints(apiData),
265
+ groupCount: apiData.groups.length
266
+ };
267
+
268
+ if (existingIndex >= 0) {
269
+ // Update existing version
270
+ versionsIndex[existingIndex] = versionEntry;
271
+ } else {
272
+ // Add new version
273
+ versionsIndex.push(versionEntry);
274
+ }
275
+
276
+ // Sort versions (newest first)
277
+ versionsIndex.sort((a, b) => {
278
+ const aVer = a.version.split('.').map(Number);
279
+ const bVer = b.version.split('.').map(Number);
280
+ for (let i = 0; i < Math.max(aVer.length, bVer.length); i++) {
281
+ const aNum = aVer[i] || 0;
282
+ const bNum = bVer[i] || 0;
283
+ if (aNum !== bNum) return bNum - aNum;
284
+ }
285
+ return 0;
286
+ });
287
+
288
+ // Save version data (without generator timestamp to allow comparison)
289
+ const versionData = {
290
+ ...apiData,
291
+ generator: {
292
+ ...apiData.generator,
293
+ versionSavedAt: timestamp
294
+ }
295
+ };
296
+
297
+ fs.writeFileSync(versionPath, JSON.stringify(versionData, null, 2), 'utf8');
298
+
299
+ // Save versions index
300
+ fs.writeFileSync(versionsIndexPath, JSON.stringify(versionsIndex, null, 2), 'utf8');
301
+
302
+ // Load all version data to embed in versions.js (for file:// protocol support)
303
+ const allVersionData = {};
304
+ for (const v of versionsIndex) {
305
+ const vPath = path.join(versionsDir, v.filename);
306
+ if (fs.existsSync(vPath)) {
307
+ try {
308
+ allVersionData[v.filename] = JSON.parse(fs.readFileSync(vPath, 'utf8'));
309
+ } catch (e) {
310
+ // Skip if can't read
311
+ }
312
+ }
313
+ }
314
+
315
+ // Generate versions.js for frontend
316
+ // Include withCompare setting and embedded version data for file:// protocol support
317
+ const withCompare = config.template && config.template.withCompare ? true : false;
318
+ const versionsJs = `var apiVersions=${JSON.stringify(versionsIndex)};var apiVersionsConfig={withCompare:${withCompare}};var apiVersionsData=${JSON.stringify(allVersionData)};`;
319
+ fs.writeFileSync(path.join(outputPath, 'versions.js'), versionsJs, 'utf8');
320
+
321
+ if (verbose) {
322
+ console.log(` Saved version ${version} to history (${versionsIndex.length} versions total)`);
323
+ }
324
+ }
325
+
326
+ function countEndpoints(apiData) {
327
+ let count = 0;
328
+ for (const group of apiData.groups) {
329
+ count += group.endpoints.length;
330
+ }
331
+ return count;
332
+ }
333
+
334
+ module.exports = { generateDocs, hashPassword };
package/lib/index.js ADDED
@@ -0,0 +1,59 @@
1
+ const { loadConfig } = require('./utils/config-loader');
2
+ const { scanFiles } = require('./utils/file-scanner');
3
+ const { parseFiles } = require('./parser');
4
+ const { buildApiData } = require('./generator/data-builder');
5
+ const { generateDocs } = require('./generator');
6
+
7
+ async function generate(options = {}) {
8
+ const {
9
+ input,
10
+ output,
11
+ config: configPath,
12
+ includePrivate = false,
13
+ singleFile = false,
14
+ verbose = false
15
+ } = options;
16
+
17
+ const config = loadConfig(configPath, input);
18
+
19
+ if (verbose) {
20
+ console.log(` Project: ${config.name} v${config.version}`);
21
+ }
22
+
23
+ const files = await scanFiles(input, { verbose });
24
+
25
+ if (files.length === 0) {
26
+ throw new Error('No source files found in the input path');
27
+ }
28
+
29
+ const { endpoints, defines } = parseFiles(files, { verbose });
30
+
31
+ if (endpoints.length === 0) {
32
+ throw new Error('No API documentation blocks found in source files');
33
+ }
34
+
35
+ const apiData = buildApiData(endpoints, config, { includePrivate });
36
+
37
+ await generateDocs(apiData, config, output, { singleFile, verbose });
38
+
39
+ return {
40
+ endpoints: endpoints.length,
41
+ groups: apiData.groups.length,
42
+ defines: defines.size
43
+ };
44
+ }
45
+
46
+ function createDoc(comment) {
47
+ const { parseAnnotations } = require('./parser/annotations');
48
+ return parseAnnotations(comment);
49
+ }
50
+
51
+ module.exports = {
52
+ generate,
53
+ createDoc,
54
+ loadConfig: require('./utils/config-loader').loadConfig,
55
+ scanFiles: require('./utils/file-scanner').scanFiles,
56
+ parseFiles: require('./parser').parseFiles,
57
+ buildApiData: require('./generator/data-builder').buildApiData,
58
+ generateDocs: require('./generator').generateDocs
59
+ };
@@ -0,0 +1,230 @@
1
+ const ANNOTATION_PATTERNS = {
2
+ api: /^@api\s+\{(\w+)\}\s+(\S+)(?:\s+(.*))?$/m,
3
+ apiName: /^@apiName\s+(.+)$/m,
4
+ apiGroup: /^@apiGroup\s+(.+)$/m,
5
+ apiVersion: /^@apiVersion\s+(.+)$/m,
6
+ apiDescription: /^@apiDescription\s+([\s\S]*?)(?=(?:^@api|\Z))/m,
7
+ apiPermission: /^@apiPermission\s+(.+)$/m,
8
+ apiDeprecated: /^@apiDeprecated(?:\s+(.*))?$/m,
9
+ apiPrivate: /^@apiPrivate$/m,
10
+ apiIgnore: /^@apiIgnore(?:\s+(.*))?$/m,
11
+ apiSampleRequest: /^@apiSampleRequest\s+(.+)$/m,
12
+ apiDefine: /^@apiDefine\s+(\w+)(?:\s+(.*))?$/m,
13
+ apiUse: /^@apiUse\s+(\w+)$/gm
14
+ };
15
+
16
+ const PARAM_PATTERNS = {
17
+ apiParam: /^@apiParam(?:\s+\(([^)]+)\))?\s+(?:\{([^}]+)\})?\s*(\[)?(\w+(?:\.\w+)*)(?:=([^\]"\s]+|"[^"]*"))?\]?\s*(.*)?$/gm,
18
+ apiQuery: /^@apiQuery(?:\s+\(([^)]+)\))?\s+(?:\{([^}]+)\})?\s*(\[)?(\w+(?:\.\w+)*)(?:=([^\]"\s]+|"[^"]*"))?\]?\s*(.*)?$/gm,
19
+ apiBody: /^@apiBody(?:\s+\(([^)]+)\))?\s+(?:\{([^}]+)\})?\s*(\[)?(\w+(?:\.\w+)*)(?:=([^\]"\s]+|"[^"]*"))?\]?\s*(.*)?$/gm,
20
+ apiHeader: /^@apiHeader(?:\s+\(([^)]+)\))?\s+(?:\{([^}]+)\})?\s*(\[)?(\w+(?:\.\w+)*)(?:=([^\]"\s]+|"[^"]*"))?\]?\s*(.*)?$/gm,
21
+ apiSuccess: /^@apiSuccess(?:\s+\(([^)]+)\))?\s+(?:\{([^}]+)\})?\s*(\w+(?:\.\w+)*)\s*(.*)?$/gm,
22
+ apiError: /^@apiError(?:\s+\(([^)]+)\))?\s+(?:\{([^}]+)\})?\s*(\w+(?:\.\w+)*)\s*(.*)?$/gm
23
+ };
24
+
25
+ const EXAMPLE_PATTERNS = {
26
+ apiExample: /^@apiExample(?:\s+\{([^}]+)\})?\s+(.+)\n([\s\S]*?)(?=\n@api[A-Z]|\n\s*\n|$(?!\n))/gm,
27
+ apiSuccessExample: /^@apiSuccessExample(?:\s+\{([^}]+)\})?\s*(.*)?\n([\s\S]*?)(?=\n@api[A-Z]|\n\s*\n|$(?!\n))/gm,
28
+ apiErrorExample: /^@apiErrorExample(?:\s+\{([^}]+)\})?\s*(.*)?\n([\s\S]*?)(?=\n@api[A-Z]|\n\s*\n|$(?!\n))/gm,
29
+ apiParamExample: /^@apiParamExample(?:\s+\{([^}]+)\})?\s*(.*)?\n([\s\S]*?)(?=\n@api[A-Z]|\n\s*\n|$(?!\n))/gm,
30
+ apiHeaderExample: /^@apiHeaderExample(?:\s+\{([^}]+)\})?\s*(.*)?\n([\s\S]*?)(?=\n@api[A-Z]|\n\s*\n|$(?!\n))/gm
31
+ };
32
+
33
+ function parseAnnotations(block) {
34
+ // Normalize line endings
35
+ block = block.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
36
+
37
+ const result = {
38
+ type: null,
39
+ method: null,
40
+ path: null,
41
+ title: '',
42
+ name: null,
43
+ group: 'Default',
44
+ version: '1.0.0',
45
+ description: '',
46
+ permission: null,
47
+ deprecated: false,
48
+ deprecatedText: null,
49
+ private: false,
50
+ ignore: false,
51
+ sampleRequest: null,
52
+ params: [],
53
+ query: [],
54
+ body: [],
55
+ headers: [],
56
+ success: [],
57
+ error: [],
58
+ examples: [],
59
+ successExamples: [],
60
+ errorExamples: [],
61
+ paramExamples: [],
62
+ headerExamples: [],
63
+ define: null,
64
+ uses: []
65
+ };
66
+
67
+ const apiMatch = block.match(ANNOTATION_PATTERNS.api);
68
+ if (apiMatch) {
69
+ result.type = 'endpoint';
70
+ result.method = apiMatch[1].toUpperCase();
71
+ result.path = apiMatch[2];
72
+ result.title = apiMatch[3] || '';
73
+ }
74
+
75
+ const defineMatch = block.match(ANNOTATION_PATTERNS.apiDefine);
76
+ if (defineMatch) {
77
+ result.type = 'define';
78
+ result.define = {
79
+ name: defineMatch[1],
80
+ title: defineMatch[2] || ''
81
+ };
82
+ }
83
+
84
+ const nameMatch = block.match(ANNOTATION_PATTERNS.apiName);
85
+ if (nameMatch) result.name = nameMatch[1].trim();
86
+
87
+ const groupMatch = block.match(ANNOTATION_PATTERNS.apiGroup);
88
+ if (groupMatch) result.group = groupMatch[1].trim();
89
+
90
+ const versionMatch = block.match(ANNOTATION_PATTERNS.apiVersion);
91
+ if (versionMatch) result.version = versionMatch[1].trim();
92
+
93
+ const descMatch = block.match(ANNOTATION_PATTERNS.apiDescription);
94
+ if (descMatch) result.description = descMatch[1].trim();
95
+
96
+ const permMatch = block.match(ANNOTATION_PATTERNS.apiPermission);
97
+ if (permMatch) result.permission = permMatch[1].trim();
98
+
99
+ const deprecatedMatch = block.match(ANNOTATION_PATTERNS.apiDeprecated);
100
+ if (deprecatedMatch) {
101
+ result.deprecated = true;
102
+ result.deprecatedText = deprecatedMatch[1] || null;
103
+ }
104
+
105
+ if (ANNOTATION_PATTERNS.apiPrivate.test(block)) {
106
+ result.private = true;
107
+ }
108
+
109
+ const ignoreMatch = block.match(ANNOTATION_PATTERNS.apiIgnore);
110
+ if (ignoreMatch) {
111
+ result.ignore = true;
112
+ }
113
+
114
+ const sampleMatch = block.match(ANNOTATION_PATTERNS.apiSampleRequest);
115
+ if (sampleMatch) result.sampleRequest = sampleMatch[1].trim();
116
+
117
+ let useMatch;
118
+ while ((useMatch = ANNOTATION_PATTERNS.apiUse.exec(block)) !== null) {
119
+ result.uses.push(useMatch[1]);
120
+ }
121
+
122
+ result.params = parseParamBlock(block, PARAM_PATTERNS.apiParam, 'Parameter');
123
+ result.query = parseParamBlock(block, PARAM_PATTERNS.apiQuery, 'Query');
124
+ result.body = parseParamBlock(block, PARAM_PATTERNS.apiBody, 'Body');
125
+ result.headers = parseParamBlock(block, PARAM_PATTERNS.apiHeader, 'Header');
126
+ result.success = parseResponseBlock(block, PARAM_PATTERNS.apiSuccess, 'Success 200');
127
+ result.error = parseResponseBlock(block, PARAM_PATTERNS.apiError, 'Error 4xx');
128
+
129
+ result.examples = parseExampleBlock(block, EXAMPLE_PATTERNS.apiExample);
130
+ result.successExamples = parseExampleBlock(block, EXAMPLE_PATTERNS.apiSuccessExample);
131
+ result.errorExamples = parseExampleBlock(block, EXAMPLE_PATTERNS.apiErrorExample);
132
+ result.paramExamples = parseExampleBlock(block, EXAMPLE_PATTERNS.apiParamExample);
133
+ result.headerExamples = parseExampleBlock(block, EXAMPLE_PATTERNS.apiHeaderExample);
134
+
135
+ return result;
136
+ }
137
+
138
+ function parseParamBlock(block, pattern, defaultGroup) {
139
+ const params = [];
140
+ let match;
141
+ const regex = new RegExp(pattern.source, 'gm');
142
+
143
+ while ((match = regex.exec(block)) !== null) {
144
+ const [, group, type, optional, field, defaultValue, description] = match;
145
+
146
+ params.push({
147
+ group: group || defaultGroup,
148
+ type: parseType(type),
149
+ field: field,
150
+ optional: !!optional,
151
+ defaultValue: defaultValue ? defaultValue.replace(/^"|"$/g, '') : null,
152
+ description: description ? description.trim() : ''
153
+ });
154
+ }
155
+
156
+ return params;
157
+ }
158
+
159
+ function parseResponseBlock(block, pattern, defaultGroup) {
160
+ const responses = [];
161
+ let match;
162
+ const regex = new RegExp(pattern.source, 'gm');
163
+
164
+ while ((match = regex.exec(block)) !== null) {
165
+ const [, group, type, field, description] = match;
166
+
167
+ responses.push({
168
+ group: group || defaultGroup,
169
+ type: parseType(type),
170
+ field: field,
171
+ description: description ? description.trim() : ''
172
+ });
173
+ }
174
+
175
+ return responses;
176
+ }
177
+
178
+ function parseExampleBlock(block, pattern) {
179
+ const examples = [];
180
+ let match;
181
+ const regex = new RegExp(pattern.source, 'gm');
182
+
183
+ while ((match = regex.exec(block)) !== null) {
184
+ const [, type, title, content] = match;
185
+
186
+ examples.push({
187
+ type: type || 'json',
188
+ title: title ? title.trim() : '',
189
+ content: content ? content.trim() : ''
190
+ });
191
+ }
192
+
193
+ return examples;
194
+ }
195
+
196
+ function parseType(typeStr) {
197
+ if (!typeStr) return { name: 'String', isArray: false, allowedValues: null, size: null };
198
+
199
+ const result = {
200
+ name: 'String',
201
+ isArray: false,
202
+ allowedValues: null,
203
+ size: null
204
+ };
205
+
206
+ let type = typeStr.trim();
207
+
208
+ if (type.endsWith('[]')) {
209
+ result.isArray = true;
210
+ type = type.slice(0, -2);
211
+ }
212
+
213
+ const allowedMatch = type.match(/^(\w+)=(.+)$/);
214
+ if (allowedMatch) {
215
+ type = allowedMatch[1];
216
+ result.allowedValues = allowedMatch[2].split(',').map(v => v.replace(/^"|"$/g, '').trim());
217
+ }
218
+
219
+ const sizeMatch = type.match(/^(\w+)\{(.+)\}$/);
220
+ if (sizeMatch) {
221
+ type = sizeMatch[1];
222
+ result.size = sizeMatch[2];
223
+ }
224
+
225
+ result.name = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase();
226
+
227
+ return result;
228
+ }
229
+
230
+ module.exports = { parseAnnotations, ANNOTATION_PATTERNS, PARAM_PATTERNS, EXAMPLE_PATTERNS };
@@ -0,0 +1,86 @@
1
+ const fs = require('fs');
2
+ const { extractCommentBlocks } = require('./languages');
3
+ const { parseAnnotations } = require('./annotations');
4
+ const { getLanguage } = require('../utils/file-scanner');
5
+
6
+ function parseFile(filePath) {
7
+ const content = fs.readFileSync(filePath, 'utf8');
8
+ const language = getLanguage(filePath);
9
+
10
+ if (!language) return [];
11
+
12
+ const blocks = extractCommentBlocks(content, language);
13
+ const parsed = blocks.map(block => parseAnnotations(block));
14
+
15
+ return parsed.map(item => ({
16
+ ...item,
17
+ sourceFile: filePath
18
+ }));
19
+ }
20
+
21
+ function parseFiles(files, options = {}) {
22
+ const { verbose } = options;
23
+ const allBlocks = [];
24
+ const defines = new Map();
25
+
26
+ for (const file of files) {
27
+ try {
28
+ const blocks = parseFile(file);
29
+ allBlocks.push(...blocks);
30
+ } catch (error) {
31
+ if (verbose) {
32
+ console.log(` Warning: Could not parse ${file}: ${error.message}`);
33
+ }
34
+ }
35
+ }
36
+
37
+ for (const block of allBlocks) {
38
+ if (block.type === 'define' && block.define) {
39
+ defines.set(block.define.name, block);
40
+ }
41
+ }
42
+
43
+ for (const block of allBlocks) {
44
+ if (block.uses && block.uses.length > 0) {
45
+ for (const useName of block.uses) {
46
+ const defineBlock = defines.get(useName);
47
+ if (defineBlock) {
48
+ mergeDefine(block, defineBlock);
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ const endpoints = allBlocks.filter(block =>
55
+ block.type === 'endpoint' && !block.ignore
56
+ );
57
+
58
+ if (verbose) {
59
+ console.log(` Parsed ${endpoints.length} API endpoints`);
60
+ console.log(` Found ${defines.size} @apiDefine blocks`);
61
+ }
62
+
63
+ return { endpoints, defines };
64
+ }
65
+
66
+ function mergeDefine(target, source) {
67
+ if (source.description && !target.description) {
68
+ target.description = source.description;
69
+ }
70
+
71
+ if (source.permission && !target.permission) {
72
+ target.permission = source.permission;
73
+ }
74
+
75
+ target.params = [...target.params, ...source.params];
76
+ target.query = [...target.query, ...source.query];
77
+ target.body = [...target.body, ...source.body];
78
+ target.headers = [...target.headers, ...source.headers];
79
+ target.success = [...target.success, ...source.success];
80
+ target.error = [...target.error, ...source.error];
81
+ target.examples = [...target.examples, ...source.examples];
82
+ target.successExamples = [...target.successExamples, ...source.successExamples];
83
+ target.errorExamples = [...target.errorExamples, ...source.errorExamples];
84
+ }
85
+
86
+ module.exports = { parseFile, parseFiles };