Commit aa03e8c8e0

aa03e8c8e02357cfe5f94188fdab3c79846cebae

parent: 0a69a8facd

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-12 01:23 UTC

httpd: budget snippet rendering, fix a dead branch and a lost failure flash

snippetPage now fills Content only for the owner and spends
maxRenderBytes across the page's files in order, so a snippet with
many or oversized files no longer highlights every byte for every
viewer; a file that does not fit gets a raw link instead. snippetAction
always dispatched through runControlStdinCode, since an empty stdin
string already reads as empty either way, and a failed delete now
lands back on the snippet's own page with the message rather than
silently redirecting to the list. highlight gets a sibling,
highlightPlain, without linkable line-number ids, since a page with
several highlighted files repeated id="L1" per file.

Ref #195
internal/httpd/snippets.go +35 −19
@@ -66,13 +66,18 @@ func (s *Server) snippetsPage(w http.ResponseWriter, r *http.Request) {
6666}
6767
6868type snippetFileView struct {
69 Name string
70 Size int64
71 Lines int
72 Content string
73 HTML template.HTML
69 Name string
70 Size int64
71 Lines int
72 Content string
73 HTML template.HTML
74 TooLarge bool
7475}
7576
77// snippetPage highlights files up to a shared budget across the page: a
78// snippet with many or large files does not make one request highlight
79// megabytes of markup. Content is filled only for the owner, whose edit
80// textarea needs the raw text regardless of the budget.
7681func (s *Server) snippetPage(w http.ResponseWriter, r *http.Request) {
7782 sn, viewer, ok := s.snippetScope(w, r)
7883 if !ok {
@@ -83,13 +88,25 @@ func (s *Server) snippetPage(w http.ResponseWriter, r *http.Request) {
8388 http.Error(w, "internal error", http.StatusInternalServerError)
8489 return
8590 }
91 canWrite := policy.CanWriteSnippet(viewer, sn)
92 budget := int64(maxRenderBytes)
8693 views := make([]snippetFileView, 0, len(files))
8794 for _, f := range files {
8895 lines := bytes.Count(f.Content, []byte("\n"))
8996 if len(f.Content) > 0 && f.Content[len(f.Content)-1] != '\n' {
9097 lines++
9198 }
92 views = append(views, snippetFileView{f.Name, f.Size, lines, string(f.Content), highlight(f.Name, f.Content)})
99 view := snippetFileView{Name: f.Name, Size: f.Size, Lines: lines}
100 if canWrite {
101 view.Content = string(f.Content)
102 }
103 if f.Size <= budget {
104 view.HTML = highlightPlain(f.Name, f.Content)
105 budget -= f.Size
106 } else {
107 view.TooLarge = true
108 }
109 views = append(views, view)
93110 }
94111 s.render(w, "snippet.html", struct {
95112 basePage
@@ -98,7 +115,7 @@ func (s *Server) snippetPage(w http.ResponseWriter, r *http.Request) {
98115 Files []snippetFileView
99116 CanWrite bool
100117 Notice string
101 }{s.baseFor(viewer), sn.OwnerName, sn, views, policy.CanWriteSnippet(viewer, sn), s.takeFlash(w, r)})
118 }{s.baseFor(viewer), sn.OwnerName, sn, views, canWrite, s.takeFlash(w, r)})
102119}
103120
104121// snippetRaw serves one file as text, inert on the forge's origin.
@@ -147,28 +164,27 @@ func (s *Server) snippetNewSubmit(w http.ResponseWriter, r *http.Request, u stor
147164 http.Redirect(w, r, "/"+u.Username+"/-/snippets/"+out.ID, http.StatusSeeOther)
148165}
149166
150// snippetAction runs a write on the snippet in the URL and returns to
151// its page with the message, or to the list after a delete. A snippet
152// the viewer may not read is the 404 page, as on every read.
167// snippetAction runs a write on the snippet in the URL and returns to its
168// page with the message, or to dest (the list, for a delete) on success.
169// A snippet the viewer may not read is the 404 page, as on every read.
153170func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.User, argv []string, stdin string, dest string) {
154171 sn, _, ok := s.snippetScope(w, r)
155172 if !ok {
156173 return
157174 }
175 page := "/" + sn.OwnerName + "/-/snippets/" + sn.PublicID
158176 if dest == "" {
159 dest = "/" + sn.OwnerName + "/-/snippets/" + sn.PublicID
177 dest = page
160178 }
161179 back := func(w http.ResponseWriter, r *http.Request, msg string) {
162180 s.setFlash(w, msg)
163 http.Redirect(w, r, dest, http.StatusSeeOther)
164 }
165 var msg string
166 var code int
167 if stdin == "" {
168 _, msg, code = s.runControlCode(u, argv)
169 } else {
170 msg, code = s.runControlStdinCode(u, argv, stdin)
181 to := dest
182 if msg != "" {
183 to = page
184 }
185 http.Redirect(w, r, to, http.StatusSeeOther)
171186 }
187 msg, code := s.runControlStdinCode(u, argv, stdin)
172188 if code == protocol.ExitDenied {
173189 http.Error(w, msg, http.StatusForbidden)
174190 return
internal/httpd/web.go +16 −1
@@ -910,7 +910,22 @@ var chromaFormatter = html.New(html.WithClasses(true),
910910 html.WithLineNumbers(true), html.LineNumbersInTable(false),
911911 html.WithLinkableLineNumbers(true, "L"))
912912
913// chromaFormatterPlain is chromaFormatter without linkable line numbers,
914// for a page that highlights more than one file: linkable ids are
915// per-file line numbers, so several files on one page would repeat
916// id="L1", id="L2", ...
917var chromaFormatterPlain = html.New(html.WithClasses(true),
918 html.WithLineNumbers(true), html.LineNumbersInTable(false))
919
913920func highlight(filePath string, data []byte) template.HTML {
921 return highlightWith(chromaFormatter, filePath, data)
922}
923
924func highlightPlain(filePath string, data []byte) template.HTML {
925 return highlightWith(chromaFormatterPlain, filePath, data)
926}
927
928func highlightWith(formatter *html.Formatter, filePath string, data []byte) template.HTML {
914929 lexer := lexers.Match(filePath)
915930 if lexer == nil {
916931 lexer = lexers.Fallback
@@ -920,7 +935,7 @@ func highlight(filePath string, data []byte) template.HTML {
920935 return template.HTML("<pre>" + template.HTMLEscapeString(string(data)) + "</pre>")
921936 }
922937 var buf bytes.Buffer
923 if err := chromaFormatter.Format(&buf, styles.Get(lightStyle), iterator); err != nil {
938 if err := formatter.Format(&buf, styles.Get(lightStyle), iterator); err != nil {
924939 return template.HTML("<pre>" + template.HTMLEscapeString(string(data)) + "</pre>")
925940 }
926941 return template.HTML(buf.String())
internal/web/static/style.css +1 −1
@@ -1499,7 +1499,7 @@ table.tree td.name.dir a { color: var(--accent); }
14991499p.clone { margin: 0 0 var(--sp-3); }
15001500p.filefacts { color: var(--muted); font-size: var(--fs-1); margin: 0 0 var(--sp-3); }
15011501.pathbar .actions { font-size: var(--fs-1); color: var(--muted); }
1502.snippetfile { margin-bottom: 1.5rem }
1502.snippetfile { margin-bottom: var(--sp-5); }
15031503
15041504/* merge request: a two-column split, so state has somewhere to live that
15051505 is not a run-on sentence under the title */
internal/web/templates/snippet.html +1 −1
@@ -12,7 +12,7 @@
1212 <span class="actions"><a href="/{{$.Owner}}/-/snippets/{{$.Snippet.PublicID}}/raw/{{.Name}}">raw</a></span>
1313</div>
1414<p class="filefacts">{{.Lines}} lines · {{.Size}} bytes</p>
15<div class="code">{{.HTML}}</div>
15{{if .TooLarge}}<p class="empty-note">too large to render here — <a href="/{{$.Owner}}/-/snippets/{{$.Snippet.PublicID}}/raw/{{.Name}}">raw</a></p>{{else}}<div class="code">{{.HTML}}</div>{{end}}
1616{{if $.CanWrite}}<details class="editbox"><summary>edit {{.Name}}</summary>
1717<form method="post" action="/{{$.Owner}}/-/snippets/{{$.Snippet.PublicID}}/file" class="commentform">
1818<input type="hidden" name="name" value="{{.Name}}">