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.
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/bin/apidocly.js +62 -0
- package/lib/generator/data-builder.js +170 -0
- package/lib/generator/index.js +334 -0
- package/lib/index.js +59 -0
- package/lib/parser/annotations.js +230 -0
- package/lib/parser/index.js +86 -0
- package/lib/parser/languages.js +57 -0
- package/lib/utils/config-loader.js +67 -0
- package/lib/utils/file-scanner.js +64 -0
- package/lib/utils/minifier.js +106 -0
- package/package.json +46 -0
- package/template/css/style.css +2670 -0
- package/template/index.html +243 -0
- package/template/js/auth.js +281 -0
- package/template/js/main.js +2933 -0
|
@@ -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, '&')
|
|
226
|
+
.replace(/</g, '<')
|
|
227
|
+
.replace(/>/g, '>')
|
|
228
|
+
.replace(/"/g, '"')
|
|
229
|
+
.replace(/'/g, ''');
|
|
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 };
|