froggy-docs 1.0.8 → 1.0.9
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/frontend/web/app.js +251 -0
- package/frontend/web/index.html +27 -5
- package/package.json +1 -1
- package/frontend/web/main.client.dart.js +0 -81
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
let apiData = null;
|
|
2
|
+
let isDark = false;
|
|
3
|
+
let searchQuery = '';
|
|
4
|
+
let authHeader = '';
|
|
5
|
+
const responses = {};
|
|
6
|
+
const loading = {};
|
|
7
|
+
const requestBodyValues = {};
|
|
8
|
+
const expandedEndpoints = {};
|
|
9
|
+
|
|
10
|
+
const methodColors = {
|
|
11
|
+
GET: '#61affe',
|
|
12
|
+
POST: '#49cc90',
|
|
13
|
+
PUT: '#fca130',
|
|
14
|
+
DELETE: '#f93e3e',
|
|
15
|
+
PATCH: '#50e3c2'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function loadData() {
|
|
19
|
+
try {
|
|
20
|
+
const resp = await fetch('/froggy_docs.json');
|
|
21
|
+
if (resp.ok) {
|
|
22
|
+
apiData = await resp.json();
|
|
23
|
+
render();
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error('Failed to load API data:', e);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getEndpointsByTag() {
|
|
31
|
+
const byTag = {};
|
|
32
|
+
const paths = apiData?.paths || {};
|
|
33
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
34
|
+
for (const [method, spec] of Object.entries(methods)) {
|
|
35
|
+
const tags = spec.tags || ['Untagged'];
|
|
36
|
+
for (const tag of tags) {
|
|
37
|
+
if (!byTag[tag]) byTag[tag] = [];
|
|
38
|
+
byTag[tag].push({ path, method, spec });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return byTag;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function render() {
|
|
46
|
+
document.getElementById('docTitle').textContent = apiData?.info?.title || 'API Documentation';
|
|
47
|
+
renderSidebar();
|
|
48
|
+
renderApiList();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderSidebar() {
|
|
52
|
+
const byTag = getEndpointsByTag();
|
|
53
|
+
const filtered = Object.entries(byTag).filter(([tag, endpoints]) => {
|
|
54
|
+
if (searchQuery === '') return true;
|
|
55
|
+
if (tag.toLowerCase().includes(searchQuery.toLowerCase())) return true;
|
|
56
|
+
return endpoints.some(ep => ep.path.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let html = '';
|
|
60
|
+
for (const [tag, endpoints] of filtered) {
|
|
61
|
+
const tagMatchesSearch = searchQuery === '' || tag.toLowerCase().includes(searchQuery.toLowerCase());
|
|
62
|
+
const tagFiltered = searchQuery === '' || tagMatchesSearch
|
|
63
|
+
? endpoints
|
|
64
|
+
: endpoints.filter(ep => ep.path.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
65
|
+
|
|
66
|
+
if (searchQuery !== '' && tagFiltered.length === 0) continue;
|
|
67
|
+
|
|
68
|
+
html += `
|
|
69
|
+
<div class="tag-group">
|
|
70
|
+
<div class="tag-header">▶ ${tag}</div>
|
|
71
|
+
<div class="tag-endpoints">
|
|
72
|
+
${tagFiltered.map(ep => `
|
|
73
|
+
<a class="nav-item" href="#${ep.method.toUpperCase()}-${ep.path}">
|
|
74
|
+
<span class="method-tag ${ep.method.toUpperCase()}" style="background:${methodColors[ep.method.toUpperCase()] || '#666'}">${ep.method.toUpperCase()}</span>
|
|
75
|
+
<span>${ep.path}</span>
|
|
76
|
+
</a>
|
|
77
|
+
`).join('')}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
document.getElementById('navList').innerHTML = html;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderApiList() {
|
|
86
|
+
if (!apiData) {
|
|
87
|
+
document.getElementById('apiList').innerHTML = '<div class="api-section"><p>Loading...</p></div>';
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const paths = apiData.paths || {};
|
|
92
|
+
let html = '';
|
|
93
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
94
|
+
for (const [method, spec] of Object.entries(methods)) {
|
|
95
|
+
html += renderEndpoint(path, method, spec);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
document.getElementById('apiList').innerHTML = html;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderEndpoint(path, method, spec) {
|
|
102
|
+
const key = `${method}-${path}`;
|
|
103
|
+
const props = spec.requestBody?.content?.['application/json']?.schema?.properties;
|
|
104
|
+
const hasAuth = spec.security != null;
|
|
105
|
+
const isExpanded = expandedEndpoints[key] || false;
|
|
106
|
+
const savedValues = requestBodyValues[key] || {};
|
|
107
|
+
|
|
108
|
+
let paramsHtml = '';
|
|
109
|
+
if (props && Object.keys(props).length > 0) {
|
|
110
|
+
paramsHtml = `
|
|
111
|
+
<h3 class="section-title">Parameters</h3>
|
|
112
|
+
<table class="params-table">
|
|
113
|
+
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
|
114
|
+
<tbody>
|
|
115
|
+
${Object.entries(props).map(([name, prop]) => `
|
|
116
|
+
<tr>
|
|
117
|
+
<td><b>${name}</b></td>
|
|
118
|
+
<td><span class="type-label">${prop.type || 'string'}</span></td>
|
|
119
|
+
<td>${prop.description || '-'}</td>
|
|
120
|
+
</tr>
|
|
121
|
+
`).join('')}
|
|
122
|
+
</tbody>
|
|
123
|
+
</table>
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return `
|
|
128
|
+
<section class="api-section" id="${method.toUpperCase()}-${path}">
|
|
129
|
+
<div class="endpoint-header">
|
|
130
|
+
<span class="method-tag ${method.toUpperCase()}" style="background:${methodColors[method.toUpperCase()] || '#666'}">${method.toUpperCase()}</span>
|
|
131
|
+
<span class="path-text">${path}</span>
|
|
132
|
+
${hasAuth ? '<span class="auth-badge">🔒 Auth</span>' : ''}
|
|
133
|
+
</div>
|
|
134
|
+
<p class="api-description">${spec.summary || 'No description'}</p>
|
|
135
|
+
|
|
136
|
+
${props && Object.keys(props).length > 0 ? `
|
|
137
|
+
<button class="try-it-out-btn ${isExpanded ? '' : 'secondary'}" onclick="toggleBody('${key}')">
|
|
138
|
+
${isExpanded ? 'Hide Request Body' : 'Show Request Body'}
|
|
139
|
+
</button>
|
|
140
|
+
${isExpanded ? `
|
|
141
|
+
<h3 class="section-title">Request Body</h3>
|
|
142
|
+
<div class="request-body-form">
|
|
143
|
+
${Object.entries(props).map(([name, prop]) => `
|
|
144
|
+
<div class="form-field">
|
|
145
|
+
<label>${name} (${prop.type || 'string'})</label>
|
|
146
|
+
${prop.description ? `<span class="field-desc">${prop.description}</span>` : ''}
|
|
147
|
+
<input type="text" class="form-input" placeholder="${prop.default || ''}"
|
|
148
|
+
value="${savedValues[name] || ''}"
|
|
149
|
+
oninput="saveBodyValue('${key}', '${name}', this.value)">
|
|
150
|
+
</div>
|
|
151
|
+
`).join('')}
|
|
152
|
+
</div>
|
|
153
|
+
` : ''}
|
|
154
|
+
` : ''}
|
|
155
|
+
|
|
156
|
+
<button class="try-it-out-btn" onclick="executeRequest('${method}', '${path}', ${hasAuth}, ${props ? JSON.stringify(props).replace(/"/g, '"') : 'null'})">
|
|
157
|
+
${loading[key] ? 'Loading...' : 'Try It Out'}
|
|
158
|
+
</button>
|
|
159
|
+
|
|
160
|
+
${responses[key] ? `
|
|
161
|
+
<div class="response-section">
|
|
162
|
+
<h4>Response:</h4>
|
|
163
|
+
<pre class="response-body">${responses[key]}</pre>
|
|
164
|
+
</div>
|
|
165
|
+
` : ''}
|
|
166
|
+
|
|
167
|
+
${paramsHtml}
|
|
168
|
+
</section>
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toggleBody(key) {
|
|
173
|
+
expandedEndpoints[key] = !expandedEndpoints[key];
|
|
174
|
+
renderApiList();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function saveBodyValue(key, field, value) {
|
|
178
|
+
if (!requestBodyValues[key]) requestBodyValues[key] = {};
|
|
179
|
+
requestBodyValues[key][field] = value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function executeRequest(method, path, hasAuth, props) {
|
|
183
|
+
const key = `${method}-${path}`;
|
|
184
|
+
loading[key] = true;
|
|
185
|
+
renderApiList();
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
189
|
+
if (authHeader) headers['Authorization'] = authHeader;
|
|
190
|
+
|
|
191
|
+
let body = '{}';
|
|
192
|
+
if (props && Object.keys(props).length > 0) {
|
|
193
|
+
const bodyData = {};
|
|
194
|
+
const savedValues = requestBodyValues[key] || {};
|
|
195
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
196
|
+
const value = savedValues[name] || prop.default || '';
|
|
197
|
+
if (value) bodyData[name] = prop.type === 'number' ? Number(value) : value;
|
|
198
|
+
}
|
|
199
|
+
body = JSON.stringify(bodyData);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let resp;
|
|
203
|
+
const url = `${window.location.origin}${path}`;
|
|
204
|
+
const options = { method, headers };
|
|
205
|
+
|
|
206
|
+
if (method !== 'GET' && method !== 'DELETE') {
|
|
207
|
+
options.body = body;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
resp = await fetch(url, options);
|
|
211
|
+
|
|
212
|
+
let responseText;
|
|
213
|
+
try {
|
|
214
|
+
const json = await resp.json();
|
|
215
|
+
responseText = JSON.stringify(json, null, 2);
|
|
216
|
+
} catch {
|
|
217
|
+
responseText = await resp.text();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
responses[key] = resp.ok ? responseText : `Error ${resp.status}: ${responseText}`;
|
|
221
|
+
} catch (e) {
|
|
222
|
+
responses[key] = `Error: ${e.message}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
loading[key] = false;
|
|
226
|
+
renderApiList();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
230
|
+
searchQuery = e.target.value;
|
|
231
|
+
render();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
document.getElementById('authInput').addEventListener('input', (e) => {
|
|
235
|
+
authHeader = e.target.value;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
document.getElementById('themeToggle').addEventListener('click', () => {
|
|
239
|
+
isDark = !isDark;
|
|
240
|
+
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
|
241
|
+
document.getElementById('themeToggle').textContent = isDark ? '☀️' : '🌙';
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
245
|
+
isDark = true;
|
|
246
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
247
|
+
document.getElementById('themeToggle').textContent = '☀️';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
loadData();
|
|
251
|
+
setInterval(loadData, 3000);
|
package/frontend/web/index.html
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<meta charset="
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"
|
|
6
|
-
<title>FroggyDocs</title>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>FroggyDocs - API Documentation</title>
|
|
7
7
|
<link rel="stylesheet" href="styles.css">
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
|
-
<
|
|
10
|
+
<div class="app-container">
|
|
11
|
+
<aside class="sidebar">
|
|
12
|
+
<div class="sidebar-header">
|
|
13
|
+
<h2><span>🐸</span> FroggyDocs</h2>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="search-container">
|
|
16
|
+
<input type="text" class="search-box" id="searchInput" placeholder="Search tags or endpoints...">
|
|
17
|
+
</div>
|
|
18
|
+
<nav class="nav-list" id="navList"></nav>
|
|
19
|
+
</aside>
|
|
20
|
+
<main class="main-content">
|
|
21
|
+
<header class="header">
|
|
22
|
+
<h1 id="docTitle">API Documentation</h1>
|
|
23
|
+
<button class="theme-toggle" id="themeToggle">🌙</button>
|
|
24
|
+
</header>
|
|
25
|
+
<div class="settings-section">
|
|
26
|
+
<div>Authorization: </div>
|
|
27
|
+
<input type="text" class="auth-input" id="authInput" placeholder="Bearer token">
|
|
28
|
+
</div>
|
|
29
|
+
<div id="apiList"></div>
|
|
30
|
+
</main>
|
|
31
|
+
</div>
|
|
32
|
+
<script src="app.js"></script>
|
|
11
33
|
</body>
|
|
12
34
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "froggy-docs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Auto-generate API documentation from code annotations. Works with any programming language.",
|
|
5
5
|
"author": "Kaung Mrat Thu <kaungmyatthuu.dev@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/Kaung-Myat/froggydocs",
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
(function() {
|
|
2
|
-
var _currentDirectory = (function () {
|
|
3
|
-
var _url;
|
|
4
|
-
var lines = new Error().stack.split('\n');
|
|
5
|
-
function lookupUrl() {
|
|
6
|
-
if (lines.length > 2) {
|
|
7
|
-
var match = lines[1].match(/^\s+at (.+):\d+:\d+$/);
|
|
8
|
-
// Chrome.
|
|
9
|
-
if (match) return match[1];
|
|
10
|
-
// Chrome nested eval case.
|
|
11
|
-
match = lines[1].match(/^\s+at eval [(](.+):\d+:\d+[)]$/);
|
|
12
|
-
if (match) return match[1];
|
|
13
|
-
// Edge.
|
|
14
|
-
match = lines[1].match(/^\s+at.+\((.+):\d+:\d+\)$/);
|
|
15
|
-
if (match) return match[1];
|
|
16
|
-
// Firefox.
|
|
17
|
-
match = lines[0].match(/[<][@](.+):\d+:\d+$/)
|
|
18
|
-
if (match) return match[1];
|
|
19
|
-
}
|
|
20
|
-
// Safari.
|
|
21
|
-
return lines[0].match(/[@](.+):\d+:\d+$/)[1];
|
|
22
|
-
}
|
|
23
|
-
_url = lookupUrl();
|
|
24
|
-
var lastSlash = _url.lastIndexOf('/');
|
|
25
|
-
if (lastSlash == -1) return _url;
|
|
26
|
-
var currentDirectory = _url.substring(0, lastSlash + 1);
|
|
27
|
-
return currentDirectory;
|
|
28
|
-
})();
|
|
29
|
-
|
|
30
|
-
var baseUrl = (function () {
|
|
31
|
-
// Attempt to detect --precompiled mode for tests, and set the base url
|
|
32
|
-
// appropriately, otherwise set it to '/'.
|
|
33
|
-
var pathParts = location.pathname.split("/");
|
|
34
|
-
if (pathParts[0] == "") {
|
|
35
|
-
pathParts.shift();
|
|
36
|
-
}
|
|
37
|
-
if (pathParts.length > 1 && pathParts[1] == "test") {
|
|
38
|
-
return "/" + pathParts.slice(0, 2).join("/") + "/";
|
|
39
|
-
}
|
|
40
|
-
// Attempt to detect base url using <base href> html tag
|
|
41
|
-
// base href should start and end with "/"
|
|
42
|
-
if (typeof document !== 'undefined') {
|
|
43
|
-
var el = document.getElementsByTagName('base');
|
|
44
|
-
if (el && el[0] && el[0].getAttribute("href") && el[0].getAttribute
|
|
45
|
-
("href").startsWith("/") && el[0].getAttribute("href").endsWith("/")){
|
|
46
|
-
return el[0].getAttribute("href");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// return default value
|
|
50
|
-
return "/";
|
|
51
|
-
}());
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
var mapperUri = baseUrl + "packages/build_web_compilers/src/dev_compiler_stack_trace/stack_trace_mapper.dart.js";
|
|
55
|
-
var requireUri = baseUrl +
|
|
56
|
-
"packages/build_web_compilers/src/dev_compiler/require.js";
|
|
57
|
-
var mainUri = _currentDirectory + "main.client.dart.bootstrap";
|
|
58
|
-
|
|
59
|
-
if (typeof document != 'undefined') {
|
|
60
|
-
var el = document.createElement("script");
|
|
61
|
-
el.defer = true;
|
|
62
|
-
el.async = false;
|
|
63
|
-
el.src = mapperUri;
|
|
64
|
-
document.head.appendChild(el);
|
|
65
|
-
|
|
66
|
-
el = document.createElement("script");
|
|
67
|
-
el.defer = true;
|
|
68
|
-
el.async = false;
|
|
69
|
-
el.src = requireUri;
|
|
70
|
-
el.setAttribute("data-main", mainUri);
|
|
71
|
-
document.head.appendChild(el);
|
|
72
|
-
} else {
|
|
73
|
-
importScripts(mapperUri, requireUri);
|
|
74
|
-
require.config({
|
|
75
|
-
baseUrl: baseUrl,
|
|
76
|
-
});
|
|
77
|
-
// TODO: update bootstrap code to take argument - dart-lang/build#1115
|
|
78
|
-
window = self;
|
|
79
|
-
require([mainUri + '.js']);
|
|
80
|
-
}
|
|
81
|
-
})();
|