commit c152a522ff4215c0dcc176118c4997f7f79fe770
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-06-27T01:08:16Z |
| subject | Renders README and markdown files |
commit c152a522ff4215c0dcc176118c4997f7f79fe770
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-06-27T01:08:16Z
Renders README and markdown files
---
go.mod | 11 ++++++
go.sum | 10 +++++
internal/buildinfo/buildinfo_gen.go | 4 +-
internal/render/markdown.go | 51 ++++++++++++++++++++++++
internal/render/render.go | 78 ++++++++++++++++++++++++++++++++-----
internal/render/rtree.go | 11 ++++++
internal/render/templates.go | 17 +++++++-
7 files changed, 170 insertions(+), 12 deletions(-)
diff --git a/go.mod b/go.mod
index 12611ab..b824ee2 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,14 @@
module github.com/tgckpg/flatgit
go 1.25
+
+require (
+ github.com/microcosm-cc/bluemonday v1.0.27
+ github.com/yuin/goldmark v1.8.2
+)
+
+require (
+ github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/gorilla/css v1.0.1 // indirect
+ golang.org/x/net v0.26.0 // indirect
+)
diff --git a/go.sum b/go.sum
index e69de29..884e507 100644
--- a/go.sum
+++ b/go.sum
@@ -0,0 +1,10 @@
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
+github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
+github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
diff --git a/internal/buildinfo/buildinfo_gen.go b/internal/buildinfo/buildinfo_gen.go
index 61f6d7a..9cc9af0 100644
--- a/internal/buildinfo/buildinfo_gen.go
+++ b/internal/buildinfo/buildinfo_gen.go
@@ -2,6 +2,6 @@ package buildinfo
const (
Version = "dev"
- GitRevision = "e7920416df1fd5a5ec6ec046a931cef5232b08af"
- Timestamp = "20260624.053038"
+ GitRevision = "5ce6c3b70b12fb55cf0e108c7eabc51d5c50ae43"
+ Timestamp = "20260627.005304"
)
diff --git a/internal/render/markdown.go b/internal/render/markdown.go
new file mode 100644
index 0000000..343eb29
--- /dev/null
+++ b/internal/render/markdown.go
@@ -0,0 +1,51 @@
+package render
+
+import (
+ "bytes"
+ "path/filepath"
+ "strings"
+
+ "github.com/microcosm-cc/bluemonday"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/parser"
+)
+
+func renderMarkdownSafe(src []byte) ([]byte, error) {
+ md := goldmark.New(
+ goldmark.WithExtensions(extension.GFM),
+ goldmark.WithParserOptions(parser.WithAutoHeadingID()),
+ )
+
+ var buf bytes.Buffer
+ if err := md.Convert(src, &buf); err != nil {
+ return nil, err
+ }
+
+ policy := bluemonday.UGCPolicy()
+ return policy.SanitizeBytes(buf.Bytes()), nil
+}
+
+func isMarkdownPath(path string) bool {
+ name := strings.ToLower(filepath.Base(path))
+ ext := strings.ToLower(filepath.Ext(path))
+
+ return ext == ".md" || name == "readme"
+}
+
+func findReadmePath(tree []TreeEntry) string {
+ for _, e := range tree {
+ if e.Type != "blob" {
+ continue
+ }
+
+ name := strings.ToLower(e.Path)
+
+ switch name {
+ case "readme.md", "readme.markdown", "readme":
+ return e.Path
+ }
+ }
+
+ return ""
+}
diff --git a/internal/render/render.go b/internal/render/render.go
index c634bf3..6b70cac 100644
--- a/internal/render/render.go
+++ b/internal/render/render.go
@@ -114,14 +114,27 @@ func (r *Renderer) RenderRepo(ctx context.Context, repo config.Repo) error {
GeneratedAt: manifest.GeneratedAt.Format(time.RFC3339),
}
+ readmeHTML, err := r.renderReadmeHTML(ctx, repo, commit, tree)
+ if err != nil {
+ return err
+ }
+
if err := renderTemplate(filepath.Join(next, "index.html"), indexTemplate, struct {
basePage
- Refs []RefInfo
- Commits []CommitInfo
- Files []TreeEntry
- }{page, refs, firstCommits(commits, 20), firstTree(tree, 50)}); err != nil {
+ Refs []RefInfo
+ Commits []CommitInfo
+ Files []TreeEntry
+ ReadmeHTML template.HTML
+ }{
+ page,
+ refs,
+ firstCommits(commits, 3),
+ firstTree(tree, 50),
+ readmeHTML,
+ }); err != nil {
return err
}
+
if err := renderTemplate(filepath.Join(next, "refs.html"), refsTemplate, struct {
basePage
Refs []RefInfo
@@ -195,6 +208,51 @@ func (r *Renderer) resolveDefaultCommit(ctx context.Context, repo config.Repo) (
return "", last
}
+func (r *Renderer) renderReadmeHTML(
+ ctx context.Context,
+ repo config.Repo,
+ commit string,
+ tree []TreeEntry,
+) (template.HTML, error) {
+ readmePath := findReadmePath(tree)
+ if readmePath == "" {
+ return "", nil
+ }
+
+ content, err := r.blob(ctx, repo, commit, readmePath)
+ if err != nil {
+ return "", err
+ }
+
+ htmlBytes, err := renderMarkdownSafe(content)
+ if err != nil {
+ return "", err
+ }
+
+ return template.HTML(htmlBytes), nil
+}
+
+func (r *Renderer) blob(
+ ctx context.Context,
+ repo config.Repo,
+ commit string,
+ path string,
+) ([]byte, error) {
+ spec := fmt.Sprintf("%s:%s", commit, path)
+
+ content, err := r.Git.Output(
+ ctx,
+ repo.MirrorDir,
+ "show",
+ spec,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("read blob %s at %s: %w", path, commit, err)
+ }
+
+ return content, nil
+}
+
func (r *Renderer) refs(ctx context.Context, repo config.Repo) ([]RefInfo, error) {
out, err := r.Git.Output(ctx, repo.MirrorDir, "for-each-ref", "--format=%(refname)%00%(objectname)", "refs/heads", "refs/tags")
if err != nil {
@@ -283,11 +341,13 @@ type basePage struct {
}
type blobView struct {
- Path string
- Size int64
- RawHref string
- Text string
- Binary bool
+ Path string
+ Size int64
+ RawHref string
+ Text string
+ Binary bool
+ HTML template.HTML
+ RenderedMarkdown bool
}
func writeStatic(root string) error {
diff --git a/internal/render/rtree.go b/internal/render/rtree.go
index c53206b..f65074b 100644
--- a/internal/render/rtree.go
+++ b/internal/render/rtree.go
@@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
+ "html/template"
"io"
"io/fs"
"os"
@@ -168,6 +169,16 @@ func (r *Renderer) renderBlobEntry(
blob.Text = string(content)
}
+ if !blob.Binary && isMarkdownPath(blob.Path) {
+ htmlBytes, err := renderMarkdownSafe(content)
+ if err != nil {
+ return err
+ }
+
+ blob.HTML = template.HTML(htmlBytes)
+ blob.RenderedMarkdown = true
+ }
+
if err := renderTemplate(blobPath, blobTemplate, struct {
basePage
Blob blobView
diff --git a/internal/render/templates.go b/internal/render/templates.go
index 3e7a6d9..e86b876 100644
--- a/internal/render/templates.go
+++ b/internal/render/templates.go
@@ -58,6 +58,13 @@ const indexTemplate = `{{define "pageTitle"}}{{.RepoManifest.Repository.FullName
<tr><td class="mono"><a href="{{$.RepoManifest.Repository.SitePath}}blob/{{$.RepoManifest.Repository.DefaultRefSlug}}/{{.Path}}.html">{{.Path}}</a></td><td>{{.Size}}</td></tr>
{{end}}
</table>
+
+{{if .ReadmeHTML}}
+<section class="readme">
+ {{.ReadmeHTML}}
+</section>
+{{end}}
+
{{end}}
`
@@ -119,7 +126,15 @@ const blobTemplate = `{{define "pageTitle"}}{{.RepoManifest.Repository.FullName}
{{if .Blob.Binary}}
<p>Binary or large file; HTML preview skipped.</p>
{{else}}
-<pre>{{.Blob.Text}}</pre>
+
+{{if .Blob.RenderedMarkdown}}
+<article class="markdown-body">
+{{.Blob.HTML}}
+</article>
+{{else}}
+<pre><code>{{.Blob.Text}}</code></pre>
+{{end}}
+
{{end}}
{{end}}
`