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

appview: proper nested templates

This now allows for a more organized template structure. All templates
can now be accessed via "parent/child" paths, and are templated atop
the layouts/*.html files.

The layout files themselves are modular and can be included in each
other. The only caveat is layout/*.html NEED to define their own
template names like so:

{{ define "base" }}

The other page templates do not (and musn't) define their own template.

+1 -1
.air/appview.toml
···
root = "."
exclude_regex = [".*_templ.go"]
-
include_ext = ["go", "templ"]
+
include_ext = ["go", "templ", "html"]
exclude_dir = ["target", "atrium"]
appview/pages/knot.html appview/pages/templates/knot.html
appview/pages/knots.html appview/pages/templates/knots.html
-20
appview/pages/layout.html
···
-
<!DOCTYPE html>
-
<html lang="en">
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
-
<title>{{block "title" .}}tangled{{end}}</title>
-
</head>
-
<body>
-
-
<div class="navbar">
-
<a href="/" style="color: white; text-decoration: none;">Home</a>
-
</div>
-
-
<div class="content">
-
{{block "content" .}}{{end}}
-
</div>
-
-
</body>
-
</html>
-14
appview/pages/login.html
···
-
{{define "title"}}login{{end}}
-
-
{{define "content"}}
-
<h1>login</h1>
-
<form method="POST" action="/login">
-
<label for="handle">handle</label>
-
<input type="text" id="handle" name="handle" required>
-
-
<label for="app_password">app password</label>
-
<input type="password" id="app_password" name="app_password" required>
-
-
<button type="submit">login</button>
-
</form>
-
{{end}}
appview/pages/new-repo.html appview/pages/templates/new-repo.html
+59 -30
appview/pages/pages.go
···
import (
"embed"
+
"fmt"
"html/template"
"io"
-
"sync"
+
"io/fs"
+
"log"
+
"strings"
"github.com/sotangled/tangled/appview/auth"
"github.com/sotangled/tangled/appview/db"
)
-
//go:embed *.html
+
//go:embed templates/*
var files embed.FS
-
var (
-
cache = make(map[string]*template.Template)
-
mutex sync.Mutex
-
)
+
type Pages struct {
+
t map[string]*template.Template
+
}
+
+
func NewPages() *Pages {
+
templates := make(map[string]*template.Template)
+
+
// Walk through embedded templates directory and parse all .html files
+
err := fs.WalkDir(files, "templates", func(path string, d fs.DirEntry, err error) error {
+
if err != nil {
+
return err
+
}
+
+
if !d.IsDir() && strings.HasSuffix(path, ".html") {
+
name := strings.TrimPrefix(path, "templates/")
+
name = strings.TrimSuffix(name, ".html")
-
func parse(file string) *template.Template {
-
mutex.Lock()
-
defer mutex.Unlock()
+
if !strings.HasPrefix(path, "templates/layouts/") {
+
// Add the page template on top of the base
+
tmpl, err := template.New(name).ParseFS(files, path, "templates/layouts/*.html")
+
if err != nil {
+
return fmt.Errorf("setting up template: %w", err)
+
}
-
if tmpl, found := cache[file]; found {
-
return tmpl
+
templates[name] = tmpl
+
log.Printf("loaded template: %s", name)
+
}
+
+
return nil
+
}
+
return nil
+
})
+
if err != nil {
+
log.Fatalf("walking template dir: %v", err)
}
-
tmpl := template.Must(
-
template.New("layout.html").ParseFS(files, "layout.html", file),
-
)
+
log.Printf("total templates loaded: %d", len(templates))
-
cache[file] = tmpl
-
return tmpl
+
return &Pages{
+
t: templates,
+
}
}
type LoginParams struct {
}
-
func Login(w io.Writer, p LoginParams) error {
-
return parse("login.html").Execute(w, p)
+
func (p *Pages) execute(name string, w io.Writer, params any) error {
+
return p.t[name].ExecuteTemplate(w, "layouts/base", params)
+
}
+
+
func (p *Pages) Login(w io.Writer, params LoginParams) error {
+
return p.t["user/login"].ExecuteTemplate(w, "layouts/base", params)
}
type TimelineParams struct {
User *auth.User
}
-
func Timeline(w io.Writer, p TimelineParams) error {
-
return parse("timeline.html").Execute(w, p)
+
func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
+
return p.execute("timeline", w, params)
}
type SettingsParams struct {
···
PubKeys []db.PublicKey
}
-
func Settings(w io.Writer, p SettingsParams) error {
-
return parse("settings.html").Execute(w, p)
+
func (p *Pages) Settings(w io.Writer, params SettingsParams) error {
+
return p.execute("settings/keys", w, params)
}
type KnotsParams struct {
···
Registrations []db.Registration
}
-
func Knots(w io.Writer, p KnotsParams) error {
-
return parse("knots.html").Execute(w, p)
+
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
+
return p.execute("knots", w, params)
}
type KnotParams struct {
···
IsOwner bool
}
-
func Knot(w io.Writer, p KnotParams) error {
-
return parse("knot.html").Execute(w, p)
+
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
+
return p.execute("knot", w, params)
}
type NewRepoParams struct {
User *auth.User
}
-
func NewRepo(w io.Writer, p NewRepoParams) error {
-
return parse("new-repo.html").Execute(w, p)
+
func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
+
return p.execute("repo/new", w, params)
}
type ProfilePageParams struct {
···
Repos []db.Repo
}
-
func ProfilePage(w io.Writer, p ProfilePageParams) error {
-
return parse("profile.html").Execute(w, p)
+
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
+
return p.execute("user/profile", w, params)
}
appview/pages/profile.html appview/pages/templates/user/profile.html
appview/pages/settings.html appview/pages/templates/settings/keys.html
+10
appview/pages/templates/errors/404.html
···
+
<html>
+
<title>404</title>
+
{{ template "layouts/head" . }}
+
<body>
+
{{ template "layouts/nav" . }}
+
<main>
+
<h3>404 &mdash; nothing like that here.</h3>
+
</main>
+
</body>
+
</html>
+10
appview/pages/templates/errors/500.html
···
+
<html>
+
<title>500</title>
+
{{ template "layouts/head" . }}
+
<body>
+
{{ template "layouts/nav" . }}
+
<main>
+
<h3>500 &mdash; something broke!</h3>
+
</main>
+
</body>
+
</html>
+21
appview/pages/templates/index.html
···
+
<html>
+
{{ template "layouts/head" . }}
+
+
<header>
+
<h1>{{ .meta.Title }}</h1>
+
<h2>{{ .meta.Description }}</h2>
+
</header>
+
<body>
+
<main>
+
<div class="index">
+
{{ range .info }}
+
<div class="index-name">
+
<a href="/{{ .Name }}">{{ .DisplayName }}</a>
+
</div>
+
<div class="desc">{{ .Desc }}</div>
+
<div>{{ .Idle }}</div>
+
{{ end }}
+
</div>
+
</main>
+
</body>
+
</html>
+18
appview/pages/templates/layouts/base.html
···
+
{{ define "layouts/base" }}
+
<!doctype html>
+
<html lang="en">
+
<head>
+
<meta charset="UTF-8" />
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
<script
+
src="https://unpkg.com/htmx.org@2.0.4"
+
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
+
crossorigin="anonymous"
+
></script>
+
<title>{{block "title" .}}{{end}}</title>
+
</head>
+
<body>
+
<main class="content">{{block "content" .}}{{end}}</main>
+
</body>
+
</html>
+
{{ end }}
+37
appview/pages/templates/layouts/head.html
···
+
<head>
+
<meta charset="utf-8" />
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
+
<link rel="stylesheet" href="/static/style.css" type="text/css" />
+
<link rel="icon" type="image/png" size="32x32" href="/static/legit.png" />
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
+
<meta name="htmx-config" content='{"selfRequestsOnly":false}' />
+
+
{{ if .parent }}
+
<title>
+
{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .parent }}/
+
</title>
+
+
{{ else if .path }}
+
<title>
+
{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .path }}
+
</title>
+
{{ else if .files }}
+
<title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }})</title>
+
{{ else if .commit }}
+
<title>{{ .meta.Title }} &mdash; {{ .name }}: {{ .commit.This }}</title>
+
{{ else if .branches }}
+
<title>{{ .meta.Title }} &mdash; {{ .name }}: refs</title>
+
{{ else if .commits }} {{ if .log }}
+
<title>{{ .meta.Title }} &mdash; {{ .name }}: log</title>
+
{{ else }}
+
<title>{{ .meta.Title }} &mdash; {{ .name }}</title>
+
{{ end }} {{ else }}
+
<title>{{ .meta.Title }}</title>
+
{{ end }} {{ if and .servername .gomod }}
+
<meta
+
name="go-import"
+
content="{{ .servername}}/{{ .name }} git https://{{ .servername }}/{{ .name }}"
+
/>
+
{{ end }}
+
<!-- other meta tags here -->
+
</head>
+13
appview/pages/templates/layouts/nav.html
···
+
{{ define "nav" }}
+
<nav>
+
<ul>
+
{{ if .name }}
+
<li><a href="/{{ .name }}">summary</a></li>
+
<li><a href="/{{ .name }}/refs">refs</a> {{ if .ref }}</li>
+
+
<li><a href="/{{ .name }}/tree/{{ .ref }}/">tree</a></li>
+
<li><a href="/{{ .name }}/log/{{ .ref }}">log</a> {{ end }}</li>
+
{{ end }}
+
</ul>
+
</nav>
+
{{ end }}
+11
appview/pages/templates/layouts/repo-header.html
···
+
{{ define "repo-header" }}
+
<header>
+
<h2>
+
<a href="/">all repos</a>
+
&mdash; {{ .displayname }} {{ if .ref }}
+
<span class="ref">@ {{ .ref }}</span>
+
{{ end }}
+
</h2>
+
<h3 class="desc">{{ .desc }}</h3>
+
</header>
+
{{ end }}
+5
appview/pages/templates/layouts/test.html
···
+
<p>Hello world!</p>
+
<div class="example">
+
<h1>Welcome</h1>
+
<p>This is a simple HTML example</p>
+
</div>
+100
appview/pages/templates/repo/commit.html
···
+
<html>
+
{{ template "layouts/head" . }}
+
+
{{ template "layouts/repo-header" . }}
+
<body>
+
{{ template "layouts/nav" . }}
+
<main>
+
<section class="commit">
+
<pre>{{- .commit.Message -}}</pre>
+
<div class="commit-info">
+
{{ .commit.Author.Name }} <a href="mailto:{{ .commit.Author.Email }}" class="commit-email">{{ .commit.Author.Email}}</a>
+
<div>{{ .commit.Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
+
</div>
+
+
<div>
+
<strong>commit</strong>
+
<p><a href="/{{ .name }}/commit/{{ .commit.This }}" class="commit-hash">
+
{{ .commit.This }}
+
</a>
+
</p>
+
</div>
+
+
{{ if .commit.Parent }}
+
<div>
+
<strong>parent</strong>
+
<p><a href="/{{ .name }}/commit/{{ .commit.Parent }}" class="commit-hash">
+
{{ .commit.Parent }}
+
</a></p>
+
</div>
+
+
{{ end }}
+
<div class="diff-stat">
+
<div>
+
{{ .stat.FilesChanged }} files changed,
+
{{ .stat.Insertions }} insertions(+),
+
{{ .stat.Deletions }} deletions(-)
+
</div>
+
<div>
+
<br>
+
<strong>jump to</strong>
+
{{ range .diff }}
+
<ul>
+
<li><a href="#{{ .Name.New }}">{{ .Name.New }}</a></li>
+
</ul>
+
{{ end }}
+
</div>
+
</div>
+
</section>
+
<section>
+
{{ $repo := .name }}
+
{{ $this := .commit.This }}
+
{{ $parent := .commit.Parent }}
+
{{ range .diff }}
+
<div id="{{ .Name.New }}">
+
<div class="diff">
+
{{ if .IsNew }}
+
<span class="diff-type">A</span>
+
{{ end }}
+
{{ if .IsDelete }}
+
<span class="diff-type">D</span>
+
{{ end }}
+
{{ if not (or .IsNew .IsDelete) }}
+
<span class="diff-type">M</span>
+
{{ end }}
+
{{ if .Name.Old }}
+
<a href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}">{{ .Name.Old }}</a>
+
{{ if .Name.New }}
+
&#8594;
+
<a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>
+
{{ end }}
+
{{ else }}
+
<a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>
+
{{- end -}}
+
{{ if .IsBinary }}
+
<p>Not showing binary file.</p>
+
{{ else }}
+
<pre>
+
{{- range .TextFragments -}}
+
<p>{{- .Header -}}</p>
+
{{- range .Lines -}}
+
{{- if eq .Op.String "+" -}}
+
<span class="diff-add">{{ .String }}</span>
+
{{- end -}}
+
{{- if eq .Op.String "-" -}}
+
<span class="diff-del">{{ .String }}</span>
+
{{- end -}}
+
{{- if eq .Op.String " " -}}
+
<span class="diff-noop">{{ .String }}</span>
+
{{- end -}}
+
{{- end -}}
+
{{- end -}}
+
{{- end -}}
+
</pre>
+
</div>
+
</div>
+
{{ end }}
+
</section>
+
</main>
+
</body>
+
</html>
+9
appview/pages/templates/repo/empty.html
···
+
<html>
+
{{ template "layouts/head" . }}
+
+
<body>
+
<main>
+
<p>This is an empty Git repository. Push some commits here.</p>
+
</main>
+
</body>
+
</html>
+34
appview/pages/templates/repo/file.html
···
+
<html>
+
{{ template "layouts/head" . }}
+
{{ template "layouts/repo-header" . }}
+
<body>
+
{{ template "layouts/nav" . }}
+
<main>
+
<p>{{ .path }} (<a style="color: gray" href="?raw=true">view raw</a>)</p>
+
{{if .chroma }}
+
<div class="chroma-file-wrapper">
+
{{ .content }}
+
</div>
+
{{else}}
+
<div class="file-wrapper">
+
<table>
+
<tbody><tr>
+
<td class="line-numbers">
+
<pre>
+
{{- range .linecount }}
+
<a id="L{{ . }}" href="#L{{ . }}">{{ . }}</a>
+
{{- end -}}
+
</pre>
+
</td>
+
<td class="file-content">
+
<pre>
+
{{- .content -}}
+
</pre>
+
</td>
+
</tbody></tr>
+
</table>
+
</div>
+
{{end}}
+
</main>
+
</body>
+
</html>
+23
appview/pages/templates/repo/log.html
···
+
<html>
+
{{ template "layouts/head" . }}
+
+
{{ template "layouts/repo-header" . }}
+
<body>
+
{{ template "layouts/nav" . }}
+
<main>
+
{{ $repo := .name }}
+
<div class="log">
+
{{ range .commits }}
+
<div>
+
<div><a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a></div>
+
<pre>{{ .Message }}</pre>
+
</div>
+
<div class="commit-info">
+
{{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a>
+
<div>{{ .Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
+
</div>
+
{{ end }}
+
</div>
+
</main>
+
</body>
+
</html>
+14
appview/pages/templates/repo/new.html
···
+
{{define "title"}}new repo{{end}}
+
+
{{define "content"}}
+
<h1>new repo</h1>
+
<form method="POST" action="/repo/new">
+
<label for="name">repo name</label>
+
<input type="text" id="name" name="name" required />
+
+
<label for="domain">domain</label>
+
<input type="domain" id="domain" name="domain" required />
+
+
<button type="submit">create repo</button>
+
</form>
+
{{end}}
+38
appview/pages/templates/repo/refs.html
···
+
<html>
+
{{ template "layouts/head" . }}
+
+
{{ template "layouts/repo-header" . }}
+
<body>
+
{{ template "layouts/nav" . }}
+
<main>
+
{{ $name := .name }}
+
<h3>branches</h3>
+
<div class="refs">
+
{{ range .branches }}
+
<div>
+
<strong>{{ .Name.Short }}</strong>
+
<a href="/{{ $name }}/tree/{{ .Name.Short }}/">browse</a>
+
<a href="/{{ $name }}/log/{{ .Name.Short }}">log</a>
+
<a href="/{{ $name }}/archive/{{ .Name.Short }}.tar.gz">tar.gz</a>
+
</div>
+
{{ end }}
+
</div>
+
{{ if .tags }}
+
<h3>tags</h3>
+
<div class="refs">
+
{{ range .tags }}
+
<div>
+
<strong>{{ .Name }}</strong>
+
<a href="/{{ $name }}/tree/{{ .Name }}/">browse</a>
+
<a href="/{{ $name }}/log/{{ .Name }}">log</a>
+
<a href="/{{ $name }}/archive/{{ .Name }}.tar.gz">tar.gz</a>
+
{{ if .Message }}
+
<pre>{{ .Message }}</pre>
+
</div>
+
{{ end }}
+
{{ end }}
+
</div>
+
{{ end }}
+
</main>
+
</body>
+
</html>
+36
appview/pages/templates/repo/repo.html
···
+
<html>
+
{{ template "layouts/head" . }}
+
+
{{ template "layouts/repo-header" . }}
+
+
<body>
+
{{ template "layouts/nav" . }}
+
<main>
+
{{ $repo := .name }}
+
<div class="log">
+
{{ range .commits }}
+
<div>
+
<div><a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a></div>
+
<pre>{{ .Message }}</pre>
+
</div>
+
<div class="commit-info">
+
{{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a>
+
<div>{{ .Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
+
</div>
+
{{ end }}
+
</div>
+
{{- if .readme }}
+
<article class="readme">
+
{{- .readme -}}
+
</article>
+
{{- end -}}
+
+
<div class="clone-url">
+
<strong>clone</strong>
+
<pre>
+
git clone https://{{ .servername }}/{{ .name }}
+
</pre>
+
</div>
+
</main>
+
</body>
+
</html>
+53
appview/pages/templates/repo/tree.html
···
+
<html>
+
+
{{ template "layouts/head" . }}
+
+
{{ template "layouts/repo-header" . }}
+
<body>
+
{{ template "layouts/nav" . }}
+
<main>
+
{{ $repo := .name }}
+
{{ $ref := .ref }}
+
{{ $parent := .parent }}
+
+
<div class="tree">
+
{{ if $parent }}
+
<div></div>
+
<div></div>
+
<div><a href="/{{ $repo }}/tree/{{ $ref }}/{{ .dotdot }}">..</a></div>
+
{{ end }}
+
{{ range .files }}
+
{{ if not .IsFile }}
+
<div class="mode">{{ .Mode }}</div>
+
<div class="size">{{ .Size }}</div>
+
<div>
+
{{ if $parent }}
+
<a href="/{{ $repo }}/tree/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}/</a>
+
{{ else }}
+
<a href="/{{ $repo }}/tree/{{ $ref }}/{{ .Name }}">{{ .Name }}/</a>
+
{{ end }}
+
</div>
+
{{ end }}
+
{{ end }}
+
{{ range .files }}
+
{{ if .IsFile }}
+
<div class="mode">{{ .Mode }}</div>
+
<div class="size">{{ .Size }}</div>
+
<div>
+
{{ if $parent }}
+
<a href="/{{ $repo }}/blob/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}</a>
+
{{ else }}
+
<a href="/{{ $repo }}/blob/{{ $ref }}/{{ .Name }}">{{ .Name }}</a>
+
{{ end }}
+
</div>
+
{{ end }}
+
{{ end }}
+
</div>
+
<article>
+
<pre>
+
{{- if .readme }}{{ .readme }}{{- end -}}
+
</pre>
+
</article>
+
</main>
+
</body>
+
</html>
+12
appview/pages/templates/user/login.html
···
+
{{ define "title" }}login{{end}} {{define "content"}}
+
<h1>login</h1>
+
<form method="POST" action="/login">
+
<label for="handle">handle</label>
+
<input type="text" id="handle" name="handle" required />
+
+
<label for="app_password">app password</label>
+
<input type="password" id="app_password" name="app_password" required />
+
+
<button type="submit">login</button>
+
</form>
+
{{end}}
appview/pages/timeline.html appview/pages/templates/timeline.html
+14 -9
appview/state/state.go
···
auth *auth.Auth
enforcer *rbac.Enforcer
tidClock *syntax.TIDClock
+
pages *pages.Pages
}
func Make() (*State, error) {
-
db, err := db.Make(appview.SqliteDbPath)
if err != nil {
return nil, err
···
clock := syntax.NewTIDClock(0)
-
return &State{db, auth, enforcer, clock}, nil
+
pgs := pages.NewPages()
+
+
return &State{db, auth, enforcer, clock, pgs}, nil
}
func (s *State) TID() string {
···
switch r.Method {
case http.MethodGet:
-
pages.Login(w, pages.LoginParams{})
+
err := s.pages.Login(w, pages.LoginParams{})
+
if err != nil {
+
log.Printf("rendering login page: %s", err)
+
}
return
case http.MethodPost:
handle := r.FormValue("handle")
···
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
-
pages.Timeline(w, pages.TimelineParams{
+
s.pages.Timeline(w, pages.TimelineParams{
User: user,
})
return
···
log.Println(err)
}
-
pages.Settings(w, pages.SettingsParams{
+
s.pages.Settings(w, pages.SettingsParams{
User: user,
PubKeys: pubKeys,
})
···
IsOwner: isOwner,
}
-
pages.Knot(w, p)
+
s.pages.Knot(w, p)
}
// get knots registered by this user
···
log.Println(err)
}
-
pages.Knots(w, pages.KnotsParams{
+
s.pages.Knots(w, pages.KnotsParams{
User: user,
Registrations: registrations,
})
···
func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
-
pages.NewRepo(w, pages.NewRepoParams{
+
s.pages.NewRepo(w, pages.NewRepoParams{
User: s.auth.GetUser(r),
})
case http.MethodPost:
···
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
}
-
pages.ProfilePage(w, pages.ProfilePageParams{
+
s.pages.ProfilePage(w, pages.ProfilePageParams{
LoggedInUser: s.auth.GetUser(r),
UserDid: ident.DID.String(),
UserHandle: ident.Handle.String(),