diff --git a/page_home.go b/page_home.go new file mode 100644 index 0000000..00c598a --- /dev/null +++ b/page_home.go @@ -0,0 +1,90 @@ +package main + +import ( + "fmt" + "html" + "net/http" + "net/url" + "strings" +) + +func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) { + + this.reposMut.RLock() + defer this.reposMut.RUnlock() + + if len(this.reposCache) == 0 { + // We haven't loaded the repositories from Gitea yet + this.Delay(w, r) + return + } + + // Ready for template + + this.Templatepage(w, r, "", "", func() { + fmt.Fprint(w, ` + `+this.cfg.Template.HomepageHeaderHTML+` + + +

Projects (`+fmt.Sprintf("%d", len(this.reposCache))+`)

+ +`) + for _, repo := range this.reposCache { + pageHref := html.EscapeString(`/` + url.PathEscape(repo.Name)) + + normalisedDesc := repo.Description + normalisedDesc = strings.TrimRight(repo.Description, `.`) + if len(normalisedDesc) > 0 { + // Lowercase the first letter of the description, unless it starts with an acronym (all letters uppercase first word) or CamelCase word + firstWord := strings.SplitN(normalisedDesc, " ", 2)[0] + isAcronymOrCamelCase := len(firstWord) > 1 && (firstWord[1:] != strings.ToLower(firstWord[1:])) + + if !(isAcronymOrCamelCase || firstWord == `Go`) { + normalisedDesc = strings.ToLower(normalisedDesc[0:1]) + normalisedDesc[1:] + } + + // Add leading `` to separate from the repo title + normalisedDesc = `, ` + normalisedDesc + } + + rowClass := "" + for _, topic := range repo.topics { + rowClass += `taggedWith-` + topic + ` ` + } + + fmt.Fprint(w, ` + + + + + `) + } + fmt.Fprint(w, ` +
+ + + `+html.EscapeString(repo.Name)+``+html.EscapeString(normalisedDesc)+` + more... +
+ + `) + for _, topic := range repo.topics { + fmt.Fprint(w, ``+html.EscapeString(topic)+` `) + } + fmt.Fprint(w, ` + +
+ `) + }) +} diff --git a/page_imageredir.go b/page_imageredir.go new file mode 100644 index 0000000..aab77e5 --- /dev/null +++ b/page_imageredir.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "net/http" +) + +func (this *Application) Bannerpage(w http.ResponseWriter, r *http.Request, repoName string) { + ctx := r.Context() + + images, err := this.imageFilesForRepo(ctx, repoName) + if err != nil { + this.internalError(w, r, fmt.Errorf("listing images: %w", err)) + return + } + + if len(images) == 0 { + w.Header().Set(`Location`, `/static/no_image.png`) + w.WriteHeader(301) + return + } + + w.Header().Set(`Location`, images[0].RawURL) + w.WriteHeader(301) +} diff --git a/page_repository.go b/page_repository.go new file mode 100644 index 0000000..ca3ccf3 --- /dev/null +++ b/page_repository.go @@ -0,0 +1,109 @@ +package main + +import ( + "bytes" + "fmt" + "html" + "net/http" + "net/url" + "strings" +) + +func (this *Application) Repopage(w http.ResponseWriter, r *http.Request, repoName string) { + ctx := r.Context() + repoURL := this.cfg.Gitea.URL + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repoName) + extraHead := "" + + readme, err := this.repoFile(ctx, repoName, `README.md`) + if err != nil { + this.internalError(w, r, fmt.Errorf("loading README.md: %w", err)) + return + } + + lines := strings.Split(string(readme), "\n") + + // Check if this repo has the 'article' tag + hasArticleTag := false + this.reposMut.RLock() + for _, rr := range this.reposCache { + if rr.Name != repoName { + continue + } + for _, topic := range rr.topics { + if topic == "article" { + hasArticleTag = true + break + } + } + } + this.reposMut.RUnlock() + + // We add some extra badges based on special text entries + extraBadgesMd := `` + if !hasArticleTag { + extraBadgesMd += ` %%REPLACEME__BADGE%%` + } + extraBadgesMd += ` [![](https://img.shields.io/badge/vcs-git-green?logo=git)](` + repoURL + `)` + + // Inject more badges to 3rd line; or, create badges on 3rd line if there are none already + if len(lines) >= 3 && strings.Contains(lines[2], `shields.io`) { + lines[2] += ` ` + extraBadgesMd + } else { + // Push other lines down + lines = append([]string{lines[0], lines[1], extraBadgesMd, ""}, lines[2:]...) + } + + readmeHtml, err := this.renderMarkdown(ctx, repoName, strings.Join(lines, "\n")) + if err != nil { + this.internalError(w, r, fmt.Errorf("rendering markdown: %w", err)) + return + } + + readmeHtml = []byte(strings.Replace(string(readmeHtml), `%%REPLACEME__BADGE%%`, ``, 1)) + + images, err := this.imageFilesForRepo(ctx, repoName) + if err != nil { + this.internalError(w, r, fmt.Errorf("listing images: %w", err)) + return + } + + // If the Git repository contains a top-level go.mod file, allow vanity imports + if goMod, err := this.repoFile(ctx, repoName, `go.mod`); err == nil { + + // Check the first line should be `module MODULENAME\n` + firstLine := bytes.SplitN(goMod, []byte("\n"), 2)[0] + if bytes.HasPrefix(firstLine, []byte("module ")) { + moduleName := firstLine[7:] + extraHead = `` + } + } + + // De-escalate all headers in rendered markdown to match our style + repl := strings.NewReplacer(``, ``, ``, ``, ``, ``) + + // Ready for template + + this.Templatepage(w, r, repoName, extraHead, func() { + + projBodyclass := `projbody` + if len(images) > 0 { + projBodyclass += ` projbody_halfw` + } + + fmt.Fprint(w, `
`) + repl.WriteString(w, string(readmeHtml)) + fmt.Fprint(w, `
`) + + if len(images) > 0 { + fmt.Fprint(w, `
`) + for _, img := range images { + fmt.Fprint(w, ``) + } + fmt.Fprint(w, `
`) + } + + fmt.Fprint(w, `
`) + fmt.Fprint(w, `
`) // projbody + }) +} diff --git a/page_util.go b/page_util.go new file mode 100644 index 0000000..f9b0408 --- /dev/null +++ b/page_util.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "html" + "log" + "net/http" +) + +func (this *Application) Templatepage(w http.ResponseWriter, r *http.Request, pageDesc, extraHead string, cb func()) { + + pageTitle := this.cfg.Template.AppName + if pageDesc != "" { + pageTitle = pageDesc + ` | ` + pageTitle + } + + w.Header().Set(`Content-Type`, `text/html; charset=UTF-8`) + w.WriteHeader(200) + fmt.Fprint(w, ` + + + + + + + `+html.EscapeString(pageTitle)+` + `+extraHead+` + + + + + +
+
+

`+html.EscapeString(this.cfg.Template.AppName)+`

+ `) + cb() + fmt.Fprint(w, ` + + + +`) + +} + +func (this *Application) internalError(w http.ResponseWriter, r *http.Request, err error) { + log.Printf("%s %s: %s", r.Method, r.URL.Path, err) + http.Error(w, "An internal error occurred.", 500) +} + +func (this *Application) Delay(w http.ResponseWriter, r *http.Request) { + this.Templatepage(w, r, "Loading...", "", func() { + fmt.Fprintf(w, ` +

Loading, please wait...

+ + + `) + }) +} diff --git a/pages.go b/pages.go deleted file mode 100644 index 71d44fc..0000000 --- a/pages.go +++ /dev/null @@ -1,261 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "html" - "log" - "net/http" - "net/url" - "strings" -) - -func (this *Application) Templatepage(w http.ResponseWriter, r *http.Request, pageDesc, extraHead string, cb func()) { - - pageTitle := this.cfg.Template.AppName - if pageDesc != "" { - pageTitle = pageDesc + ` | ` + pageTitle - } - - w.Header().Set(`Content-Type`, `text/html; charset=UTF-8`) - w.WriteHeader(200) - fmt.Fprint(w, ` - - - - - - - `+html.EscapeString(pageTitle)+` - `+extraHead+` - - - - - -
-
-

`+html.EscapeString(this.cfg.Template.AppName)+`

- `) - cb() - fmt.Fprint(w, ` - - - -`) - -} - -func (this *Application) internalError(w http.ResponseWriter, r *http.Request, err error) { - log.Printf("%s %s: %s", r.Method, r.URL.Path, err) - http.Error(w, "An internal error occurred.", 500) -} - -func (this *Application) Delay(w http.ResponseWriter, r *http.Request) { - this.Templatepage(w, r, "Loading...", "", func() { - fmt.Fprintf(w, ` -

Loading, please wait...

- - - `) - }) -} - -func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) { - - this.reposMut.RLock() - defer this.reposMut.RUnlock() - - if len(this.reposCache) == 0 { - // We haven't loaded the repositories from Gitea yet - this.Delay(w, r) - return - } - - // Ready for template - - this.Templatepage(w, r, "", "", func() { - fmt.Fprint(w, ` - `+this.cfg.Template.HomepageHeaderHTML+` - - -

Projects (`+fmt.Sprintf("%d", len(this.reposCache))+`)

- -`) - for _, repo := range this.reposCache { - pageHref := html.EscapeString(`/` + url.PathEscape(repo.Name)) - - normalisedDesc := repo.Description - normalisedDesc = strings.TrimRight(repo.Description, `.`) - if len(normalisedDesc) > 0 { - // Lowercase the first letter of the description, unless it starts with an acronym (all letters uppercase first word) or CamelCase word - firstWord := strings.SplitN(normalisedDesc, " ", 2)[0] - isAcronymOrCamelCase := len(firstWord) > 1 && (firstWord[1:] != strings.ToLower(firstWord[1:])) - - if !(isAcronymOrCamelCase || firstWord == `Go`) { - normalisedDesc = strings.ToLower(normalisedDesc[0:1]) + normalisedDesc[1:] - } - - // Add leading `` to separate from the repo title - normalisedDesc = `, ` + normalisedDesc - } - - rowClass := "" - for _, topic := range repo.topics { - rowClass += `taggedWith-` + topic + ` ` - } - - fmt.Fprint(w, ` - - - - - `) - } - fmt.Fprint(w, ` -
- - - `+html.EscapeString(repo.Name)+``+html.EscapeString(normalisedDesc)+` - more... -
- - `) - for _, topic := range repo.topics { - fmt.Fprint(w, ``+html.EscapeString(topic)+` `) - } - fmt.Fprint(w, ` - -
- `) - }) -} - -func (this *Application) Bannerpage(w http.ResponseWriter, r *http.Request, repoName string) { - ctx := r.Context() - - images, err := this.imageFilesForRepo(ctx, repoName) - if err != nil { - this.internalError(w, r, fmt.Errorf("listing images: %w", err)) - return - } - - if len(images) == 0 { - w.Header().Set(`Location`, `/static/no_image.png`) - w.WriteHeader(301) - return - } - - w.Header().Set(`Location`, images[0].RawURL) - w.WriteHeader(301) -} - -func (this *Application) Repopage(w http.ResponseWriter, r *http.Request, repoName string) { - ctx := r.Context() - repoURL := this.cfg.Gitea.URL + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repoName) - extraHead := "" - - readme, err := this.repoFile(ctx, repoName, `README.md`) - if err != nil { - this.internalError(w, r, fmt.Errorf("loading README.md: %w", err)) - return - } - - lines := strings.Split(string(readme), "\n") - - // Check if this repo has the 'article' tag - hasArticleTag := false - this.reposMut.RLock() - for _, rr := range this.reposCache { - if rr.Name != repoName { - continue - } - for _, topic := range rr.topics { - if topic == "article" { - hasArticleTag = true - break - } - } - } - this.reposMut.RUnlock() - - // We add some extra badges based on special text entries - extraBadgesMd := `` - if !hasArticleTag { - extraBadgesMd += ` %%REPLACEME__BADGE%%` - } - extraBadgesMd += ` [![](https://img.shields.io/badge/vcs-git-green?logo=git)](` + repoURL + `)` - - // Inject more badges to 3rd line; or, create badges on 3rd line if there are none already - if len(lines) >= 3 && strings.Contains(lines[2], `shields.io`) { - lines[2] += ` ` + extraBadgesMd - } else { - // Push other lines down - lines = append([]string{lines[0], lines[1], extraBadgesMd, ""}, lines[2:]...) - } - - readmeHtml, err := this.renderMarkdown(ctx, repoName, strings.Join(lines, "\n")) - if err != nil { - this.internalError(w, r, fmt.Errorf("rendering markdown: %w", err)) - return - } - - readmeHtml = []byte(strings.Replace(string(readmeHtml), `%%REPLACEME__BADGE%%`, ``, 1)) - - images, err := this.imageFilesForRepo(ctx, repoName) - if err != nil { - this.internalError(w, r, fmt.Errorf("listing images: %w", err)) - return - } - - // If the Git repository contains a top-level go.mod file, allow vanity imports - if goMod, err := this.repoFile(ctx, repoName, `go.mod`); err == nil { - - // Check the first line should be `module MODULENAME\n` - firstLine := bytes.SplitN(goMod, []byte("\n"), 2)[0] - if bytes.HasPrefix(firstLine, []byte("module ")) { - moduleName := firstLine[7:] - extraHead = `` - } - } - - // De-escalate all headers in rendered markdown to match our style - repl := strings.NewReplacer(``, ``, ``, ``, ``, ``) - - // Ready for template - - this.Templatepage(w, r, repoName, extraHead, func() { - - projBodyclass := `projbody` - if len(images) > 0 { - projBodyclass += ` projbody_halfw` - } - - fmt.Fprint(w, `
`) - repl.WriteString(w, string(readmeHtml)) - fmt.Fprint(w, `
`) - - if len(images) > 0 { - fmt.Fprint(w, `
`) - for _, img := range images { - fmt.Fprint(w, ``) - } - fmt.Fprint(w, `
`) - } - - fmt.Fprint(w, `
`) - fmt.Fprint(w, `
`) // projbody - }) -}