1+ #! /bin/bash
2+ 
3+ #  Script to generate HTML documentation from all API specs using Redocly
4+ #  Preserves folder structure and generates a card-based index page
5+ 
6+ set  -ex
7+ set  -o pipefail
8+ 
9+ #  Colors
10+ RED=' \033[0;31m' 
11+ GREEN=' \033[0;32m' 
12+ YELLOW=' \033[1;33m' 
13+ BLUE=' \033[0;34m' 
14+ NC=' \033[0m' 
15+ 
16+ #  Directories
17+ SPECS_DIR=" specs" 
18+ OUTPUT_DIR=" docs/api-docs" 
19+ INDEX_FILE=" $OUTPUT_DIR /index.html" 
20+ ERROR_LOG=" $OUTPUT_DIR /errors.log" 
21+ 
22+ echo  -e " ${BLUE} 🚀 Starting API documentation generation...${NC} " 
23+ 
24+ #  Clean output folder
25+ rm -rf " $OUTPUT_DIR " 
26+ mkdir -p " $OUTPUT_DIR " 
27+ 
28+ #  Check Redocly
29+ if  !  command  -v redocly & > /dev/null;  then 
30+     echo  -e " ${RED} ❌ Redocly CLI not found. Install it:${NC} " 
31+     echo  " npm install -g @redocly/cli" 
32+     exit  1
33+ fi 
34+ echo  -e " ${GREEN} ✅ Redocly found: $( redocly --version) ${NC} " 
35+ 
36+ success_count=0
37+ error_count=0
38+ >  " $ERROR_LOG " 
39+ 
40+ 
41+ convert_spec_to_html () {
42+     local  spec_file=" $1 " 
43+     local  relative_path=" ${spec_file# $SPECS_DIR / } " 
44+     local  output_file=" $OUTPUT_DIR /${relative_path% .* } .html" 
45+     mkdir -p " $( dirname " $output_file " ) " 
46+ 
47+     echo  -e " ${BLUE} 📄 Converting: $spec_file ${NC} " 
48+     echo  -e " ${BLUE}     → Output: ${relative_path% .* } .html${NC} " 
49+ 
50+     if  redocly build-docs " $spec_file " " $output_file " > /dev/null 2>&1 ;  then 
51+         echo  -e " ${GREEN} ✅ Success: $output_file ${NC} " 
52+         (( success_count++ )) 
53+     else 
54+         echo  -e " ${RED} ❌ Failed: $spec_file ${NC} " 
55+         echo  " $spec_file " >>  " $ERROR_LOG " 
56+         (( error_count++ )) 
57+     fi 
58+ }
59+ 
60+ #  Find spec files
61+ mapfile -t spec_files <  <( find " $SPECS_DIR " \(  -name " *.yaml" " *.yml" \)  |  sort) 
62+ echo  -e " ${BLUE} 📊 Found ${# spec_files[@]}  specs${NC} " 
63+ 
64+ #  Convert specs
65+ for  spec_file  in  " ${spec_files[@]} " ;  do 
66+     convert_spec_to_html " $spec_file " ||  true 
67+ done 
68+ 
69+ 
70+ 
71+ #  Generate index.html
72+ cat >  " $INDEX_FILE " <<  'EOF '
73+ <!DOCTYPE html> 
74+ <html lang="en"> 
75+ <head> 
76+     <meta charset="UTF-8"> 
77+     <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
78+     <title>Devtron API Documentation</title> 
79+     <style> 
80+         body { font-family: Arial, sans-serif; margin: 20px; background: #f8f9fa; color: #333; } 
81+         h1 { text-align: center; color: #2c3e50; margin-bottom: 40px; } 
82+         .container { max-width: 1200px; margin: auto; } 
83+         .categories-grid { 
84+             display: flex; 
85+             flex-wrap: wrap; 
86+             justify-content: center; 
87+             gap: 30px; 
88+             margin-top: 20px; 
89+             align-items: stretch; /* Ensures all cards stretch to same height */ 
90+         } 
91+ 
92+         /* Single card centering */ 
93+         .categories-grid.single-card { 
94+             justify-content: center; 
95+             max-width: 400px; 
96+             margin: 20px auto; 
97+         } 
98+ 
99+         /* Category Cards */ 
100+         .category-card { 
101+             background: #fff; 
102+             border-radius: 12px; 
103+             padding: 25px; 
104+             width: calc(33.33% - 30px); 
105+             min-width: 300px; 
106+             max-width: 400px; 
107+             height: auto; 
108+             min-height: 300px; /* Minimum height for consistency */ 
109+             box-shadow: 0 4px 12px rgba(0,0,0,0.1); 
110+             border: 1px solid #e1e5e9; 
111+             transition: transform 0.2s ease, box-shadow 0.2s ease; 
112+             display: flex; 
113+             flex-direction: column; 
114+         } 
115+         .category-card:hover { 
116+             transform: translateY(-2px); 
117+             box-shadow: 0 6px 20px rgba(0,0,0,0.15); 
118+         } 
119+ 
120+         /* Single card styling */ 
121+         .category-card.single { 
122+             width: 100%; 
123+             max-width: 400px; 
124+         } 
125+ 
126+         /* Category Headers */ 
127+         .category-header { 
128+             color: #2c3e50; 
129+             font-size: 1.4em; 
130+             font-weight: bold; 
131+             margin-bottom: 15px; 
132+             padding-bottom: 10px; 
133+             border-bottom: 2px solid #3498db; 
134+             text-align: center; 
135+         } 
136+ 
137+         /* API Links within Categories */ 
138+         .api-links { 
139+             display: flex; 
140+             flex-direction: column; 
141+             gap: 8px; 
142+             flex-grow: 1; /* Takes up remaining space in the card */ 
143+             overflow-y: auto; /* Allows scrolling if too many links */ 
144+             max-height: 400px; /* Maximum height before scrolling */ 
145+         } 
146+         .api-link { 
147+             display: block; 
148+             padding: 8px 12px; 
149+             background: #f8f9fa; 
150+             border-radius: 6px; 
151+             text-decoration: none; 
152+             color: #1a73e8; 
153+             font-weight: 500; 
154+             transition: all 0.2s ease; 
155+             border-left: 3px solid transparent; 
156+         } 
157+         .api-link:hover { 
158+             background: #e3f2fd; 
159+             border-left-color: #1a73e8; 
160+             text-decoration: none; 
161+             transform: translateX(5px); 
162+         } 
163+ 
164+         /* Footer */ 
165+         .footer { 
166+             margin-top: 50px; 
167+             font-size: 0.9rem; 
168+             color: #666; 
169+             text-align: center; 
170+             padding-top: 20px; 
171+             border-top: 1px solid #e1e5e9; 
172+         } 
173+         .footer a { color: #1a73e8; text-decoration: none; } 
174+         .footer a:hover { text-decoration: underline; } 
175+         .timestamp { font-style: italic; } 
176+ 
177+         /* Responsive Design */ 
178+         @media(max-width: 1024px){ 
179+             .category-card { width: calc(50% - 30px); } 
180+             .categories-grid.single-card { max-width: 500px; } 
181+         } 
182+         @media(max-width: 768px){ 
183+             .category-card { width: 100%; min-width: unset; max-width: none; } 
184+             .categories-grid.single-card { max-width: 100%; margin: 20px 0; } 
185+         } 
186+         @media(max-width: 480px){ 
187+             .category-card { margin: 0 10px; } 
188+             .categories-grid { gap: 20px; } 
189+         } 
190+     </style> 
191+ </head> 
192+ <body> 
193+     <div class="container"> 
194+         <h1> Devtron API Documentation</h1> 
195+         <div id="categories" class="categories-grid"></div> 
196+         <div class="footer"> 
197+             <p><a href="https://devtron.ai/" target="_blank">Devtron</a></p> 
198+             <p class="timestamp">Last updated: <span id="timestamp"></span></p> 
199+         </div> 
200+     </div> 
201+     <script> 
202+         const apiData = { 
203+ EOF 
204+ 
205+ 
206+ 
207+ #  Populate apiData preserving folder structure
208+ for  spec_file  in  " ${spec_files[@]} " ;  do 
209+     relative_path=" ${spec_file# $SPECS_DIR / } " 
210+     html_file=" ${relative_path% .* } .html" 
211+     category=$( dirname " $relative_path " ) 
212+     [[ " $category " ==  " ." &&  category=" Root" 
213+ 
214+     display_category=$( echo " $category " |  sed ' s/[-_]/ /g' |  sed ' s/\([a-z]\)\([A-Z]\)/\1 \2/g' |  sed ' s/\b\w/\U&/g' ) 
215+     title=$( grep -m 1 ' ^[[:space:]]*title:' " $spec_file " |  sed ' s/^[[:space:]]*title:[[:space:]]*//' |  tr -d ' "' ||  echo  " ${relative_path% .* } " ) 
216+ 
217+     #  Only include if HTML file was successfully generated
218+     if  [[ -f  " $OUTPUT_DIR /$html_file " ;  then 
219+         #  Ensure proper relative path from index.html to the generated HTML file
220+         #  Since index.html is in docs/api-docs/ and HTML files maintain folder structure
221+         echo  "  \" ${category} _$( basename " ${relative_path% .* } " ) \" : {\" category\" : \" ${display_category} \" , \" title\" : \" ${title} \" , \" filename\" : \" ${html_file} \" }," >>  " $INDEX_FILE " 
222+     fi 
223+ done 
224+ 
225+ sed -i ' $ s/,$//' " $INDEX_FILE " 
226+ 
227+ 
228+ 
229+ cat >>  " $INDEX_FILE " <<  'EOF '
230+         }; 
231+ 
232+         function populatePage() { 
233+             const container = document.getElementById('categories'); 
234+             const categories = {}; 
235+ 
236+             // Group APIs by category 
237+             Object.values(apiData).forEach(api => { 
238+                 if (!categories[api.category]) categories[api.category] = []; 
239+                 categories[api.category].push(api); 
240+             }); 
241+ 
242+             const categoryNames = Object.keys(categories).sort(); 
243+ 
244+             // Add class for single card centering 
245+             if (categoryNames.length === 1) { 
246+                 container.classList.add('single-card'); 
247+             } 
248+ 
249+             // Create category cards 
250+             categoryNames.forEach(categoryName => { 
251+                 // Create category card 
252+                 const categoryCard = document.createElement('div'); 
253+                 categoryCard.className = 'category-card'; 
254+ 
255+                 // Add single class if only one card 
256+                 if (categoryNames.length === 1) { 
257+                     categoryCard.classList.add('single'); 
258+                 } 
259+ 
260+                 // Create category header 
261+                 const categoryHeader = document.createElement('div'); 
262+                 categoryHeader.className = 'category-header'; 
263+                 categoryHeader.textContent = categoryName; 
264+                 categoryCard.appendChild(categoryHeader); 
265+ 
266+                 // Create links container 
267+                 const linksContainer = document.createElement('div'); 
268+                 linksContainer.className = 'api-links'; 
269+ 
270+                 // Add API links to this category 
271+                 categories[categoryName] 
272+                     .sort((a, b) => a.title.localeCompare(b.title)) 
273+                     .forEach(api => { 
274+                         const apiLink = document.createElement('a'); 
275+                         // Ensure proper relative path 
276+                         apiLink.href = api.filename; 
277+                         apiLink.textContent = api.title; 
278+                         apiLink.className = 'api-link'; 
279+                         apiLink.title = `View ${api.title} API documentation`; 
280+ 
281+                         // Add click handler to check if file exists 
282+                         apiLink.addEventListener('click', function(e) { 
283+                             // Let the browser handle the navigation normally 
284+                             // This is just for debugging - remove in production if needed 
285+                             console.log(`Navigating to: ${api.filename}`); 
286+                         }); 
287+ 
288+                         linksContainer.appendChild(apiLink); 
289+                     }); 
290+ 
291+                 categoryCard.appendChild(linksContainer); 
292+                 container.appendChild(categoryCard); 
293+             }); 
294+ 
295+             document.getElementById('timestamp').textContent = new Date().toLocaleString(); 
296+         } 
297+ 
298+         document.addEventListener('DOMContentLoaded', populatePage); 
299+     </script> 
300+ </body> 
301+ </html> 
302+ EOF 
303+ 
304+ 
305+ 
306+ echo  -e " ${GREEN} ✅ Card-based index page generated: $INDEX_FILE ${NC} " 
307+ 
308+ #  === SUMMARY ===
309+ echo  -e " ${BLUE} 📊 Final Summary:${NC} " 
310+ echo  -e " ${GREEN} ✅ Successfully converted: $success_count  specs${NC} " 
311+ if  ((  error_count >  0  )) ;  then 
312+     echo  -e " ${RED} ❌ Failed: $error_count  (see $ERROR_LOG )${NC} " 
313+ fi 
314+ echo  -e " ${BLUE} 📁 Output directory: $OUTPUT_DIR ${NC} " 
315+ echo  -e " ${BLUE} 🌐 Main index: $INDEX_FILE ${NC} " 
316+ 
317+ #  === CREATE README ===
318+ cat >  " $OUTPUT_DIR /README.md" <<  'EOF '
319+ # Devtron API Documentation 
320+ 
321+ This folder contains the HTML documentation generated from the OpenAPI specs in the `specs` directory. 
322+ EOF 
323+ 
324+ echo  -e " ${GREEN} ✅ README created: $OUTPUT_DIR /README.md${NC} " 
325+ echo  -e " ${GREEN} 🎉 API documentation generation complete!${NC} " 
0 commit comments