package server import ( "bytes" "fmt" "html/template" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer/html" ) /* Markdown to HTML rendering for README.md display. * Uses goldmark with GFM extensions (tables, strikethrough, etc.) */ var md goldmark.Markdown func init() { md = goldmark.New( goldmark.WithExtensions( extension.GFM, // GitHub Flavored Markdown extension.Typographer, // smart quotes, etc. ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), // auto-generate heading IDs ), goldmark.WithRendererOptions( html.WithHardWraps(), html.WithXHTML(), html.WithUnsafe(), // allow raw HTML in markdown ), ) } const pageTemplate = ` {{.Title}} - larc

{{.RepoName}} {{if .Revision}}r{{.Revision}}{{end}}

{{.Content}}
` var tmpl *template.Template func init() { tmpl = template.Must(template.New("page").Parse(pageTemplate)) } // PageData contains data for rendering a page type PageData struct { Title string RepoName string Revision int64 Content template.HTML } // RenderMarkdown converts markdown to HTML func RenderMarkdown(markdown []byte) ([]byte, error) { var buf bytes.Buffer if err := md.Convert(markdown, &buf); err != nil { return nil, fmt.Errorf("render markdown: %w", err) } return buf.Bytes(), nil } // RenderPage renders a full HTML page func RenderPage(data *PageData) ([]byte, error) { var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { return nil, fmt.Errorf("render page: %w", err) } return buf.Bytes(), nil } // RenderReadme renders README.md as a full HTML page func RenderReadme(repoName string, revision int64, readmeContent []byte) ([]byte, error) { htmlContent, err := RenderMarkdown(readmeContent) if err != nil { return nil, err } data := &PageData{ Title: repoName, RepoName: repoName, Revision: revision, Content: template.HTML(htmlContent), } return RenderPage(data) } const treeTemplate = ` {{.RepoName}} - Files

{{.RepoName}} r{{.Revision}}

{{if .Path}} {{end}}
{{if .Entries}} {{range .Dirs}} {{end}} {{range .Files}}
{{.Name}} {{.SizeStr}}
{{end}} {{else}}
Empty directory
{{end}}
` // TreeEntry for template type TreeEntryView struct { Name string Path string Size int64 SizeStr string IsDir bool } // Breadcrumb for navigation type Breadcrumb struct { Name string URL string } // TreeData for tree template type TreeData struct { RepoName string Revision int64 Path string Breadcrumbs []Breadcrumb Entries bool Dirs []TreeEntryView Files []TreeEntryView } var treeTmpl *template.Template func init() { treeTmpl = template.Must(template.New("tree").Parse(treeTemplate)) } func formatSize(size int64) string { if size < 1024 { return fmt.Sprintf("%d B", size) } else if size < 1024*1024 { return fmt.Sprintf("%.1f KB", float64(size)/1024) } else if size < 1024*1024*1024 { return fmt.Sprintf("%.1f MB", float64(size)/(1024*1024)) } return fmt.Sprintf("%.1f GB", float64(size)/(1024*1024*1024)) } // RenderTree renders file tree as HTML func RenderTree(data *TreeData) ([]byte, error) { var buf bytes.Buffer if err := treeTmpl.Execute(&buf, data); err != nil { return nil, fmt.Errorf("render tree: %w", err) } return buf.Bytes(), nil } const logTemplate = ` {{.RepoName}} - Log

{{.RepoName}}

{{if .Commits}} {{range .Commits}}
r{{.Number}} {{.Branch}} {{.Author}} {{.DateStr}}
{{.Message}}
{{end}} {{else}}
No commits yet
{{end}}
{{if or .HasPrev .HasNext}} {{end}}
` // CommitView for template type CommitView struct { Number int64 Branch string Author string Message string DateStr string } // LogData for log template type LogData struct { RepoName string LatestRev int64 Commits []CommitView HasPrev bool HasNext bool PrevOffset int NextOffset int } var logTmpl *template.Template func init() { logTmpl = template.Must(template.New("log").Parse(logTemplate)) } // RenderLog renders commit log as HTML func RenderLog(data *LogData) ([]byte, error) { var buf bytes.Buffer if err := logTmpl.Execute(&buf, data); err != nil { return nil, fmt.Errorf("render log: %w", err) } return buf.Bytes(), nil } /* Home page templates for server index */ const homePageTemplate = ` larc

larc

{{.Content}}
{{if .Repos}}

Repositories

{{range .Repos}}
{{.Name}} {{if .Description}}{{.Description}}{{end}} {{if .Public}}public{{end}}
{{end}}
{{end}}
` const repoListTemplate = ` larc

larc

Repositories

{{if .Repos}} {{range .Repos}}
{{.Name}} {{if .Description}}{{.Description}}{{end}} {{if .Public}}public{{end}}
{{end}} {{else}}
No repositories configured
{{end}}
` // HomePageData for home page template type HomePageData struct { Content template.HTML Repos []RepoConfig } // RepoListData for repo list template type RepoListData struct { Repos []RepoConfig } var homePageTmpl *template.Template var repoListTmpl *template.Template func init() { homePageTmpl = template.Must(template.New("homepage").Parse(homePageTemplate)) repoListTmpl = template.Must(template.New("repolist").Parse(repoListTemplate)) } // RenderHomePage renders home page with custom README.md func RenderHomePage(readmeContent []byte, repos []RepoConfig) ([]byte, error) { htmlContent, err := RenderMarkdown(readmeContent) if err != nil { return nil, err } data := &HomePageData{ Content: template.HTML(htmlContent), Repos: repos, } var buf bytes.Buffer if err := homePageTmpl.Execute(&buf, data); err != nil { return nil, fmt.Errorf("render home page: %w", err) } return buf.Bytes(), nil } // RenderRepoList renders repository list page func RenderRepoList(repos []RepoConfig) ([]byte, error) { data := &RepoListData{ Repos: repos, } var buf bytes.Buffer if err := repoListTmpl.Execute(&buf, data); err != nil { return nil, fmt.Errorf("render repo list: %w", err) } return buf.Bytes(), nil }