diff --git a/api/tangled/repotree.go b/api/tangled/repotree.go index f84e98ae..b7776815 100644 --- a/api/tangled/repotree.go +++ b/api/tangled/repotree.go @@ -31,10 +31,20 @@ type RepoTree_Output struct { Files []*RepoTree_TreeEntry `json:"files" cborgen:"files"` // parent: The parent path in the tree Parent *string `json:"parent,omitempty" cborgen:"parent,omitempty"` + // readme: Readme for this file tree + Readme *RepoTree_Readme `json:"readme,omitempty" cborgen:"readme,omitempty"` // ref: The git reference used Ref string `json:"ref" cborgen:"ref"` } +// RepoTree_Readme is a "readme" in the sh.tangled.repo.tree schema. +type RepoTree_Readme struct { + // contents: Contents of the readme file + Contents string `json:"contents" cborgen:"contents"` + // filename: Name of the readme file + Filename string `json:"filename" cborgen:"filename"` +} + // RepoTree_TreeEntry is a "treeEntry" in the sh.tangled.repo.tree schema. type RepoTree_TreeEntry struct { // is_file: Whether this entry is a file diff --git a/appview/pages/markup/format.go b/appview/pages/markup/format.go index ffa7b511..82e1c206 100644 --- a/appview/pages/markup/format.go +++ b/appview/pages/markup/format.go @@ -1,6 +1,8 @@ package markup -import "strings" +import ( + "regexp" +) type Format string @@ -10,27 +12,23 @@ const ( ) var FileTypes map[Format][]string = map[Format][]string{ - FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"}, + FormatMarkdown: {".md", ".markdown", ".mdown", ".mkdn", ".mkd"}, +} + +var FileTypePatterns = map[Format]*regexp.Regexp{ + FormatMarkdown: regexp.MustCompile(`(?i)\.(md|markdown|mdown|mkdn|mkd)$`), } -// ReadmeFilenames contains the list of common README filenames to search for, -// in order of preference. Only includes well-supported formats. -var ReadmeFilenames = []string{ - "README.md", "readme.md", - "README", - "readme", - "README.markdown", - "readme.markdown", - "README.txt", - "readme.txt", +var ReadmePattern = regexp.MustCompile(`(?i)^readme(\.(md|markdown|txt))?$`) + +func IsReadmeFile(filename string) bool { + return ReadmePattern.MatchString(filename) } func GetFormat(filename string) Format { - for format, extensions := range FileTypes { - for _, extension := range extensions { - if strings.HasSuffix(filename, extension) { - return format - } + for format, pattern := range FileTypePatterns { + if pattern.MatchString(filename) { + return format } } // default format diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 53b09fb9..ad3629eb 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -714,6 +714,8 @@ type RepoTreeParams struct { Active string BreadCrumbs [][]string TreePath string + Raw bool + HTMLReadme template.HTML types.RepoTreeResponse } @@ -740,6 +742,24 @@ func (r RepoTreeParams) TreeStats() RepoTreeStats { func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { params.Active = "overview" + + p.rctx.RepoInfo = params.RepoInfo + p.rctx.RepoInfo.Ref = params.Ref + p.rctx.RendererType = markup.RendererTypeRepoMarkdown + + if params.ReadmeFileName != "" { + ext := filepath.Ext(params.ReadmeFileName) + switch ext { + case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": + params.Raw = false + htmlString := p.rctx.RenderMarkdown(params.Readme) + sanitized := p.rctx.SanitizeDefault(htmlString) + params.HTMLReadme = template.HTML(sanitized) + default: + params.Raw = true + } + } + return p.executeRepo("repo/tree", w, params) } diff --git a/appview/pages/templates/repo/fragments/readme.html b/appview/pages/templates/repo/fragments/readme.html index 64426307..ee9e77f7 100644 --- a/appview/pages/templates/repo/fragments/readme.html +++ b/appview/pages/templates/repo/fragments/readme.html @@ -1,13 +1,13 @@ {{ define "repo/fragments/readme" }}
{{- if .ReadmeFileName -}} -
+
{{ i "file-text" "w-4 h-4" "text-gray-600 dark:text-gray-400" }} {{ .ReadmeFileName }}
{{- end -}}
{{end}} + +{{ define "repoAfter" }} + {{- if or .HTMLReadme .Readme -}} + {{ template "repo/fragments/readme" . }} + {{- end -}} +{{ end }} diff --git a/appview/repo/index.go b/appview/repo/index.go index c5f3c2d7..db4bfcd7 100644 --- a/appview/repo/index.go +++ b/appview/repo/index.go @@ -22,7 +22,6 @@ import ( "tangled.org/core/appview/db" "tangled.org/core/appview/models" "tangled.org/core/appview/pages" - "tangled.org/core/appview/pages/markup" "tangled.org/core/appview/reporesolver" "tangled.org/core/appview/xrpcclient" "tangled.org/core/types" @@ -328,26 +327,6 @@ func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client } }() - // readme content - wg.Add(1) - go func() { - defer wg.Done() - for _, filename := range markup.ReadmeFilenames { - blobResp, err := tangled.RepoBlob(ctx, xrpcc, filename, false, ref, repo) - if err != nil { - continue - } - - if blobResp == nil { - continue - } - - readmeContent = blobResp.Content - readmeFileName = filename - break - } - }() - wg.Wait() if errs != nil { @@ -376,6 +355,11 @@ func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client } } + if treeResp != nil && treeResp.Readme != nil { + readmeFileName = treeResp.Readme.Filename + readmeContent = treeResp.Readme.Contents + } + result := &types.RepoIndexResponse{ IsEmpty: false, Ref: ref, diff --git a/appview/repo/repo.go b/appview/repo/repo.go index f6c2c393..e82dfc80 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -484,6 +484,10 @@ func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { if xrpcResp.Dotdot != nil { result.DotDot = *xrpcResp.Dotdot } + if xrpcResp.Readme != nil { + result.ReadmeFileName = xrpcResp.Readme.Filename + result.Readme = xrpcResp.Readme.Contents + } // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, // so we can safely redirect to the "parent" (which is the same file). diff --git a/knotserver/xrpc/repo_tree.go b/knotserver/xrpc/repo_tree.go index fa7819d9..ba9ad711 100644 --- a/knotserver/xrpc/repo_tree.go +++ b/knotserver/xrpc/repo_tree.go @@ -4,8 +4,10 @@ import ( "net/http" "path/filepath" "time" + "unicode/utf8" "tangled.org/core/api/tangled" + "tangled.org/core/appview/pages/markup" "tangled.org/core/knotserver/git" xrpcerr "tangled.org/core/xrpc/errors" ) @@ -43,6 +45,24 @@ func (x *Xrpc) RepoTree(w http.ResponseWriter, r *http.Request) { return } + // if any of these files are a readme candidate, pass along its blob contents too + var readmeFileName string + var readmeContents string + for _, file := range files { + if markup.IsReadmeFile(file.Name) { + contents, err := gr.RawContent(filepath.Join(path, file.Name)) + if err != nil { + x.Logger.Error("failed to read contents of file", "path", path, "file", file.Name) + } + + if utf8.Valid(contents) { + readmeFileName = file.Name + readmeContents = string(contents) + break + } + } + } + // convert NiceTree -> tangled.RepoTree_TreeEntry treeEntries := make([]*tangled.RepoTree_TreeEntry, len(files)) for i, file := range files { @@ -83,6 +103,10 @@ func (x *Xrpc) RepoTree(w http.ResponseWriter, r *http.Request) { Parent: parentPtr, Dotdot: dotdotPtr, Files: treeEntries, + Readme: &tangled.RepoTree_Readme{ + Filename: readmeFileName, + Contents: readmeContents, + }, } writeJson(w, response) diff --git a/lexicons/repo/tree.json b/lexicons/repo/tree.json index c3e5fe54..4d5101d0 100644 --- a/lexicons/repo/tree.json +++ b/lexicons/repo/tree.json @@ -41,6 +41,11 @@ "type": "string", "description": "Parent directory path" }, + "readme": { + "type": "ref", + "ref": "#readme", + "description": "Readme for this file tree" + }, "files": { "type": "array", "items": { @@ -70,6 +75,20 @@ } ] }, + "readme": { + "type": "object", + "required": ["filename", "contents"], + "properties": { + "filename": { + "type": "string", + "description": "Name of the readme file" + }, + "contents": { + "type": "string", + "description": "Contents of the readme file" + } + } + }, "treeEntry": { "type": "object", "required": ["name", "mode", "size", "is_file", "is_subtree"], diff --git a/types/repo.go b/types/repo.go index e0d3d889..3f479089 100644 --- a/types/repo.go +++ b/types/repo.go @@ -41,11 +41,13 @@ type RepoFormatPatchResponse struct { } type RepoTreeResponse struct { - Ref string `json:"ref,omitempty"` - Parent string `json:"parent,omitempty"` - Description string `json:"description,omitempty"` - DotDot string `json:"dotdot,omitempty"` - Files []NiceTree `json:"files,omitempty"` + Ref string `json:"ref,omitempty"` + Parent string `json:"parent,omitempty"` + Description string `json:"description,omitempty"` + DotDot string `json:"dotdot,omitempty"` + Files []NiceTree `json:"files,omitempty"` + ReadmeFileName string `json:"readme_filename,omitempty"` + Readme string `json:"readme_contents,omitempty"` } type TagReference struct {