@@ -3,7 +3,7 @@ package teapot
33import (
44 "encoding/json"
55 "fmt"
6- "html"
6+ "html/template "
77 "io"
88 "log"
99 "net/http"
@@ -125,52 +125,35 @@ func NewListRoutesHandlerWithRoutes(routes []RouteInfo, opts ListRoutesOptions)
125125 }
126126}
127127
128- func renderListRoutes (w http.ResponseWriter , req * http.Request , filteredRoutes []RouteInfo , baseURL string ) {
129- // Check Accept header for JSON vs HTML
130- accept := req .Header .Get ("Accept" )
131- if strings .Contains (accept , "application/json" ) {
132- w .Header ().Set ("Content-Type" , "application/json" )
133- err := json .NewEncoder (w ).Encode (map [string ]any {
134- "count" : len (filteredRoutes ),
135- "routes" : filteredRoutes ,
136- })
137- if err != nil {
138- log .Printf ("[teapot-router] failed to encode routes as JSON: %v" , err )
139- http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
140- return
141- }
142- return
143- }
144-
145- // Default to HTML
146- w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
147-
148- // Only show Query/Headers columns when at least one route uses them
149- hasQuery := false
150- hasHeaders := false
151- for _ , rt := range filteredRoutes {
152- if len (rt .QueryParams ) > 0 {
153- hasQuery = true
154- }
155- if len (rt .HeaderParams ) > 0 {
156- hasHeaders = true
157- }
158- if hasQuery && hasHeaders {
159- break
160- }
161- }
128+ // routesPageData is the template data for the routes HTML page.
129+ type routesPageData struct {
130+ Count int
131+ HasQuery bool
132+ HasHeaders bool
133+ Routes []routeRowData
134+ }
162135
163- base := strings .TrimRight (baseURL , "/" )
136+ // routeRowData is the per-route data passed to the routes HTML template.
137+ type routeRowData struct {
138+ MethodClass string
139+ Method string
140+ Pattern string
141+ PatternURL string // non-empty only when the pattern has no path parameters
142+ QueryParams string
143+ HeaderParams string
144+ Name string
145+ Action string
146+ }
164147
165- _ , _ = fmt . Fprintf ( w , `<!DOCTYPE html>
148+ var routesPageTmpl = template . Must ( template . New ( "routes" ). Parse ( `<!DOCTYPE html>
166149<html>
167150<head>
168151 <meta charset="UTF-8">
169152 <title>Routes</title>
170153 <style>
171154 body { font-family: system-ui, -apple-system, sans-serif; margin: 2rem; }
172155 h1 { color: #333; }
173- table { border-collapse: collapse; width: 100%% ; margin-top: 1rem; }
156+ table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
174157 th, td { text-align: left; padding: 0.75rem; border-bottom: 1px solid #ddd; }
175158 th { background-color: #f5f5f5; font-weight: 600; }
176159 tr:hover { background-color: #f9f9f9; }
@@ -193,30 +176,78 @@ func renderListRoutes(w http.ResponseWriter, req *http.Request, filteredRoutes [
193176</head>
194177<body>
195178 <h1>Registered Routes</h1>
196- <p class="count">Total: %d routes</p>
179+ <p class="count">Total: {{.Count}} routes</p>
197180 <table>
198181 <thead>
199182 <tr>
200183 <th>Method</th>
201184 <th>Pattern</th>
202- ` , len (filteredRoutes ))
203- if hasQuery {
204- _ , _ = fmt .Fprint (w , ` <th>Query</th>
205- ` )
206- }
207- if hasHeaders {
208- _ , _ = fmt .Fprint (w , ` <th>Headers</th>
209- ` )
210- }
211- _ , _ = fmt .Fprint (w , ` <th>Name</th>
185+ {{if .HasQuery}}<th>Query</th>{{end}}
186+ {{if .HasHeaders}}<th>Headers</th>{{end}}
187+ <th>Name</th>
212188 <th>Action</th>
213189 </tr>
214190 </thead>
215191 <tbody>
216- ` )
192+ {{range .Routes}}<tr>
193+ <td class="method {{.MethodClass}}">{{.Method}}</td>
194+ <td class="pattern">{{if .PatternURL}}<a href="{{.PatternURL}}">{{.Pattern}}</a>{{else}}{{.Pattern}}{{end}}</td>
195+ {{if $.HasQuery}}<td class="query">{{.QueryParams}}</td>{{end}}
196+ {{if $.HasHeaders}}<td class="query">{{.HeaderParams}}</td>{{end}}
197+ <td class="name">{{.Name}}</td>
198+ <td class="action">{{.Action}}</td>
199+ </tr>
200+ {{end}}
201+ </tbody>
202+ </table>
203+ </body>
204+ </html>` ))
205+
206+ func renderListRoutes (w http.ResponseWriter , req * http.Request , filteredRoutes []RouteInfo , baseURL string ) {
207+ // Check Accept header for JSON vs HTML
208+ accept := req .Header .Get ("Accept" )
209+ if strings .Contains (accept , "application/json" ) {
210+ w .Header ().Set ("Content-Type" , "application/json" )
211+ err := json .NewEncoder (w ).Encode (map [string ]any {
212+ "count" : len (filteredRoutes ),
213+ "routes" : filteredRoutes ,
214+ })
215+ if err != nil {
216+ log .Printf ("[teapot-router] failed to encode routes as JSON: %v" , err )
217+ http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
218+ return
219+ }
220+ return
221+ }
217222
218- for _ , route := range filteredRoutes {
219- methodClass := strings .ToLower (route .Method )
223+ // Default to HTML
224+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
225+
226+ // Only show Query/Headers columns when at least one route uses them.
227+ hasQuery := false
228+ hasHeaders := false
229+ for _ , rt := range filteredRoutes {
230+ if len (rt .QueryParams ) > 0 {
231+ hasQuery = true
232+ }
233+ if len (rt .HeaderParams ) > 0 {
234+ hasHeaders = true
235+ }
236+ if hasQuery && hasHeaders {
237+ break
238+ }
239+ }
240+
241+ base := strings .TrimRight (baseURL , "/" )
242+
243+ data := routesPageData {
244+ Count : len (filteredRoutes ),
245+ HasQuery : hasQuery ,
246+ HasHeaders : hasHeaders ,
247+ Routes : make ([]routeRowData , len (filteredRoutes )),
248+ }
249+
250+ for i , route := range filteredRoutes {
220251 name := route .Name
221252 if name == "" {
222253 name = "-"
@@ -225,41 +256,27 @@ func renderListRoutes(w http.ResponseWriter, req *http.Request, filteredRoutes [
225256 if action == "" {
226257 action = "-"
227258 }
228- patternCell := patternHTML (route .Pattern , base )
229- _ , _ = fmt .Fprintf (w , ` <tr>
230- <td class="method %s">%s</td>
231- <td class="pattern">%s</td>
232- ` , methodClass , html .EscapeString (route .Method ), patternCell )
233- if hasQuery {
234- _ , _ = fmt .Fprintf (w , ` <td class="query">%s</td>
235- ` , html .EscapeString (formatQueryParams (route .QueryParams )))
259+
260+ var patternURL string
261+ if base != "" && ! strings .Contains (route .Pattern , "{" ) {
262+ patternURL = base + route .Pattern
236263 }
237- if hasHeaders {
238- _ , _ = fmt .Fprintf (w , ` <td class="query">%s</td>
239- ` , html .EscapeString (formatHeaderParams (route .HeaderParams )))
264+
265+ data .Routes [i ] = routeRowData {
266+ MethodClass : strings .ToLower (route .Method ),
267+ Method : route .Method ,
268+ Pattern : route .Pattern ,
269+ PatternURL : patternURL ,
270+ QueryParams : formatQueryParams (route .QueryParams ),
271+ HeaderParams : formatHeaderParams (route .HeaderParams ),
272+ Name : name ,
273+ Action : action ,
240274 }
241- _ , _ = fmt .Fprintf (w , ` <td class="name">%s</td>
242- <td class="action">%s</td>
243- </tr>
244- ` , html .EscapeString (name ), html .EscapeString (action ))
245275 }
246276
247- _ , _ = fmt .Fprintf (w , ` </tbody>
248- </table>
249- </body>
250- </html>` )
251- }
252-
253- // patternHTML returns the HTML for a pattern table cell. When base is
254- // non-empty and the pattern contains no path-parameter segments (i.e. no
255- // "{…}"), the pattern is wrapped in an anchor tag pointing to base+pattern.
256- func patternHTML (pattern , base string ) string {
257- escaped := html .EscapeString (pattern )
258- if base == "" || strings .Contains (pattern , "{" ) {
259- return escaped
277+ if err := routesPageTmpl .Execute (w , data ); err != nil {
278+ log .Printf ("[teapot-router] failed to render routes HTML: %v" , err )
260279 }
261- href := html .EscapeString (base + pattern )
262- return fmt .Sprintf (`<a href="%s">%s</a>` , href , escaped )
263280}
264281
265282// FormatRoutesJSON writes routes as JSON to the writer.
0 commit comments