123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 | package render
import (
"fmt"
"html/template"
"path"
"strings"
"time"
"code.bruce-hill.com/repo-renderer/internal/gitx"
"code.bruce-hill.com/repo-renderer/internal/highlight"
"code.bruce-hill.com/repo-renderer/internal/markdown"
)
// ---- file pages ----
type fileView struct {
Kind string // code, markdown, image, binary
Name string
Size string
Modified string
Lines int // as wc -l counts, the same as the listings
Lexer string
Exec bool
Notice string
RawURL string
ImageURL string // for Kind image: the URL to show (themed copy for SVG)
Code template.HTML
LineNums template.HTML
Rendered template.HTML
}
var imageExts = map[string]bool{
".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true, ".avif": true, ".bmp": true, ".ico": true,
".svg": true,
}
func isMarkdown(name string) bool {
switch strings.ToLower(path.Ext(name)) {
case ".md", ".markdown", ".mdown", ".mkd":
return true
}
return false
}
func lineNumbers(n int) template.HTML {
var b strings.Builder
b.Grow(n * 32)
// Anchors are display:block, so no newlines between them: inside a
// <pre> those would add a second line per number.
for i := 1; i <= n; i++ {
fmt.Fprintf(&b, `<a id="L%d" href="#L%d">%d</a>`, i, i, i)
}
return template.HTML(b.String())
}
func (c *repoCtx) renderBlob(e *gitx.Entry) error {
cfg := c.s.Cfg
name := path.Base(e.Path)
themed := c.s.Lay.SVG(c.key, e.Path)
fv := &fileView{Name: name, Size: humanSize(e.Size), Exec: e.IsExecutable()}
if t := c.mtime[e.Path]; t > 0 {
fv.Modified = time.Unix(t, 0).UTC().Format("2006-01-02")
}
// The count the listings show, as wc -l counts; binary files have none.
if n := c.lines[e.Path]; n > 0 {
fv.Lines = n
}
// Too big to serve: decide from the size ls-tree reported and never
// read the blob, so a pusher cannot make every worker hold a huge
// file in memory at once.
if e.Size > cfg.RawMaxBytes {
if err := removeOutputs(themed); err != nil {
return err
}
if err := c.removeRaw(e.Path); err != nil {
return err
}
fv.Kind = "large"
fv.Notice = "This file is too large to serve directly; clone the repository to get it."
return c.writeFilePage(e, fv)
}
data, err := c.batch.Get(e.SHA)
if err != nil {
return err
}
binary := highlight.IsBinary(data)
ext := strings.ToLower(path.Ext(name))
// Raw copies get no gzip twin: raw text is rarely fetched, and a
// committed "x.gz" would otherwise collide with x's.
if err := writeFileAtomic(c.s.Lay.Raw(c.key, e.Path), data); err != nil {
return err
}
fv.RawURL = c.s.RawURL(c.key, e.Path)
// An SVG also gets a themed copy for embedding (see themeSVG).
if ext == ".svg" {
if err := writePair(themed, themeSVG(data, svgRootEnd(data), c.s.HL.TextColour(), c.s.HL.LightTextColour()), true); err != nil {
return err
}
}
switch {
case imageExts[ext] && (binary || ext == ".svg"):
fv.Kind = "image"
fv.ImageURL = fv.RawURL
if ext == ".svg" {
fv.ImageURL = c.s.SVGURL(c.key, e.Path)
}
case binary:
fv.Kind = "binary"
case e.Size > cfg.HighlightMaxBytes:
// Text, but embedding megabytes of it in a page helps nobody and
// costs the renderer several times the file size in memory.
fv.Kind = "large"
fv.Notice = "This file is too large to display here; use the raw link."
case isMarkdown(name) && e.Size <= cfg.MarkdownMaxBytes:
fv.Kind = "markdown"
rendered, err := c.s.MD.Render(data, c.mdContext(path.Dir(e.Path)))
if err != nil {
return err
}
fv.Rendered = rendered
lx := c.s.HL.ByName("markdown")
fv.Lexer = displayLang(highlight.Name(lx))
fv.Code = c.s.HL.HTML(data, lx)
fv.LineNums = lineNumbers(highlight.LineCount(data))
default:
fv.Kind = "code"
lx := c.s.HL.ByFile(e.Path, data)
fv.Lexer = displayLang(highlight.Name(lx))
fv.Code = c.s.HL.HTML(data, lx)
fv.LineNums = lineNumbers(highlight.LineCount(data))
}
return c.writeFilePage(e, fv)
}
// writeFilePage renders and writes the HTML page for one blob.
func (c *repoCtx) writeFilePage(e *gitx.Entry, fv *fileView) error {
page := c.page()
page.Title = c.repo + c.branchSuffix() + "/" + e.Path
page.URL = c.s.absURL(c.s.CodeURL(c.key, e.Path))
page.BodyClass = "file file-" + fv.Kind
page.Crumbs = c.s.crumbsFor(c.key, e.Path)
page.Drawer = c.drawer(e.Path)
page.File = fv
html, err := c.s.render("file", page)
if err != nil {
return err
}
return writePair(c.s.Lay.Page(c.key, e.Path), html, true)
}
func (c *repoCtx) mdContext(dir string) markdown.Context {
if dir == "." {
dir = ""
}
repo := c.key
return markdown.Context{
Dir: dir,
PageURL: func(p string) string { return c.s.CodeURL(repo, p) },
ImageURL: func(p string) string { return c.s.imageURL(repo, p) },
}
}
// imageURL picks the URL to embed for an image path: SVGs come from the
// themed copy under /svg, everything else from /raw.
func (s *Site) imageURL(repo, p string) string {
if strings.EqualFold(path.Ext(p), ".svg") {
return s.SVGURL(repo, p)
}
return s.RawURL(repo, p)
}
|