Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/modules/blob.go

d658fcc4ea9f3bb562fd00dce8f863934560c56c · 5.1 KB · 164 lines raw

1 package modules
2
3 import (
4 "bytes"
5 "html/template"
6 "net/http"
7 "strings"
8 "unicode/utf8"
9
10 "gsg/internal/core"
11 )
12
13 type Blob struct {
14 Path string
15 Size int
16 Binary bool
17 Image string // validated raster MIME, "" when not a previewable image
18 Lines []Line
19 Submodule *Submodule
20 Lang string // "json" or "yaml", when the path implies one; "" otherwise
21 ParseErr *ParseNote // set when Lang is non-empty and the content doesn't parse
22 Oversize bool // Lang is set, but the file is too large to parse and index
23 }
24
25 // Structured reports whether the fold/search/reference tooling applies to
26 // this file. A recognised extension is not enough on its own: past
27 // maxStructuredSize the view falls back to plain numbered lines.
28 func (b Blob) Structured() bool { return b.Lang != "" && !b.Oversize }
29
30 // StructuredLimit is the size past which that tooling switches itself off,
31 // so the banner explaining the fallback can name the threshold.
32 func (b Blob) StructuredLimit() int { return maxStructuredSize }
33
34 // Preview reports whether the image is small enough to render inline. An
35 // image over maxPreviewSize is still downloadable; it just is not fetched a
36 // second time by every reader who only wanted to see the tree around it.
37 func (b Blob) Preview() bool { return b.Image != "" && b.Size <= maxPreviewSize }
38
39 // PreviewLimit names the threshold for the too-large notice.
40 func (b Blob) PreviewLimit() int { return maxPreviewSize }
41
42 // Submodule describes a gitlink entry (mode 160000): a pointer to a commit
43 // in another repository rather than file content, so there is nothing to
44 // diff or display line-by-line.
45 type Submodule struct {
46 SHA string
47 Remote string // from .gitmodules, best-effort; empty if not found
48 }
49
50 // Line is one rendered source line. Everything past Text is structure the
51 // blob view uses to fold, filter and cross-reference JSON/YAML, and is zero
52 // for every other language.
53 type Line struct {
54 N int
55 Text template.HTML
56
57 Depth int // nesting level, in indent units
58 FoldTo int // last line of the block this one opens; 0 if it opens none
59 Key string // mapping key declared here, for key-scoped search
60 Path string // canonical value path, for path queries; "" when unknown
61 Anchor *Anchor // YAML anchor declared here
62 Ref *StructRef // YAML alias resolved here
63 }
64
65 func init() { core.Register("blob", blobMod) }
66
67 // blob: file contents at Ctx.Path, split into numbered lines.
68 // Binary detection is git's own heuristic: a NUL in the first 8000 bytes.
69 func blobMod(c *core.Ctx, args ...string) (any, error) {
70 path := c.Path
71 if len(args) > 0 {
72 path = args[0]
73 }
74 if path == "" {
75 return nil, core.Errorf(http.StatusNotFound, "no path")
76 }
77 if entry, ok, _ := c.Repo.Stat(c.Commit, path); ok && entry.Type == "commit" {
78 return Blob{Path: path, Submodule: &Submodule{
79 SHA: entry.SHA,
80 Remote: c.Repo.SubmoduleURL(c.Commit, path),
81 }}, nil
82 }
83 raw, err := c.Repo.Blob(c.Commit, path)
84 if err != nil {
85 return nil, core.Errorf(http.StatusNotFound, "no such file %q", path)
86 }
87 b := Blob{Path: path, Size: len(raw)}
88
89 head := raw
90 if len(head) > 8000 {
91 head = head[:8000]
92 }
93 // Check the image magic before the generic binary test: a valid raster
94 // image is served as a preview rather than a wall of numbered bytes.
95 b.Image = imageContentType(path, raw)
96 if b.Image != "" {
97 b.Binary = true
98 return b, nil
99 }
100 if bytes.IndexByte(head, 0) >= 0 || !utf8.Valid(head) {
101 b.Binary = true
102 return b, nil
103 }
104 b.Lang = structuredLang(path)
105 b.Oversize = b.Lang != "" && len(raw) > maxStructuredSize
106 if b.Structured() {
107 b.ParseErr = validateStructured(b.Lang, raw)
108 }
109
110 hl, ok := highlightLines(path, raw)
111 if !ok {
112 for _, t := range strings.Split(strings.TrimRight(string(raw), "\n"), "\n") {
113 hl = append(hl, template.HTML(template.HTMLEscapeString(t)))
114 }
115 } else if b.Structured() {
116 hl = withIndentGuides(hl, indentUnit(raw))
117 }
118 for i, t := range hl {
119 b.Lines = append(b.Lines, Line{N: i + 1, Text: t})
120 }
121 if b.Structured() {
122 annotateStructure(b.Lines, raw, b.Lang)
123 }
124 return b, nil
125 }
126
127 // RawBlob is the unmodified object, for the /raw/ route. Kept separate from
128 // Blob so the line-split view and the byte stream cache independently.
129 type RawBlob struct {
130 Path string
131 Data []byte
132 Binary bool
133 Image string // validated raster MIME; see imageContentType
134 }
135
136 func (r RawBlob) Bytes() []byte { return r.Data }
137 func (r RawBlob) IsBinary() bool { return r.Binary }
138 func (r RawBlob) InlineContentType() string { return r.Image }
139
140 func init() { core.Register("blobraw", blobRawMod) }
141
142 func blobRawMod(c *core.Ctx, args ...string) (any, error) {
143 path := c.Path
144 if len(args) > 0 {
145 path = args[0]
146 }
147 if path == "" {
148 return nil, core.Errorf(http.StatusNotFound, "no path")
149 }
150 raw, err := c.Repo.Blob(c.Commit, path)
151 if err != nil {
152 return nil, core.Errorf(http.StatusNotFound, "no such file %q", path)
153 }
154 head := raw
155 if len(head) > 8000 {
156 head = head[:8000]
157 }
158 return RawBlob{
159 Path: path,
160 Data: raw,
161 Binary: bytes.IndexByte(head, 0) >= 0,
162 Image: imageContentType(path, raw),
163 }, nil
164 }