123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 | package render
import (
"html"
"html/template"
"maps"
"slices"
"strings"
"code.bruce-hill.com/repo-renderer/internal/gitx"
)
// The file drawer is a document of its own, shown in an <iframe> and
// shared by every page of the repository, so a change to the tree
// rewrites one document instead of every page. It carries the whole tree,
// each folder a closed <details> whose name links to its page.
//
// A page selects its own entry with the fragment of the frame's URL, its
// escaped path. Files carry it as an id, and folders on the list of their
// entries, so that a folder's page opens it. The browser opens the
// <details> around the target and scrolls to it, and the stylesheet marks
// it, without script. (Browsers that do not open them show the tree shut,
// with the page's breadcrumbs still there.) The ids are on <li>s and
// <ul>s, not links: a browser focuses a target it can, which would pull
// the page's focus into the frame. The header has id ".", for the root.
// sortedChildren returns a directory's children, directories first, then by
// name.
func sortedChildren(n *node) []*node {
out := slices.Collect(maps.Values(n.children))
slices.SortFunc(out, func(a, b *node) int {
if a.dir != b.dir {
if a.dir {
return -1
}
return 1
}
return strings.Compare(a.name, b.name)
})
return out
}
// drawerView is the frame showing the drawer document.
type drawerView struct {
Src string
}
// drawerID is the fragment that selects path p. The browser matches a
// fragment against ids before decoding it, so the escaped form is the id
// too.
func drawerID(p string) string { return escapePath(p) }
// drawer is the frame for the page at current ("" is the root).
func (c *repoCtx) drawer(current string) *drawerView {
id := "."
if current != "" {
id = drawerID(current)
}
return &drawerView{Src: c.s.FileTreeURL(c.key) + "#" + id}
}
type drawerDoc struct {
Name string // repository (and branch) shown at the top
URL string
Tree template.HTML
}
// drawerChanged reports whether changes add, remove or retype entries,
// or repoint a symlink, which changes the drawer; edits leave it alone.
func drawerChanged(changes []gitx.Change, t *tree) bool {
for _, ch := range changes {
if ch.Status != 'M' {
return true
}
if e := t.byPath[ch.Path]; e != nil && e.IsSymlink() {
return true
}
}
return false
}
// writeDrawer writes the repository's drawer document.
func (c *repoCtx) writeDrawer() error {
var b strings.Builder
if err := c.writeTree(&b, c.tree.root, ""); err != nil {
return err
}
html, err := c.s.renderDoc("drawer", drawerDoc{
Name: c.repo + c.branchSuffix(),
URL: c.s.CodeURL(c.key, ""),
Tree: template.HTML(b.String()),
})
if err != nil {
return err
}
return writePair(c.s.Lay.FileTree(c.key), html, true)
}
// writeEntry writes a file, symlink or submodule. Symlinks and submodules
// have no page: a symlink links to its target's, when that is in the
// repository, and is otherwise plain text, as a submodule is. A file
// carries id, if any: the fragment its page selects.
func (c *repoCtx) writeEntry(b *strings.Builder, ch *node, full, id string) error {
switch {
case ch.entry.IsSymlink():
url, err := c.symlinkURL(full)
if err != nil {
return err
}
if url == "" {
b.WriteString(`<li class="s"><span>` + iconLink)
b.WriteString(html.EscapeString(ch.name))
b.WriteString(`</span></li>`)
break
}
b.WriteString(`<li class="s"><a href="`)
b.WriteString(html.EscapeString(url))
b.WriteString(`">` + iconLink)
b.WriteString(html.EscapeString(ch.name))
b.WriteString(`</a></li>`)
case ch.entry.IsSubmodule():
b.WriteString(`<li class="m"><span>` + iconModule)
b.WriteString(html.EscapeString(ch.name))
b.WriteString(`</span></li>`)
default:
b.WriteString(`<li class="f"`)
if id != "" {
b.WriteString(` id="` + html.EscapeString(id) + `"`)
}
b.WriteString(`><a href="`)
b.WriteString(html.EscapeString(c.s.CodeURL(c.key, full)))
b.WriteString(`">` + iconFile)
b.WriteString(html.EscapeString(ch.name))
b.WriteString(`</a></li>`)
}
return nil
}
// writeTree writes n's entries and, inside each folder's closed
// <details>, the folder's own. A folder's icon is outside its link, so a
// click on it toggles the folder, and one on the name opens its page.
func (c *repoCtx) writeTree(b *strings.Builder, n *node, prefix string) error {
b.WriteString(`<ul`)
if prefix != "" {
b.WriteString(` id="` + html.EscapeString(drawerID(prefix)) + `"`)
}
b.WriteString(`>`)
for _, ch := range sortedChildren(n) {
full := ch.name
if prefix != "" {
full = prefix + "/" + ch.name
}
if !ch.dir {
if err := c.writeEntry(b, ch, full, drawerID(full)); err != nil {
return err
}
continue
}
b.WriteString(`<li class="d"><details><summary>` + iconDir + `<a href="`)
b.WriteString(html.EscapeString(c.s.CodeURL(c.key, full)))
b.WriteString(`">` + html.EscapeString(ch.name) + `/</a></summary>`)
if err := c.writeTree(b, ch, full); err != nil {
return err
}
b.WriteString(`</details></li>`)
}
b.WriteString(`</ul>`)
return nil
}
|