diff --git a/README.md b/README.md index b526756..7823d99 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ugit -ugit logo +ugit logo Minimal git server @@ -29,4 +29,4 @@ http get https://github.com/.keys | save --force path/to/authorized_ke [MIT](LICENSE) -Lots of inspiration and some starting code used from [wish](https://github.com/charmbracelet/wish) [(MIT)](https://github.com/charmbracelet/wish/blob/3e6f92a166118390484ce4a0904114b375b9e485/LICENSE) and [legit](https://github.com/icyphox/legit) [(MIT)](https://github.com/icyphox/legit/blob/bdfc973207a67a3b217c130520d53373d088763c/license). +Lots of inspiration and some starting code used from [gitea](https://github.com/go-gitea/gitea) [(MIT)](https://github.com/go-gitea/gitea/blob/eba9c0ce48c7d43910eb77db74c6648157663ceb/LICENSE), [wish](https://github.com/charmbracelet/wish) [(MIT)](https://github.com/charmbracelet/wish/blob/3e6f92a166118390484ce4a0904114b375b9e485/LICENSE), and [legit](https://github.com/icyphox/legit) [(MIT)](https://github.com/icyphox/legit/blob/bdfc973207a67a3b217c130520d53373d088763c/license). diff --git a/flake.nix b/flake.nix index f9c52d0..15612ff 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ path = ./.; }); subPackages = ["cmd/ugitd"]; - vendorHash = "sha256-E4cwC6c0d+HvHldqGYiWdPEdS2fch6imvAXzxb2MMdY="; + vendorHash = "sha256-2vIccmJs6YitRndccQOnUuFZCIbwzi0NfRzbixaLVTo="; meta = with pkgs.lib; { description = "Minimal git server"; homepage = "https://git.jolheiser.com/ugit"; diff --git a/go.mod b/go.mod index 3af97fb..28b39d6 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark-emoji v1.0.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc + golang.org/x/net v0.19.0 ) require ( @@ -47,7 +48,6 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/tools v0.16.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/internal/html/markdown.go b/internal/html/markdown.go index e109162..c4c8d5f 100644 --- a/internal/html/markdown.go +++ b/internal/html/markdown.go @@ -2,7 +2,12 @@ package html import ( "bytes" + "fmt" + "golang.org/x/net/html" + "io" + "net/url" "path/filepath" + "strings" "go.jolheiser.com/ugit/internal/git" @@ -10,17 +15,23 @@ import ( "github.com/yuin/goldmark" emoji "github.com/yuin/goldmark-emoji" highlighting "github.com/yuin/goldmark-highlighting/v2" + "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" goldmarkhtml "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" ) -var Markdown = goldmark.New( +var markdown = goldmark.New( goldmark.WithRendererOptions( goldmarkhtml.WithUnsafe(), ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), + parser.WithASTTransformers( + util.Prioritized(astTransformer{}, 100), + ), ), goldmark.WithExtensions( extension.GFM, @@ -48,11 +59,23 @@ func Readme(repo *git.Repo, ref, path string) (string, error) { } if readme != "" { + ctx := parser.NewContext() + mdCtx := markdownContext{ + repo: repo.Name(), + ref: ref, + path: path, + } + ctx.Set(renderContextKey, mdCtx) var buf bytes.Buffer - if err := Markdown.Convert([]byte(readme), &buf); err != nil { + if err := markdown.Convert([]byte(readme), &buf, parser.WithContext(ctx)); err != nil { return "", err } - return buf.String(), nil + var out bytes.Buffer + if err := postProcess(buf.String(), mdCtx, &out); err != nil { + return "", err + } + + return out.String(), nil } for _, md := range []string{"README.txt", "README", "readme.txt", "readme"} { @@ -64,3 +87,128 @@ func Readme(repo *git.Repo, ref, path string) (string, error) { return "", nil } + +var renderContextKey = parser.NewContextKey() + +type markdownContext struct { + repo string + ref string + path string +} + +type astTransformer struct{} + +func (a astTransformer) Transform(node *ast.Document, _ text.Reader, pc parser.Context) { + _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + ctx := pc.Get(renderContextKey).(markdownContext) + + switch v := n.(type) { + case *ast.Image: + link := v.Destination + if len(link) > 0 && !bytes.HasPrefix(link, []byte("http")) { + v.Destination = []byte(resolveLink(ctx.repo, ctx.ref, ctx.path, string(link)) + "?raw&pretty") + } + + parent := n.Parent() + if _, ok := parent.(*ast.Link); !ok && parent != nil { + next := n.NextSibling() + wrapper := ast.NewLink() + wrapper.Destination = v.Destination + wrapper.Title = v.Title + wrapper.SetAttributeString("target", []byte("_blank")) + img := ast.NewImage(ast.NewLink()) + img.Destination = link + img.Title = v.Title + for _, attr := range v.Attributes() { + img.SetAttribute(attr.Name, attr.Value) + } + for child := v.FirstChild(); child != nil; { + nextChild := child.NextSibling() + img.AppendChild(img, child) + child = nextChild + } + wrapper.AppendChild(wrapper, img) + wrapper.SetNextSibling(next) + parent.ReplaceChild(parent, n, wrapper) + v.SetNextSibling(next) + } + case *ast.Link: + link := v.Destination + if len(link) > 0 && !bytes.HasPrefix(link, []byte("http")) && link[0] != '#' && !bytes.HasPrefix(link, []byte("mailto")) { + v.Destination = []byte(resolveLink(ctx.repo, ctx.ref, ctx.path, string(link))) + } + } + + return ast.WalkContinue, nil + }) +} + +func postProcess(in string, ctx markdownContext, out io.Writer) error { + node, err := html.Parse(strings.NewReader("" + in + " 0 && !strings.HasPrefix(attr.Val, "http") && !strings.HasPrefix(attr.Val, "data:image/") { + attr.Val = resolveLink(ctx.repo, ctx.ref, ctx.path, attr.Val) + "?raw&pretty" + } + node.Attr[i] = attr + } + } + for n := node.FirstChild; n != nil; n = n.NextSibling { + process(ctx, n) + } +} + +func resolveLink(repo, ref, path, link string) string { + baseURL, err := url.Parse(fmt.Sprintf("/%s/tree/%s/%s", repo, ref, path)) + if err != nil { + return "" + } + linkURL, err := url.Parse(link) + if err != nil { + return "" + } + return baseURL.ResolveReference(linkURL).String() +} diff --git a/internal/html/repo_commit.templ b/internal/html/repo_commit.templ index 7f07f14..bb95a59 100644 --- a/internal/html/repo_commit.templ +++ b/internal/html/repo_commit.templ @@ -21,7 +21,19 @@ templ RepoCommit(rcc RepoCommitContext) {
{ fmt.Sprintf("%d changed files, %d additions(+), %d deletions(-)", rcc.Commit.Stats.Changed, rcc.Commit.Stats.Additions, rcc.Commit.Stats.Deletions) }
for _, file := range rcc.Commit.Files { -
{ string(file.Action[0]) }{ " " }{ file.From.Path }{ " -> " }{ file.To.Path }
+
+ { string(file.Action[0]) } + { " " } + if file.From.Path != "" { + { file.From.Path } + } + if file.From.Path != "" && file.To.Path != "" { + { " -> " } + } + if file.To.Path != "" { + { file.To.Path } + } +
@templ.Raw(file.Patch)
} } diff --git a/internal/html/repo_commit_templ.go b/internal/html/repo_commit_templ.go index 8a48de2..49c39c5 100644 --- a/internal/html/repo_commit_templ.go +++ b/internal/html/repo_commit_templ.go @@ -219,90 +219,112 @@ func RepoCommit(rcc RepoCommitContext) templ.Component { return templ_7745c5c3_Err } for _, file := range rcc.Commit.Files { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(string(file.Action[0])) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 24, Col: 77} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var19 string templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(" ") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 96} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 25, Col: 9} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(file.From.Path) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 27, Col: 227} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err + if file.From.Path != "" && file.To.Path != "" { + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(" -> ") + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 30, Col: 13} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(file.From.Path) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 320} + if file.To.Path != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(file.To.Path) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 33, Col: 221} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(" -> ") - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 334} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(file.To.Path) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 552} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }