larc r9

894 lines ยท 25.1 KB Raw
1 package server
2
3 import (
4 "bytes"
5 "fmt"
6 "html/template"
7
8 "github.com/yuin/goldmark"
9 "github.com/yuin/goldmark/extension"
10 "github.com/yuin/goldmark/parser"
11 "github.com/yuin/goldmark/renderer/html"
12 )
13
14 /* Markdown to HTML rendering for README.md display.
15 * Uses goldmark with GFM extensions (tables, strikethrough, etc.) */
16
17 var md goldmark.Markdown
18
19 func init() {
20 md = goldmark.New(
21 goldmark.WithExtensions(
22 extension.GFM, // GitHub Flavored Markdown
23 extension.Typographer, // smart quotes, etc.
24 ),
25 goldmark.WithParserOptions(
26 parser.WithAutoHeadingID(), // auto-generate heading IDs
27 ),
28 goldmark.WithRendererOptions(
29 html.WithHardWraps(),
30 html.WithXHTML(),
31 html.WithUnsafe(), // allow raw HTML in markdown
32 ),
33 )
34 }
35
36 const pageTemplate = `<!DOCTYPE html>
37 <html lang="en">
38 <head>
39 <meta charset="UTF-8">
40 <meta name="viewport" content="width=device-width, initial-scale=1.0">
41 <title>{{.Title}} - larc</title>
42 <style>
43 :root {
44 --bg: #0d1117;
45 --fg: #c9d1d9;
46 --border: #30363d;
47 --link: #58a6ff;
48 --code-bg: #161b22;
49 --header-bg: #161b22;
50 }
51 * { box-sizing: border-box; }
52 body {
53 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
54 background: var(--bg);
55 color: var(--fg);
56 line-height: 1.6;
57 margin: 0;
58 padding: 0;
59 }
60 .header {
61 background: var(--header-bg);
62 border-bottom: 1px solid var(--border);
63 padding: 16px 24px;
64 }
65 .header h1 {
66 margin: 0;
67 font-size: 20px;
68 font-weight: 600;
69 }
70 .header h1 a {
71 color: var(--fg);
72 text-decoration: none;
73 }
74 .header .rev {
75 color: #8b949e;
76 font-size: 14px;
77 margin-left: 8px;
78 }
79 .container {
80 max-width: 1012px;
81 margin: 0 auto;
82 padding: 24px;
83 }
84 .readme {
85 background: var(--header-bg);
86 border: 1px solid var(--border);
87 border-radius: 6px;
88 padding: 32px;
89 }
90 .readme h1, .readme h2, .readme h3 {
91 border-bottom: 1px solid var(--border);
92 padding-bottom: 8px;
93 margin-top: 24px;
94 }
95 .readme h1:first-child { margin-top: 0; }
96 .readme a { color: var(--link); }
97 .readme code {
98 background: var(--code-bg);
99 padding: 2px 6px;
100 border-radius: 3px;
101 font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace;
102 font-size: 85%;
103 }
104 .readme pre {
105 background: var(--code-bg);
106 padding: 16px;
107 border-radius: 6px;
108 overflow-x: auto;
109 }
110 .readme pre code {
111 padding: 0;
112 background: none;
113 }
114 .readme table {
115 border-collapse: collapse;
116 width: 100%;
117 }
118 .readme th, .readme td {
119 border: 1px solid var(--border);
120 padding: 8px 12px;
121 text-align: left;
122 }
123 .readme th {
124 background: var(--code-bg);
125 }
126 .readme blockquote {
127 border-left: 4px solid var(--border);
128 margin: 0;
129 padding-left: 16px;
130 color: #8b949e;
131 }
132 .readme img {
133 max-width: 100%;
134 }
135 .readme ul, .readme ol {
136 padding-left: 24px;
137 }
138 .nav {
139 margin-bottom: 16px;
140 font-size: 14px;
141 }
142 .nav a {
143 color: var(--link);
144 text-decoration: none;
145 margin-right: 16px;
146 }
147 .nav a:hover {
148 text-decoration: underline;
149 }
150 </style>
151 </head>
152 <body>
153 <div class="header">
154 <h1>
155 <a href="/{{.RepoName}}">{{.RepoName}}</a>
156 {{if .Revision}}<span class="rev">r{{.Revision}}</span>{{end}}
157 </h1>
158 </div>
159 <div class="container">
160 <div class="nav">
161 <a href="/{{.RepoName}}">README</a>
162 <a href="/{{.RepoName}}/tree/{{.Revision}}/">Files</a>
163 <a href="/{{.RepoName}}/log">Log</a>
164 </div>
165 <div class="readme">
166 {{.Content}}
167 </div>
168 </div>
169 </body>
170 </html>`
171
172 var tmpl *template.Template
173
174 func init() {
175 tmpl = template.Must(template.New("page").Parse(pageTemplate))
176 }
177
178 // PageData contains data for rendering a page
179 type PageData struct {
180 Title string
181 RepoName string
182 Revision int64
183 Content template.HTML
184 }
185
186 // RenderMarkdown converts markdown to HTML
187 func RenderMarkdown(markdown []byte) ([]byte, error) {
188 var buf bytes.Buffer
189 if err := md.Convert(markdown, &buf); err != nil {
190 return nil, fmt.Errorf("render markdown: %w", err)
191 }
192 return buf.Bytes(), nil
193 }
194
195 // RenderPage renders a full HTML page
196 func RenderPage(data *PageData) ([]byte, error) {
197 var buf bytes.Buffer
198 if err := tmpl.Execute(&buf, data); err != nil {
199 return nil, fmt.Errorf("render page: %w", err)
200 }
201 return buf.Bytes(), nil
202 }
203
204 // RenderReadme renders README.md as a full HTML page
205 func RenderReadme(repoName string, revision int64, readmeContent []byte) ([]byte, error) {
206 htmlContent, err := RenderMarkdown(readmeContent)
207 if err != nil {
208 return nil, err
209 }
210
211 data := &PageData{
212 Title: repoName,
213 RepoName: repoName,
214 Revision: revision,
215 Content: template.HTML(htmlContent),
216 }
217
218 return RenderPage(data)
219 }
220
221 const treeTemplate = `<!DOCTYPE html>
222 <html lang="en">
223 <head>
224 <meta charset="UTF-8">
225 <meta name="viewport" content="width=device-width, initial-scale=1.0">
226 <title>{{.RepoName}} - Files</title>
227 <style>
228 :root {
229 --bg: #0d1117;
230 --fg: #c9d1d9;
231 --fg-muted: #8b949e;
232 --border: #30363d;
233 --link: #58a6ff;
234 --header-bg: #161b22;
235 --row-hover: #161b22;
236 }
237 * { box-sizing: border-box; }
238 body {
239 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
240 background: var(--bg);
241 color: var(--fg);
242 line-height: 1.5;
243 margin: 0;
244 }
245 .header {
246 background: var(--header-bg);
247 border-bottom: 1px solid var(--border);
248 padding: 16px 24px;
249 }
250 .header h1 { margin: 0; font-size: 20px; }
251 .header h1 a { color: var(--fg); text-decoration: none; }
252 .header .rev { color: var(--fg-muted); font-size: 14px; margin-left: 8px; }
253 .container { max-width: 1012px; margin: 0 auto; padding: 24px; }
254 .nav { margin-bottom: 16px; font-size: 14px; }
255 .nav a { color: var(--link); text-decoration: none; margin-right: 16px; }
256 .nav a:hover { text-decoration: underline; }
257 .nav a.active { color: var(--fg); font-weight: 600; }
258 .breadcrumb { margin-bottom: 16px; font-size: 14px; }
259 .breadcrumb a { color: var(--link); text-decoration: none; }
260 .breadcrumb span { color: var(--fg-muted); margin: 0 4px; }
261 .file-tree {
262 background: var(--header-bg);
263 border: 1px solid var(--border);
264 border-radius: 6px;
265 overflow: hidden;
266 }
267 .file-row {
268 display: flex;
269 align-items: center;
270 padding: 8px 16px;
271 border-bottom: 1px solid var(--border);
272 font-size: 14px;
273 }
274 .file-row:last-child { border-bottom: none; }
275 .file-row:hover { background: var(--row-hover); }
276 .file-icon {
277 width: 20px;
278 margin-right: 8px;
279 text-align: center;
280 color: var(--fg-muted);
281 }
282 .file-name { flex: 1; }
283 .file-name a { color: var(--fg); text-decoration: none; }
284 .file-name a:hover { color: var(--link); text-decoration: underline; }
285 .file-size { color: var(--fg-muted); font-size: 12px; min-width: 80px; text-align: right; }
286 .dir-icon::before { content: "F"; }
287 .file-icon-default::before { content: "D"; }
288 .empty { padding: 32px; text-align: center; color: var(--fg-muted); }
289 </style>
290 </head>
291 <body>
292 <div class="header">
293 <h1>
294 <a href="/{{.RepoName}}">{{.RepoName}}</a>
295 <span class="rev">r{{.Revision}}</span>
296 </h1>
297 </div>
298 <div class="container">
299 <div class="nav">
300 <a href="/{{.RepoName}}">README</a>
301 <a href="/{{.RepoName}}/tree/{{.Revision}}/" class="active">Files</a>
302 <a href="/{{.RepoName}}/log">Log</a>
303 </div>
304 {{if .Path}}
305 <div class="breadcrumb">
306 <a href="/{{.RepoName}}/tree/{{.Revision}}/">root</a>
307 {{range .Breadcrumbs}}<span>/</span><a href="{{.URL}}">{{.Name}}</a>{{end}}
308 </div>
309 {{end}}
310 <div class="file-tree">
311 {{if .Entries}}
312 {{range .Dirs}}
313 <div class="file-row">
314 <span class="file-icon dir-icon"></span>
315 <span class="file-name"><a href="/{{$.RepoName}}/tree/{{$.Revision}}/{{.Path}}">{{.Name}}/</a></span>
316 <span class="file-size">โ€”</span>
317 </div>
318 {{end}}
319 {{range .Files}}
320 <div class="file-row">
321 <span class="file-icon file-icon-default"></span>
322 <span class="file-name"><a href="/{{$.RepoName}}/blob/{{$.Revision}}/{{.Path}}">{{.Name}}</a></span>
323 <span class="file-size">{{.SizeStr}}</span>
324 </div>
325 {{end}}
326 {{else}}
327 <div class="empty">Empty directory</div>
328 {{end}}
329 </div>
330 </div>
331 </body>
332 </html>`
333
334 // TreeEntry for template
335 type TreeEntryView struct {
336 Name string
337 Path string
338 Size int64
339 SizeStr string
340 IsDir bool
341 }
342
343 // Breadcrumb for navigation
344 type Breadcrumb struct {
345 Name string
346 URL string
347 }
348
349 // TreeData for tree template
350 type TreeData struct {
351 RepoName string
352 Revision int64
353 Path string
354 Breadcrumbs []Breadcrumb
355 Entries bool
356 Dirs []TreeEntryView
357 Files []TreeEntryView
358 }
359
360 var treeTmpl *template.Template
361
362 func init() {
363 treeTmpl = template.Must(template.New("tree").Parse(treeTemplate))
364 }
365
366 func formatSize(size int64) string {
367 if size < 1024 {
368 return fmt.Sprintf("%d B", size)
369 } else if size < 1024*1024 {
370 return fmt.Sprintf("%.1f KB", float64(size)/1024)
371 } else if size < 1024*1024*1024 {
372 return fmt.Sprintf("%.1f MB", float64(size)/(1024*1024))
373 }
374 return fmt.Sprintf("%.1f GB", float64(size)/(1024*1024*1024))
375 }
376
377 // RenderTree renders file tree as HTML
378 func RenderTree(data *TreeData) ([]byte, error) {
379 var buf bytes.Buffer
380 if err := treeTmpl.Execute(&buf, data); err != nil {
381 return nil, fmt.Errorf("render tree: %w", err)
382 }
383 return buf.Bytes(), nil
384 }
385
386 const logTemplate = `<!DOCTYPE html>
387 <html lang="en">
388 <head>
389 <meta charset="UTF-8">
390 <meta name="viewport" content="width=device-width, initial-scale=1.0">
391 <title>{{.RepoName}} - Log</title>
392 <style>
393 :root {
394 --bg: #0d1117;
395 --fg: #c9d1d9;
396 --fg-muted: #8b949e;
397 --border: #30363d;
398 --link: #58a6ff;
399 --header-bg: #161b22;
400 --rev-color: #f0883e;
401 --branch-bg: #388bfd26;
402 --branch-color: #58a6ff;
403 }
404 * { box-sizing: border-box; }
405 body {
406 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
407 background: var(--bg);
408 color: var(--fg);
409 line-height: 1.5;
410 margin: 0;
411 }
412 .header {
413 background: var(--header-bg);
414 border-bottom: 1px solid var(--border);
415 padding: 16px 24px;
416 }
417 .header h1 { margin: 0; font-size: 20px; }
418 .header h1 a { color: var(--fg); text-decoration: none; }
419 .container { max-width: 1012px; margin: 0 auto; padding: 24px; }
420 .nav { margin-bottom: 16px; font-size: 14px; }
421 .nav a { color: var(--link); text-decoration: none; margin-right: 16px; }
422 .nav a:hover { text-decoration: underline; }
423 .nav a.active { color: var(--fg); font-weight: 600; }
424 .commits {
425 background: var(--header-bg);
426 border: 1px solid var(--border);
427 border-radius: 6px;
428 overflow: hidden;
429 }
430 .commit {
431 padding: 16px;
432 border-bottom: 1px solid var(--border);
433 }
434 .commit:last-child { border-bottom: none; }
435 .commit-header {
436 display: flex;
437 align-items: center;
438 margin-bottom: 8px;
439 }
440 .commit-rev {
441 font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace;
442 font-weight: 600;
443 color: var(--rev-color);
444 margin-right: 12px;
445 }
446 .commit-rev a { color: inherit; text-decoration: none; }
447 .commit-rev a:hover { text-decoration: underline; }
448 .commit-branch {
449 background: var(--branch-bg);
450 color: var(--branch-color);
451 padding: 2px 8px;
452 border-radius: 12px;
453 font-size: 12px;
454 margin-right: 12px;
455 }
456 .commit-author {
457 color: var(--fg);
458 font-weight: 500;
459 }
460 .commit-date {
461 color: var(--fg-muted);
462 font-size: 12px;
463 margin-left: auto;
464 }
465 .commit-message {
466 color: var(--fg);
467 font-size: 14px;
468 }
469 .empty { padding: 32px; text-align: center; color: var(--fg-muted); }
470 .pagination {
471 margin-top: 16px;
472 text-align: center;
473 }
474 .pagination a {
475 color: var(--link);
476 text-decoration: none;
477 padding: 8px 16px;
478 border: 1px solid var(--border);
479 border-radius: 6px;
480 margin: 0 4px;
481 }
482 .pagination a:hover { background: var(--header-bg); }
483 </style>
484 </head>
485 <body>
486 <div class="header">
487 <h1><a href="/{{.RepoName}}">{{.RepoName}}</a></h1>
488 </div>
489 <div class="container">
490 <div class="nav">
491 <a href="/{{.RepoName}}">README</a>
492 <a href="/{{.RepoName}}/tree/{{.LatestRev}}/">Files</a>
493 <a href="/{{.RepoName}}/log" class="active">Log</a>
494 </div>
495 <div class="commits">
496 {{if .Commits}}
497 {{range .Commits}}
498 <div class="commit">
499 <div class="commit-header">
500 <span class="commit-rev"><a href="/{{$.RepoName}}/tree/{{.Number}}/">r{{.Number}}</a></span>
501 <span class="commit-branch">{{.Branch}}</span>
502 <span class="commit-author">{{.Author}}</span>
503 <span class="commit-date">{{.DateStr}}</span>
504 </div>
505 <div class="commit-message">{{.Message}}</div>
506 </div>
507 {{end}}
508 {{else}}
509 <div class="empty">No commits yet</div>
510 {{end}}
511 </div>
512 {{if or .HasPrev .HasNext}}
513 <div class="pagination">
514 {{if .HasPrev}}<a href="/{{.RepoName}}/log?offset={{.PrevOffset}}"><- Newer</a>{{end}}
515 {{if .HasNext}}<a href="/{{.RepoName}}/log?offset={{.NextOffset}}">Older -></a>{{end}}
516 </div>
517 {{end}}
518 </div>
519 </body>
520 </html>`
521
522 // CommitView for template
523 type CommitView struct {
524 Number int64
525 Branch string
526 Author string
527 Message string
528 DateStr string
529 }
530
531 // LogData for log template
532 type LogData struct {
533 RepoName string
534 LatestRev int64
535 Commits []CommitView
536 HasPrev bool
537 HasNext bool
538 PrevOffset int
539 NextOffset int
540 }
541
542 var logTmpl *template.Template
543
544 func init() {
545 logTmpl = template.Must(template.New("log").Parse(logTemplate))
546 }
547
548 // RenderLog renders commit log as HTML
549 func RenderLog(data *LogData) ([]byte, error) {
550 var buf bytes.Buffer
551 if err := logTmpl.Execute(&buf, data); err != nil {
552 return nil, fmt.Errorf("render log: %w", err)
553 }
554 return buf.Bytes(), nil
555 }
556
557 /* Home page templates for server index */
558
559 const homePageTemplate = `<!DOCTYPE html>
560 <html lang="en">
561 <head>
562 <meta charset="UTF-8">
563 <meta name="viewport" content="width=device-width, initial-scale=1.0">
564 <title>larc</title>
565 <style>
566 :root {
567 --bg: #0d1117;
568 --fg: #c9d1d9;
569 --fg-muted: #8b949e;
570 --border: #30363d;
571 --link: #58a6ff;
572 --code-bg: #161b22;
573 --header-bg: #161b22;
574 }
575 * { box-sizing: border-box; }
576 body {
577 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
578 background: var(--bg);
579 color: var(--fg);
580 line-height: 1.6;
581 margin: 0;
582 padding: 0;
583 }
584 .header {
585 background: var(--header-bg);
586 border-bottom: 1px solid var(--border);
587 padding: 16px 24px;
588 }
589 .header h1 {
590 margin: 0;
591 font-size: 20px;
592 font-weight: 600;
593 }
594 .header h1 a {
595 color: var(--fg);
596 text-decoration: none;
597 }
598 .container {
599 max-width: 1012px;
600 margin: 0 auto;
601 padding: 24px;
602 }
603 .readme {
604 background: var(--header-bg);
605 border: 1px solid var(--border);
606 border-radius: 6px;
607 padding: 32px;
608 margin-bottom: 24px;
609 }
610 .readme h1, .readme h2, .readme h3 {
611 border-bottom: 1px solid var(--border);
612 padding-bottom: 8px;
613 margin-top: 24px;
614 }
615 .readme h1:first-child { margin-top: 0; }
616 .readme a { color: var(--link); }
617 .readme code {
618 background: var(--code-bg);
619 padding: 2px 6px;
620 border-radius: 3px;
621 font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace;
622 font-size: 85%;
623 }
624 .readme pre {
625 background: var(--code-bg);
626 padding: 16px;
627 border-radius: 6px;
628 overflow-x: auto;
629 }
630 .readme pre code {
631 padding: 0;
632 background: none;
633 }
634 .readme table {
635 border-collapse: collapse;
636 width: 100%;
637 }
638 .readme th, .readme td {
639 border: 1px solid var(--border);
640 padding: 8px 12px;
641 text-align: left;
642 }
643 .readme th {
644 background: var(--code-bg);
645 }
646 .readme blockquote {
647 border-left: 4px solid var(--border);
648 margin: 0;
649 padding-left: 16px;
650 color: #8b949e;
651 }
652 .readme img {
653 max-width: 100%;
654 }
655 .readme ul, .readme ol {
656 padding-left: 24px;
657 }
658 .repos-section h2 {
659 font-size: 16px;
660 margin: 0 0 16px 0;
661 color: var(--fg-muted);
662 }
663 .repo-list {
664 background: var(--header-bg);
665 border: 1px solid var(--border);
666 border-radius: 6px;
667 overflow: hidden;
668 }
669 .repo-item {
670 display: flex;
671 align-items: center;
672 padding: 12px 16px;
673 border-bottom: 1px solid var(--border);
674 }
675 .repo-item:last-child { border-bottom: none; }
676 .repo-item:hover { background: var(--bg); }
677 .repo-name {
678 font-weight: 500;
679 }
680 .repo-name a {
681 color: var(--link);
682 text-decoration: none;
683 }
684 .repo-name a:hover {
685 text-decoration: underline;
686 }
687 .repo-desc {
688 color: var(--fg-muted);
689 font-size: 14px;
690 margin-left: 16px;
691 }
692 .repo-badge {
693 margin-left: auto;
694 font-size: 12px;
695 padding: 2px 8px;
696 border-radius: 12px;
697 background: #388bfd26;
698 color: var(--link);
699 }
700 </style>
701 </head>
702 <body>
703 <div class="header">
704 <h1><a href="/">larc</a></h1>
705 </div>
706 <div class="container">
707 <div class="readme">
708 {{.Content}}
709 </div>
710 {{if .Repos}}
711 <div class="repos-section">
712 <h2>Repositories</h2>
713 <div class="repo-list">
714 {{range .Repos}}
715 <div class="repo-item">
716 <span class="repo-name"><a href="/{{.Name}}">{{.Name}}</a></span>
717 {{if .Description}}<span class="repo-desc">{{.Description}}</span>{{end}}
718 {{if .Public}}<span class="repo-badge">public</span>{{end}}
719 </div>
720 {{end}}
721 </div>
722 </div>
723 {{end}}
724 </div>
725 </body>
726 </html>`
727
728 const repoListTemplate = `<!DOCTYPE html>
729 <html lang="en">
730 <head>
731 <meta charset="UTF-8">
732 <meta name="viewport" content="width=device-width, initial-scale=1.0">
733 <title>larc</title>
734 <style>
735 :root {
736 --bg: #0d1117;
737 --fg: #c9d1d9;
738 --fg-muted: #8b949e;
739 --border: #30363d;
740 --link: #58a6ff;
741 --header-bg: #161b22;
742 }
743 * { box-sizing: border-box; }
744 body {
745 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
746 background: var(--bg);
747 color: var(--fg);
748 line-height: 1.6;
749 margin: 0;
750 padding: 0;
751 }
752 .header {
753 background: var(--header-bg);
754 border-bottom: 1px solid var(--border);
755 padding: 16px 24px;
756 }
757 .header h1 {
758 margin: 0;
759 font-size: 20px;
760 font-weight: 600;
761 }
762 .header h1 a {
763 color: var(--fg);
764 text-decoration: none;
765 }
766 .container {
767 max-width: 1012px;
768 margin: 0 auto;
769 padding: 24px;
770 }
771 .repos-section h2 {
772 font-size: 18px;
773 margin: 0 0 16px 0;
774 }
775 .repo-list {
776 background: var(--header-bg);
777 border: 1px solid var(--border);
778 border-radius: 6px;
779 overflow: hidden;
780 }
781 .repo-item {
782 display: flex;
783 align-items: center;
784 padding: 12px 16px;
785 border-bottom: 1px solid var(--border);
786 }
787 .repo-item:last-child { border-bottom: none; }
788 .repo-item:hover { background: var(--bg); }
789 .repo-name {
790 font-weight: 500;
791 }
792 .repo-name a {
793 color: var(--link);
794 text-decoration: none;
795 }
796 .repo-name a:hover {
797 text-decoration: underline;
798 }
799 .repo-desc {
800 color: var(--fg-muted);
801 font-size: 14px;
802 margin-left: 16px;
803 }
804 .repo-badge {
805 margin-left: auto;
806 font-size: 12px;
807 padding: 2px 8px;
808 border-radius: 12px;
809 background: #388bfd26;
810 color: var(--link);
811 }
812 .empty {
813 padding: 32px;
814 text-align: center;
815 color: var(--fg-muted);
816 }
817 </style>
818 </head>
819 <body>
820 <div class="header">
821 <h1><a href="/">larc</a></h1>
822 </div>
823 <div class="container">
824 <div class="repos-section">
825 <h2>Repositories</h2>
826 <div class="repo-list">
827 {{if .Repos}}
828 {{range .Repos}}
829 <div class="repo-item">
830 <span class="repo-name"><a href="/{{.Name}}">{{.Name}}</a></span>
831 {{if .Description}}<span class="repo-desc">{{.Description}}</span>{{end}}
832 {{if .Public}}<span class="repo-badge">public</span>{{end}}
833 </div>
834 {{end}}
835 {{else}}
836 <div class="empty">No repositories configured</div>
837 {{end}}
838 </div>
839 </div>
840 </div>
841 </body>
842 </html>`
843
844 // HomePageData for home page template
845 type HomePageData struct {
846 Content template.HTML
847 Repos []RepoConfig
848 }
849
850 // RepoListData for repo list template
851 type RepoListData struct {
852 Repos []RepoConfig
853 }
854
855 var homePageTmpl *template.Template
856 var repoListTmpl *template.Template
857
858 func init() {
859 homePageTmpl = template.Must(template.New("homepage").Parse(homePageTemplate))
860 repoListTmpl = template.Must(template.New("repolist").Parse(repoListTemplate))
861 }
862
863 // RenderHomePage renders home page with custom README.md
864 func RenderHomePage(readmeContent []byte, repos []RepoConfig) ([]byte, error) {
865 htmlContent, err := RenderMarkdown(readmeContent)
866 if err != nil {
867 return nil, err
868 }
869
870 data := &HomePageData{
871 Content: template.HTML(htmlContent),
872 Repos: repos,
873 }
874
875 var buf bytes.Buffer
876 if err := homePageTmpl.Execute(&buf, data); err != nil {
877 return nil, fmt.Errorf("render home page: %w", err)
878 }
879 return buf.Bytes(), nil
880 }
881
882 // RenderRepoList renders repository list page
883 func RenderRepoList(repos []RepoConfig) ([]byte, error) {
884 data := &RepoListData{
885 Repos: repos,
886 }
887
888 var buf bytes.Buffer
889 if err := repoListTmpl.Execute(&buf, data); err != nil {
890 return nil, fmt.Errorf("render repo list: %w", err)
891 }
892 return buf.Bytes(), nil
893 }
894