forked from tangled.org/core
this repo has no description

appview: pages/markup: serve relative images directly from the knot

anirudh.fi 9f6dd704 0fa363a9

verified
Changed files
+108 -160
appview
pages
markup
templates
repo
state
knotserver
+2
.gitignore
···
result
!.gitkeep
out/
+
./camo/node_modules/*
+
+49 -8
appview/pages/markup/markdown.go
···
import (
"bytes"
+
"net/url"
"path"
"github.com/yuin/goldmark"
···
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
+
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
)
// RendererType defines the type of renderer to use based on context
···
// RenderContext holds the contextual data for rendering markdown.
// It can be initialized empty, and that'll skip any transformations.
type RenderContext struct {
-
Ref string
-
FullRepoName string
+
repoinfo.RepoInfo
+
IsDev bool
RendererType RendererType
}
···
switch a.rctx.RendererType {
case RendererTypeRepoMarkdown:
-
if v, ok := n.(*ast.Link); ok {
-
a.rctx.relativeLinkTransformer(v)
+
switch n.(type) {
+
case *ast.Link:
+
a.rctx.relativeLinkTransformer(n.(*ast.Link))
+
case *ast.Image:
+
a.rctx.imageFromKnotTransformer(n.(*ast.Image))
}
// more types here like RendererTypeIssue/Pull etc.
}
···
func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) {
dst := string(link.Destination)
-
if len(dst) == 0 || dst[0] == '#' ||
-
bytes.Contains(link.Destination, []byte("://")) ||
-
bytes.HasPrefix(link.Destination, []byte("mailto:")) {
+
if isAbsoluteUrl(dst) {
return
}
-
newPath := path.Join("/", rctx.FullRepoName, "tree", rctx.Ref, dst)
+
newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, dst)
link.Destination = []byte(newPath)
}
+
+
func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) {
+
dst := string(img.Destination)
+
+
if isAbsoluteUrl(dst) {
+
return
+
}
+
+
// strip leading './'
+
if len(dst) >= 2 && dst[0:2] == "./" {
+
dst = dst[2:]
+
}
+
+
scheme := "https"
+
if rctx.IsDev {
+
scheme = "http"
+
}
+
parsedURL := &url.URL{
+
Scheme: scheme,
+
Host: rctx.Knot,
+
Path: path.Join("/",
+
rctx.RepoInfo.OwnerDid,
+
rctx.RepoInfo.Name,
+
"raw",
+
url.PathEscape(rctx.RepoInfo.Ref),
+
dst),
+
}
+
newPath := parsedURL.String()
+
img.Destination = []byte(newPath)
+
}
+
+
func isAbsoluteUrl(link string) bool {
+
parsed, err := url.Parse(link)
+
if err != nil {
+
return false
+
}
+
return parsed.IsAbs()
+
}
+44 -143
appview/pages/pages.go
···
"log"
"net/http"
"os"
-
"path"
"path/filepath"
-
"slices"
"strings"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages/markup"
+
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
"tangled.sh/tangled.sh/core/appview/pagination"
-
"tangled.sh/tangled.sh/core/appview/state/userutil"
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/types"
···
dev bool
embedFS embed.FS
templateDir string // Path to templates on disk for dev mode
+
rctx *markup.RenderContext
}
func NewPages(dev bool) *Pages {
+
// initialized with safe defaults, can be overriden per use
+
rctx := &markup.RenderContext{
+
IsDev: dev,
+
}
+
p := &Pages{
t: make(map[string]*template.Template),
dev: dev,
embedFS: Files,
+
rctx: rctx,
templateDir: "appview/pages",
}
···
type ForkRepoParams struct {
LoggedInUser *auth.User
Knots []string
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
}
func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
···
}
type RepoDescriptionParams struct {
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
}
func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error {
···
return p.executePlain("repo/fragments/repoDescription", w, params)
}
-
type RepoInfo struct {
-
Name string
-
OwnerDid string
-
OwnerHandle string
-
Description string
-
Knot string
-
RepoAt syntax.ATURI
-
IsStarred bool
-
Stats db.RepoStats
-
Roles RolesInRepo
-
Source *db.Repo
-
SourceHandle string
-
Ref string
-
DisableFork bool
-
}
-
-
type RolesInRepo struct {
-
Roles []string
-
}
-
-
func (r RolesInRepo) SettingsAllowed() bool {
-
return slices.Contains(r.Roles, "repo:settings")
-
}
-
-
func (r RolesInRepo) CollaboratorInviteAllowed() bool {
-
return slices.Contains(r.Roles, "repo:invite")
-
}
-
-
func (r RolesInRepo) RepoDeleteAllowed() bool {
-
return slices.Contains(r.Roles, "repo:delete")
-
}
-
-
func (r RolesInRepo) IsOwner() bool {
-
return slices.Contains(r.Roles, "repo:owner")
-
}
-
-
func (r RolesInRepo) IsCollaborator() bool {
-
return slices.Contains(r.Roles, "repo:collaborator")
-
}
-
-
func (r RolesInRepo) IsPushAllowed() bool {
-
return slices.Contains(r.Roles, "repo:push")
-
}
-
-
func (r RepoInfo) OwnerWithAt() string {
-
if r.OwnerHandle != "" {
-
return fmt.Sprintf("@%s", r.OwnerHandle)
-
} else {
-
return r.OwnerDid
-
}
-
}
-
-
func (r RepoInfo) FullName() string {
-
return path.Join(r.OwnerWithAt(), r.Name)
-
}
-
-
func (r RepoInfo) OwnerWithoutAt() string {
-
if strings.HasPrefix(r.OwnerWithAt(), "@") {
-
return strings.TrimPrefix(r.OwnerWithAt(), "@")
-
} else {
-
return userutil.FlattenDid(r.OwnerDid)
-
}
-
}
-
-
func (r RepoInfo) FullNameWithoutAt() string {
-
return path.Join(r.OwnerWithoutAt(), r.Name)
-
}
-
-
func (r RepoInfo) GetTabs() [][]string {
-
tabs := [][]string{
-
{"overview", "/", "square-chart-gantt"},
-
{"issues", "/issues", "circle-dot"},
-
{"pulls", "/pulls", "git-pull-request"},
-
}
-
-
if r.Roles.SettingsAllowed() {
-
tabs = append(tabs, []string{"settings", "/settings", "cog"})
-
}
-
-
return tabs
-
}
-
-
// each tab on a repo could have some metadata:
-
//
-
// issues -> number of open issues etc.
-
// settings -> a warning icon to setup branch protection? idk
-
//
-
// we gather these bits of info here, because go templates
-
// are difficult to program in
-
func (r RepoInfo) TabMetadata() map[string]any {
-
meta := make(map[string]any)
-
-
if r.Stats.PullCount.Open > 0 {
-
meta["pulls"] = r.Stats.PullCount.Open
-
}
-
-
if r.Stats.IssueCount.Open > 0 {
-
meta["issues"] = r.Stats.IssueCount.Open
-
}
-
-
// more stuff?
-
-
return meta
-
}
-
type RepoIndexParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
TagMap map[string][]string
CommitsTrunc []*object.Commit
···
return p.executeRepo("repo/empty", w, params)
}
-
rctx := markup.RenderContext{
-
Ref: params.RepoInfo.Ref,
-
FullRepoName: params.RepoInfo.FullName(),
+
p.rctx = &markup.RenderContext{
+
RepoInfo: params.RepoInfo,
+
IsDev: p.dev,
+
RendererType: markup.RendererTypeRepoMarkdown,
}
if params.ReadmeFileName != "" {
···
ext := filepath.Ext(params.ReadmeFileName)
switch ext {
case ".md", ".markdown", ".mdown", ".mkdn", ".mkd":
-
htmlString = rctx.RenderMarkdown(params.Readme)
+
htmlString = p.rctx.RenderMarkdown(params.Readme)
params.Raw = false
params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString))
default:
···
type RepoLogParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
TagMap map[string][]string
types.RepoLogResponse
Active string
···
type RepoCommitParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
EmailToDidOrHandle map[string]string
···
type RepoTreeParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
BreadCrumbs [][]string
BaseTreeLink string
···
type RepoBranchesParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
types.RepoBranchesResponse
}
···
type RepoTagsParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
types.RepoTagsResponse
}
···
type RepoBlobParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
BreadCrumbs [][]string
ShowRendered bool
···
if params.ShowRendered {
switch markup.GetFormat(params.Path) {
case markup.FormatMarkdown:
-
rctx := markup.RenderContext{
-
Ref: params.RepoInfo.Ref,
-
FullRepoName: params.RepoInfo.FullName(),
+
p.rctx = &markup.RenderContext{
+
RepoInfo: params.RepoInfo,
+
IsDev: p.dev,
RendererType: markup.RendererTypeRepoMarkdown,
}
-
params.RenderedContents = template.HTML(rctx.RenderMarkdown(params.Contents))
+
params.RenderedContents = template.HTML(p.rctx.RenderMarkdown(params.Contents))
}
}
···
type RepoSettingsParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Collaborators []Collaborator
Active string
Branches []string
···
type RepoIssuesParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
Issues []db.Issue
DidHandleMap map[string]string
···
type RepoSingleIssueParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
Issue db.Issue
Comments []db.Comment
···
type RepoNewIssueParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
}
···
type EditIssueCommentParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Issue *db.Issue
Comment *db.Comment
}
···
type SingleIssueCommentParams struct {
LoggedInUser *auth.User
DidHandleMap map[string]string
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Issue *db.Issue
Comment *db.Comment
}
···
type RepoNewPullParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Branches []types.Branch
Active string
}
···
type RepoPullsParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Pulls []*db.Pull
Active string
DidHandleMap map[string]string
···
type RepoSinglePullParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Active string
DidHandleMap map[string]string
Pull *db.Pull
···
type RepoPullPatchParams struct {
LoggedInUser *auth.User
DidHandleMap map[string]string
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Pull *db.Pull
Diff *types.NiceDiff
Round int
···
type RepoPullInterdiffParams struct {
LoggedInUser *auth.User
DidHandleMap map[string]string
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Pull *db.Pull
Round int
Interdiff *patchutil.InterdiffResult
···
}
type PullPatchUploadParams struct {
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
}
func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error {
···
}
type PullCompareBranchesParams struct {
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Branches []types.Branch
}
···
}
type PullCompareForkParams struct {
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Forks []db.Repo
}
···
}
type PullCompareForkBranchesParams struct {
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
SourceBranches []types.Branch
TargetBranches []types.Branch
}
···
type PullResubmitParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Pull *db.Pull
SubmissionId int
}
···
type PullActionsParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Pull *db.Pull
RoundNumber int
MergeCheck types.MergeCheckResponse
···
type PullNewCommentParams struct {
LoggedInUser *auth.User
-
RepoInfo RepoInfo
+
RepoInfo repoinfo.RepoInfo
Pull *db.Pull
RoundNumber int
}
+1 -1
appview/pages/templates/repo/blob.html
···
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
<span>{{ byteFmt .SizeHint }}</span>
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
-
<a href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/raw/{{ .Path }}">view raw</a>
+
<a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a>
{{ if .RenderToggle }}
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
<a
+3 -2
appview/state/repo.go
···
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/pages/markup"
+
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
"tangled.sh/tangled.sh/core/appview/pagination"
"tangled.sh/tangled.sh/core/types"
···
return collaborators, nil
}
-
func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo {
+
func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo {
isStarred := false
if u != nil {
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
···
knot = "tangled.sh"
-
repoInfo := pages.RepoInfo{
+
repoInfo := repoinfo.RepoInfo{
OwnerDid: f.OwnerDid(),
OwnerHandle: f.OwnerHandle(),
Name: f.RepoName,
+4 -4
appview/state/repo_util.go
···
"github.com/go-git/go-git/v5/plumbing/object"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
-
"tangled.sh/tangled.sh/core/appview/pages"
+
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
)
func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
···
}, nil
}
-
func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
+
func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo {
if u != nil {
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
-
return pages.RolesInRepo{r}
+
return repoinfo.RolesInRepo{r}
} else {
-
return pages.RolesInRepo{}
+
return repoinfo.RolesInRepo{}
}
}
+1 -1
appview/state/router.go
···
r.Get("/branches", s.RepoBranches)
r.Get("/tags", s.RepoTags)
r.Get("/blob/{ref}/*", s.RepoBlob)
-
r.Get("/blob/{ref}/raw/*", s.RepoBlobRaw)
+
r.Get("/raw/{ref}/*", s.RepoBlobRaw)
r.Route("/issues", func(r chi.Router) {
r.With(middleware.Paginate).Get("/", s.RepoIssues)
+4 -1
knotserver/handler.go
···
r.Route("/blob/{ref}", func(r chi.Router) {
r.Get("/*", h.Blob)
-
r.Get("/raw/*", h.BlobRaw)
+
})
+
+
r.Route("/raw/{ref}", func(r chi.Router) {
+
r.Get("/*", h.BlobRaw)
})
r.Get("/log/{ref}", h.Log)