package modules import ( "bytes" "html/template" "net/http" "strings" "unicode/utf8" "gsg/internal/core" ) type Blob struct { Path string Size int Binary bool Image string // validated raster MIME, "" when not a previewable image Lines []Line Submodule *Submodule Lang string // "json" or "yaml", when the path implies one; "" otherwise ParseErr *ParseNote // set when Lang is non-empty and the content doesn't parse Oversize bool // Lang is set, but the file is too large to parse and index } // Structured reports whether the fold/search/reference tooling applies to // this file. A recognised extension is not enough on its own: past // maxStructuredSize the view falls back to plain numbered lines. func (b Blob) Structured() bool { return b.Lang != "" && !b.Oversize } // StructuredLimit is the size past which that tooling switches itself off, // so the banner explaining the fallback can name the threshold. func (b Blob) StructuredLimit() int { return maxStructuredSize } // Preview reports whether the image is small enough to render inline. An // image over maxPreviewSize is still downloadable; it just is not fetched a // second time by every reader who only wanted to see the tree around it. func (b Blob) Preview() bool { return b.Image != "" && b.Size <= maxPreviewSize } // PreviewLimit names the threshold for the too-large notice. func (b Blob) PreviewLimit() int { return maxPreviewSize } // Submodule describes a gitlink entry (mode 160000): a pointer to a commit // in another repository rather than file content, so there is nothing to // diff or display line-by-line. type Submodule struct { SHA string Remote string // from .gitmodules, best-effort; empty if not found } // Line is one rendered source line. Everything past Text is structure the // blob view uses to fold, filter and cross-reference JSON/YAML, and is zero // for every other language. type Line struct { N int Text template.HTML Depth int // nesting level, in indent units FoldTo int // last line of the block this one opens; 0 if it opens none Key string // mapping key declared here, for key-scoped search Path string // canonical value path, for path queries; "" when unknown Anchor *Anchor // YAML anchor declared here Ref *StructRef // YAML alias resolved here } func init() { core.Register("blob", blobMod) } // blob: file contents at Ctx.Path, split into numbered lines. // Binary detection is git's own heuristic: a NUL in the first 8000 bytes. func blobMod(c *core.Ctx, args ...string) (any, error) { path := c.Path if len(args) > 0 { path = args[0] } if path == "" { return nil, core.Errorf(http.StatusNotFound, "no path") } if entry, ok, _ := c.Repo.Stat(c.Commit, path); ok && entry.Type == "commit" { return Blob{Path: path, Submodule: &Submodule{ SHA: entry.SHA, Remote: c.Repo.SubmoduleURL(c.Commit, path), }}, nil } raw, err := c.Repo.Blob(c.Commit, path) if err != nil { return nil, core.Errorf(http.StatusNotFound, "no such file %q", path) } b := Blob{Path: path, Size: len(raw)} head := raw if len(head) > 8000 { head = head[:8000] } // Check the image magic before the generic binary test: a valid raster // image is served as a preview rather than a wall of numbered bytes. b.Image = imageContentType(path, raw) if b.Image != "" { b.Binary = true return b, nil } if bytes.IndexByte(head, 0) >= 0 || !utf8.Valid(head) { b.Binary = true return b, nil } b.Lang = structuredLang(path) b.Oversize = b.Lang != "" && len(raw) > maxStructuredSize if b.Structured() { b.ParseErr = validateStructured(b.Lang, raw) } hl, ok := highlightLines(path, raw) if !ok { for _, t := range strings.Split(strings.TrimRight(string(raw), "\n"), "\n") { hl = append(hl, template.HTML(template.HTMLEscapeString(t))) } } else if b.Structured() { hl = withIndentGuides(hl, indentUnit(raw)) } for i, t := range hl { b.Lines = append(b.Lines, Line{N: i + 1, Text: t}) } if b.Structured() { annotateStructure(b.Lines, raw, b.Lang) } return b, nil } // RawBlob is the unmodified object, for the /raw/ route. Kept separate from // Blob so the line-split view and the byte stream cache independently. type RawBlob struct { Path string Data []byte Binary bool Image string // validated raster MIME; see imageContentType } func (r RawBlob) Bytes() []byte { return r.Data } func (r RawBlob) IsBinary() bool { return r.Binary } func (r RawBlob) InlineContentType() string { return r.Image } func init() { core.Register("blobraw", blobRawMod) } func blobRawMod(c *core.Ctx, args ...string) (any, error) { path := c.Path if len(args) > 0 { path = args[0] } if path == "" { return nil, core.Errorf(http.StatusNotFound, "no path") } raw, err := c.Repo.Blob(c.Commit, path) if err != nil { return nil, core.Errorf(http.StatusNotFound, "no such file %q", path) } head := raw if len(head) > 8000 { head = head[:8000] } return RawBlob{ Path: path, Data: raw, Binary: bytes.IndexByte(head, 0) >= 0, Image: imageContentType(path, raw), }, nil }