Changes
9 changed files (+257/-196)
-
-
@@ -127,28 +127,18 @@ func (g *GitRepo) LastCommit() (*object.Commit, error) {return c, nil } // FileContent returns the content of a file at the "path" and // whether the file is binary or not. func (g *GitRepo) FileContent(path string) (string, bool, error) { func (g *GitRepo) File(path string) (*object.File, error) { c, err := g.r.CommitObject(g.h) if err != nil { return "", false, fmt.Errorf("commit object: %w", err) return nil, fmt.Errorf("commit object: %w", err) } tree, err := c.Tree() if err != nil { return "", false, fmt.Errorf("file tree: %w", err) return nil, fmt.Errorf("file tree: %w", err) } file, err := tree.File(path) if err != nil { return "", false, err } isbin, _ := file.IsBinary() contents, err := file.Contents() return contents, isbin, err return tree.File(path) } func (g *GitRepo) Tags() ([]*TagReference, error) {
-
-
-
@@ -0,0 +1,75 @@// Copyright 2026 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: MIT package html import ( "bytes" "net/url" "github.com/microcosm-cc/bluemonday" "github.com/russross/blackfriday/v2" ) // MarkdownRenderer is a renderer that takes Markdown code and emits HTML. // Use "NewMarkdownRenderer" to instantiate. type MarkdownRenderer struct { // policy is a sanitization policy. policy *bluemonday.Policy } func NewMarkdownRenderer(policy *bluemonday.Policy) MarkdownRenderer { p := policy if p == nil { p = bluemonday.UGCPolicy() } return MarkdownRenderer{ policy: p, } } func (r *MarkdownRenderer) Render(code []byte, transformer Transformer) ([]byte, error) { parser := blackfriday.New(blackfriday.WithExtensions(blackfriday.CommonExtensions)) tree := parser.Parse(code) if transformer != nil { tree.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { if !entering { return blackfriday.GoToNext } if node.Type == blackfriday.Link || node.Type == blackfriday.Image { href := string(node.LinkData.Destination) parsedURL, err := url.Parse(href) if err == nil && parsedURL.Host != "" { // Full URL, skip. return blackfriday.GoToNext } if node.Type == blackfriday.Image { node.LinkData.Destination = []byte(transformer.RewriteInternalMediaSource(href)) } else { node.LinkData.Destination = []byte(transformer.RewriteInternalLink(href)) } } return blackfriday.GoToNext }) } var writer bytes.Buffer renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{}) renderer.RenderHeader(&writer, tree) tree.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { return renderer.RenderNode(&writer, node, entering) }) renderer.RenderFooter(&writer, tree) unsafe := blackfriday.Run( writer.Bytes(), blackfriday.WithExtensions(blackfriday.CommonExtensions), ) return r.policy.SanitizeBytes(unsafe), nil }
-
-
-
@@ -0,0 +1,33 @@// Copyright 2026 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: MIT package html import ( "fmt" "github.com/microcosm-cc/bluemonday" ) // PlaintextRenderer renders plaintext in a single "<pre>" element. // Use "NewPlaintextRenderer" to instantiate. type PlaintextRenderer struct { policy *bluemonday.Policy } func NewPlaintextRenderer(policy *bluemonday.Policy) PlaintextRenderer { p := policy if p == nil { p = bluemonday.UGCPolicy() } return PlaintextRenderer{ policy: p, } } func (r *PlaintextRenderer) Render(code []byte, _ Transformer) ([]byte, error) { safe := r.policy.SanitizeBytes(code) return fmt.Appendf([]byte{}, "<pre>%s</pre>", safe), nil }
-
-
-
@@ -0,0 +1,18 @@// Copyright 2026 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: MIT package html type Renderer interface { // Render processes "code" then returns HTML text. Render(code []byte, transformer Transformer) ([]byte, error) } type Transformer interface { // RewriteInternalMediaSource takes a site internal link to a media file // then returns a new link. RewriteInternalMediaSource(src string) string // RewriteInternalLink takes a site internal link then returns a new link. RewriteInternalLink(href string) string }
-
-
-
@@ -7,6 +7,7 @@ import ("github.com/microcosm-cc/bluemonday" "github.com/pocka/legit/config" "github.com/pocka/legit/renderer/html" ) // Checks for gitprotocol-http(5) specific smells; if found, passes
-
@@ -39,11 +40,14 @@ func (d *deps) Multiplex(w http.ResponseWriter, r *http.Request) {func Handlers(c *config.Config, staticDir fs.FS, templatesDir fs.FS) *http.ServeMux { mux := http.NewServeMux() ugcPolicy := bluemonday.UGCPolicy() d := deps{ c: c, staticDir: staticDir, templatesDir: templatesDir, ugcPolicy: bluemonday.UGCPolicy(), markdown: html.NewMarkdownRenderer(ugcPolicy), plaintext: html.NewPlaintextRenderer(ugcPolicy), } if !c.CompileTemplatesOnRequest {
-
-
routes/html.go (new)
-
@@ -0,0 +1,60 @@// Copyright 2026 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: MIT package routes import ( "fmt" "path/filepath" "strings" "github.com/go-git/go-git/v5/plumbing/object" "github.com/pocka/legit/renderer/html" ) type RepoLinkTransformer struct { repoName string ref string } func NewRepoLinkTransformer(repoName string, ref string) *RepoLinkTransformer { return &RepoLinkTransformer{ repoName: repoName, ref: ref, } } func (t *RepoLinkTransformer) RewriteInternalMediaSource(src string) string { href := src if strings.IndexByte(href, '/') == 0 { // Repository index page is the "root" for repository's README file. href = "." + href } return fmt.Sprintf("/%s/blob/%s/%s?raw", t.repoName, t.ref, href) } func (t *RepoLinkTransformer) RewriteInternalLink(link string) string { href := link if strings.IndexByte(href, '/') == 0 { // Repository index page is the "root" for repository's README file. href = "." + href } if strings.LastIndexByte(href, '/') == len(href)-1 { return fmt.Sprintf("/%s/tree/%s/%s", t.repoName, t.ref, href[:len(href)-1]) } else { return fmt.Sprintf("/%s/blob/%s/%s", t.repoName, t.ref, href) } } func (d *deps) htmlRenderer(file *object.File) html.Renderer { switch filepath.Ext(file.Name) { case ".md", ".mkd", ".markdown": return &d.markdown default: return nil } }
-
-
routes/preview/renderer.go (deleted)
-
@@ -1,79 +0,0 @@// This file contains preview renderers. // // Copyright 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: MIT package preview import ( "bytes" "net/url" "path/filepath" "github.com/microcosm-cc/bluemonday" "github.com/russross/blackfriday/v2" ) type Renderer interface { GetPreviewType() string Render(code []byte) ([]byte, error) } type MarkdownToHtmlRenderer struct { policy *bluemonday.Policy } func (r MarkdownToHtmlRenderer) GetPreviewType() string { return "html" } func (r MarkdownToHtmlRenderer) Render(code []byte) ([]byte, error) { parser := blackfriday.New(blackfriday.WithExtensions(blackfriday.CommonExtensions)) tree := parser.Parse(code) tree.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { if !entering { return blackfriday.GoToNext } if node.Type == blackfriday.Image { src := string(node.LinkData.Destination) parsedURL, err := url.Parse(src) if err == nil && parsedURL.Host != "" { // External URL, skip. return blackfriday.GoToNext } queries := parsedURL.Query() queries.Add("raw", "") parsedURL.RawQuery = queries.Encode() node.LinkData.Destination = []byte(parsedURL.String()) } return blackfriday.GoToNext }) var writer bytes.Buffer renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{}) renderer.RenderHeader(&writer, tree) tree.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { return renderer.RenderNode(&writer, node, entering) }) renderer.RenderFooter(&writer, tree) unsafe := blackfriday.Run(writer.Bytes(), blackfriday.WithExtensions(blackfriday.CommonExtensions)) return r.policy.SanitizeBytes(unsafe), nil } func GetPreviewRenderers(fileName string, bmPolicy *bluemonday.Policy) []Renderer { ext := filepath.Ext(fileName) switch ext { case ".md", ".mkd", ".markdown": return []Renderer{MarkdownToHtmlRenderer{bmPolicy}} default: return []Renderer{} } }
-
-
-
@@ -1,14 +1,12 @@package routes import ( "bytes" "compress/gzip" "fmt" "html/template" "io/fs" "log" "net/http" "net/url" "os" "path/filepath" "sort"
-
@@ -16,11 +14,9 @@ import (securejoin "github.com/cyphar/filepath-securejoin" "github.com/dustin/go-humanize" "github.com/microcosm-cc/bluemonday" "github.com/pocka/legit/config" "github.com/pocka/legit/git" "github.com/pocka/legit/routes/preview" "github.com/russross/blackfriday/v2" "github.com/pocka/legit/renderer/html" ) type deps struct {
-
@@ -36,8 +32,8 @@ type deps struct {// Do not use this; use deps.Template() instead. t *template.Template // ugcPolicy is a bluemonday policy for user generated content. ugcPolicy *bluemonday.Policy markdown html.MarkdownRenderer plaintext html.PlaintextRenderer } func (d *deps) Index(w http.ResponseWriter, r *http.Request) {
-
@@ -134,71 +130,31 @@ func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request) {return } transformer := NewRepoLinkTransformer(name, mainBranch) var readmeContent template.HTML for _, readme := range d.c.Repo.Readme { ext := filepath.Ext(readme) file, _ := gr.File(readme) if file == nil { continue } content, _ := file.Contents() // Getting a binary here is unlikely and we can't guess the repo owner's // intention in that case. So we process the file with Markdown processor // anyway, even if that produces nonsential garbage. content, _, _ := gr.FileContent(readme) // Skip empty files. if len(content) > 0 { switch ext { case ".md", ".mkd", ".markdown": parser := blackfriday.New(blackfriday.WithExtensions(blackfriday.CommonExtensions)) tree := parser.Parse([]byte(content)) tree.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { if !entering { return blackfriday.GoToNext } if node.Type == blackfriday.Link || node.Type == blackfriday.Image { href := string(node.LinkData.Destination) parsedURL, err := url.Parse(href) if err == nil && parsedURL.Host != "" { // Full URL, skip. return blackfriday.GoToNext } if strings.IndexByte(href, '/') == 0 { // Repository index page is the "root" for repository's README file. href = "." + href } if node.Type == blackfriday.Image { node.LinkData.Destination = fmt.Appendf([]byte{}, "/%s/blob/%s/%s?raw", name, mainBranch, href) } else if strings.LastIndexByte(href, '/') == len(href)-1 { node.LinkData.Destination = fmt.Appendf([]byte{}, "/%s/tree/%s/%s", name, mainBranch, href) node.LinkData.Destination = node.LinkData.Destination[:len(node.LinkData.Destination)-1] } else { node.LinkData.Destination = fmt.Appendf([]byte{}, "/%s/blob/%s/%s", name, mainBranch, href) } } return blackfriday.GoToNext }) var writer bytes.Buffer renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{}) renderer.RenderHeader(&writer, tree) tree.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { return renderer.RenderNode(&writer, node, entering) }) renderer.RenderFooter(&writer, tree) unsafe := blackfriday.Run( writer.Bytes(), blackfriday.WithExtensions(blackfriday.CommonExtensions), ) html := d.ugcPolicy.SanitizeBytes(unsafe) readmeContent = template.HTML(html) default: safe := d.ugcPolicy.SanitizeBytes([]byte(content)) readmeContent = template.HTML( fmt.Sprintf(`<pre>%s</pre>`, safe), ) renderer := d.htmlRenderer(file) if renderer == nil { renderer = &d.plaintext } result, err := renderer.Render([]byte(content), transformer) if err != nil { log.Printf("Unable to render %s/%s, skipping.", name, readme) continue } readmeContent = template.HTML(result) break } }
-
@@ -304,7 +260,13 @@ func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {return } contents, isBinary, err := gr.FileContent(treePath) file, err := gr.File(treePath) if err != nil { d.Write500(w) return } contents, err := file.Contents() if err != nil { d.Write500(w) return
-
@@ -317,7 +279,7 @@ func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {return } if isBinary { if isBin, _ := file.IsBinary(); isBin { contents = "Not displaying binary file" }
-
@@ -336,41 +298,40 @@ func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {if r.URL.Query().Has("preview") { previewType := r.URL.Query().Get("preview") for _, renderer := range preview.GetPreviewRenderers(treePath, d.ugcPolicy) { resolvedPreviewType := renderer.GetPreviewType() switch previewType { case "html": renderer := d.htmlRenderer(file) if renderer == nil { log.Printf("Requested HTML preview for %s/%s, but the filetype has no HTML renderer", name, treePath) d.Write404(w) return } if previewType != "" && resolvedPreviewType != previewType { continue html, err := renderer.Render([]byte(contents), NewRepoLinkTransformer(name, ref)) if err != nil { log.Printf("Failed to render HTML preview: %s", err) d.Write500(w) return } switch resolvedPreviewType { case "html": html, err := renderer.Render([]byte(contents)) if err != nil { log.Printf("Failed to render HTML preview: %s", err) d.Write500(w) return } data := repoBlobRefHTMLPreviewData{ Config: d.c, Meta: meta, Path: relpath, Content: template.HTML(html), } if err := d.Template().ExecuteTemplate(w, "repo-blob-ref-html-preview", data); err != nil { log.Println(err) return } data := repoBlobRefHTMLPreviewData{ Config: d.c, Meta: meta, Path: relpath, Content: template.HTML(html), } if err := d.Template().ExecuteTemplate(w, "repo-blob-ref-html-preview", data); err != nil { log.Println(err) return } } log.Printf("Got ?preview=%s, but not preview renderer is available for the type", previewType) d.Write404(w) return return default: log.Printf("Got ?preview=%s, but not preview renderer is available for the type", previewType) d.Write404(w) return } } lc, err := countLines(strings.NewReader(contents))
-
@@ -389,10 +350,9 @@ func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {lines[i] = uint(i + 1) } renderers := preview.GetPreviewRenderers(treePath, d.ugcPolicy) previewTypes := make([]string, len(renderers)) for i, renderer := range renderers { previewTypes[i] = renderer.GetPreviewType() previewTypes := make([]string, 0, 1) if d.htmlRenderer(file) != nil { previewTypes = append(previewTypes, "html") } data := repoBlobRefData{
-
-
-
@@ -19,7 +19,7 @@ import () func isGoModule(gr *git.GitRepo) bool { _, _, err := gr.FileContent("go.mod") _, err := gr.File("go.mod") return err == nil }
-