cerrado @ c7a8aa113a914e70e027fea93265c7232b865b5e

feat: Add compression
  1diff --git a/go.mod b/go.mod
  2index 5bd4d7647b43cb737a873b5636a162a1ea25df69..cc63e5d26352e2c39488c00f139e5fdbe17cf13f 100644
  3--- a/go.mod
  4+++ b/go.mod
  5@@ -5,9 +5,11 @@
  6 require (
  7 	git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082
  8 	github.com/alecthomas/chroma/v2 v2.13.0
  9+	github.com/andybalholm/brotli v1.1.0
 10 	github.com/go-git/go-git/v5 v5.12.0
 11 	github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
 12 	github.com/google/go-cmp v0.6.0
 13+	github.com/klauspost/compress v1.17.8
 14 	github.com/valyala/quicktemplate v1.7.0
 15 	golang.org/x/sync v0.7.0
 16 )
 17diff --git a/go.sum b/go.sum
 18index 69c34b73d113fa1f0ae49c5068a2517c3ee91725..f010dc9d81e57367a63071c716ea39fe1f0960a3 100644
 19--- a/go.sum
 20+++ b/go.sum
 21@@ -15,6 +15,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
 22 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 23 github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 24 github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 25+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 26+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
 27 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 28 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 29 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 30@@ -59,6 +61,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 31 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 32 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 33 github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 34+github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
 35+github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 36 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 37 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 38 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 39diff --git a/pkg/ext/compression.go b/pkg/ext/compression.go
 40new file mode 100644
 41index 0000000000000000000000000000000000000000..92144b82132968aa7db6115aad78f37d71c8e354
 42--- /dev/null
 43+++ b/pkg/ext/compression.go
 44@@ -0,0 +1,142 @@
 45+package ext
 46+
 47+import (
 48+	"compress/gzip"
 49+	"compress/lzw"
 50+	"errors"
 51+	"io"
 52+	"log/slog"
 53+	"net/http"
 54+	"strconv"
 55+	"strings"
 56+
 57+	"git.gabrielgio.me/cerrado/pkg/u"
 58+	"github.com/andybalholm/brotli"
 59+	"github.com/klauspost/compress/zstd"
 60+)
 61+
 62+var (
 63+	invalidParamErr = errors.New("Invalid weighted param")
 64+)
 65+
 66+type CompressionResponseWriter struct {
 67+	innerWriter    http.ResponseWriter
 68+	compressWriter io.Writer
 69+}
 70+
 71+func Compress(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
 72+	return func(w http.ResponseWriter, r *http.Request) {
 73+		if accept, ok := r.Header["Accept-Encoding"]; ok {
 74+			if compress, algo := GetCompressionWriter(u.FirstOrZero(accept), w); algo != "" {
 75+				defer compress.Close()
 76+				w.Header().Add("Content-Encoding", algo)
 77+				w = &CompressionResponseWriter{
 78+					innerWriter:    w,
 79+					compressWriter: compress,
 80+				}
 81+			}
 82+		}
 83+		next(w, r)
 84+	}
 85+}
 86+
 87+func GetCompressionWriter(header string, inner io.Writer) (io.WriteCloser, string) {
 88+	c := GetCompression(header)
 89+	switch c {
 90+	case "br":
 91+		return GetBrotliWriter(inner), c
 92+	case "gzip":
 93+		return GetGZIPWriter(inner), c
 94+	case "compress":
 95+		return GetLZWWriter(inner), c
 96+	case "zstd":
 97+		return GetZSTDWriter(inner), c
 98+	default:
 99+		return nil, ""
100+	}
101+
102+}
103+
104+func (c *CompressionResponseWriter) Header() http.Header {
105+	return c.innerWriter.Header()
106+}
107+func (c *CompressionResponseWriter) Write(b []byte) (int, error) {
108+	return c.compressWriter.Write(b)
109+}
110+
111+func (c *CompressionResponseWriter) WriteHeader(statusCode int) {
112+	c.innerWriter.WriteHeader(statusCode)
113+}
114+
115+func GetCompression(header string) string {
116+	c := "*"
117+	q := 0.0
118+
119+	if header == "" {
120+		return c
121+	}
122+
123+	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
124+	for _, e := range strings.Split(header, ",") {
125+		ps := strings.Split(e, ";")
126+		if len(ps) == 2 {
127+			w, err := getWeighedValue(ps[1])
128+			if err != nil {
129+				slog.Error(
130+					"Error parsing weighting from Accept-Encoding",
131+					"error", err,
132+				)
133+				continue
134+			}
135+			// gettting weighting value
136+			if w > q {
137+				q = w
138+				c = strings.Trim(ps[0], " ")
139+			}
140+		} else {
141+			if 1 > q {
142+				q = 1
143+				c = strings.Trim(ps[0], " ")
144+			}
145+		}
146+	}
147+
148+	return c
149+}
150+
151+func GetGZIPWriter(w io.Writer) io.WriteCloser {
152+	// error can be ignored here since it will only err when compression level
153+	// is not valid
154+	r, _ := gzip.NewWriterLevel(w, gzip.BestCompression)
155+	return r
156+}
157+
158+func GetBrotliWriter(w io.Writer) io.WriteCloser {
159+	return brotli.NewWriterLevel(w, brotli.BestCompression)
160+}
161+
162+func GetZSTDWriter(w io.Writer) io.WriteCloser {
163+	// error can be ignored here since it will only opts are given
164+	r, _ := zstd.NewWriter(w)
165+	return r
166+}
167+
168+func GetLZWWriter(w io.Writer) io.WriteCloser {
169+	return lzw.NewWriter(w, lzw.LSB, 8)
170+}
171+
172+func getWeighedValue(part string) (float64, error) {
173+	ps := strings.SplitN(part, "=", 2)
174+	if len(ps) != 2 {
175+		return 0, invalidParamErr
176+	}
177+	if name := strings.TrimSpace(ps[0]); name == "q" {
178+		w, err := strconv.ParseFloat(ps[1], 64)
179+		if err != nil {
180+			return 0, err
181+		}
182+		return w, nil
183+	}
184+
185+	return 0, invalidParamErr
186+}
187diff --git a/pkg/ext/compression_test.go b/pkg/ext/compression_test.go
188new file mode 100644
189index 0000000000000000000000000000000000000000..64243780d97c90cee130ae60d5652275dd7d9814
190--- /dev/null
191+++ b/pkg/ext/compression_test.go
192@@ -0,0 +1,42 @@
193+// go:build unit
194+package ext
195+
196+import "testing"
197+
198+func TestGetCompression(t *testing.T) {
199+	testCases := []struct {
200+		name        string
201+		header      string
202+		compression string
203+	}{
204+		{
205+			name:        "Empty",
206+			header:      "",
207+			compression: "*",
208+		},
209+		{
210+			name:        "Weighted",
211+			header:      "gzip;q=1.0, *;q=0.5",
212+			compression: "gzip",
213+		},
214+		{
215+			name:        "Mixed",
216+			header:      "deflate, gzip;q=1.0, *;q=0.5",
217+			compression: "deflate",
218+		},
219+		{
220+			name:        "Not weighted",
221+			header:      "zstd, deflate, gzip",
222+			compression: "zstd",
223+		},
224+	}
225+
226+	for _, tc := range testCases {
227+		t.Run(tc.name, func(t *testing.T) {
228+			got := GetCompression(tc.header)
229+			if got != tc.compression {
230+				t.Errorf("Wrong compression returned: got %s want %s", got, tc.compression)
231+			}
232+		})
233+	}
234+}
235diff --git a/pkg/ext/mime.go b/pkg/ext/mime.go
236new file mode 100644
237index 0000000000000000000000000000000000000000..6da66e3db03709439e1a8731116081dd5fe738bb
238--- /dev/null
239+++ b/pkg/ext/mime.go
240@@ -0,0 +1,24 @@
241+package ext
242+
243+import "net/http"
244+
245+type ContentType = string
246+
247+const (
248+	TextHTML ContentType = "text/html"
249+)
250+
251+func Html(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
252+	return func(w http.ResponseWriter, r *http.Request) {
253+		next(w, r)
254+	}
255+}
256+
257+func SetHTML(w http.ResponseWriter) {
258+	SetMIME(w, TextHTML)
259+
260+}
261+
262+func SetMIME(w http.ResponseWriter, mime ContentType) {
263+	w.Header().Add("Content-Type", mime)
264+}
265diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go
266index f3e74c747e433b33f1b68d13473ef5ab507b5b95..28cc99e1ffe8564b7d63b31a3452134a51dc33aa 100644
267--- a/pkg/handler/git/handler.go
268+++ b/pkg/handler/git/handler.go
269@@ -6,6 +6,7 @@ 	"log/slog"
270 	"net/http"
271 	"path/filepath"
272 
273+	"git.gabrielgio.me/cerrado/pkg/ext"
274 	"git.gabrielgio.me/cerrado/pkg/service"
275 	"git.gabrielgio.me/cerrado/templates"
276 	"github.com/alecthomas/chroma/v2"
277@@ -50,6 +51,7 @@ 	templates.WritePageTemplate(w, gitList)
278 }
279 
280 func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) {
281+	ext.SetHTML(w)
282 	name := r.PathValue("name")
283 	ref, err := g.gitService.GetHead(name)
284 	if err != nil {
285@@ -66,6 +68,7 @@ 	templates.WritePageTemplate(w, gitList)
286 }
287 
288 func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) {
289+	ext.SetHTML(w)
290 	name := r.PathValue("name")
291 	ref, err := g.gitService.GetHead(name)
292 	if err != nil {
293@@ -81,6 +84,7 @@ 	templates.WritePageTemplate(w, gitList)
294 }
295 
296 func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) {
297+	ext.SetHTML(w)
298 	name := r.PathValue("name")
299 
300 	tags, err := g.gitService.ListTags(name)
301@@ -113,6 +117,7 @@ 	templates.WritePageTemplate(w, gitList)
302 }
303 
304 func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) {
305+	ext.SetHTML(w)
306 	name := r.PathValue("name")
307 	ref := r.PathValue("ref")
308 	rest := r.PathValue("rest")
309@@ -137,6 +142,7 @@ 	templates.WritePageTemplate(w, gitList)
310 }
311 
312 func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) {
313+	ext.SetHTML(w)
314 	name := r.PathValue("name")
315 	ref := r.PathValue("ref")
316 	rest := r.PathValue("rest")
317@@ -178,6 +184,7 @@ 	templates.WritePageTemplate(w, gitList)
318 }
319 
320 func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) {
321+	ext.SetHTML(w)
322 	name := r.PathValue("name")
323 	ref := r.PathValue("ref")
324 
325diff --git a/pkg/handler/router.go b/pkg/handler/router.go
326index ed782f75d525c637e539a4e6c07795cc0d8ac8d2..de5117c9e0a6b8836117e40c1b237bdb1afa6346 100644
327--- a/pkg/handler/router.go
328+++ b/pkg/handler/router.go
329@@ -4,6 +4,7 @@ import (
330 	"net/http"
331 
332 	serverconfig "git.gabrielgio.me/cerrado/pkg/config"
333+	"git.gabrielgio.me/cerrado/pkg/ext"
334 	"git.gabrielgio.me/cerrado/pkg/handler/about"
335 	"git.gabrielgio.me/cerrado/pkg/handler/config"
336 	"git.gabrielgio.me/cerrado/pkg/handler/git"
337@@ -31,15 +32,19 @@ 	}
338 
339 	mux := http.NewServeMux()
340 
341-	mux.HandleFunc("/static/{file}", staticHandler)
342-	mux.HandleFunc("/{name}/about/{$}", gitHandler.About)
343-	mux.HandleFunc("/{name}", gitHandler.Summary)
344-	mux.HandleFunc("/{name}/refs/{$}", gitHandler.Refs)
345-	mux.HandleFunc("/{name}/tree/{ref}/{rest...}", gitHandler.Tree)
346-	mux.HandleFunc("/{name}/blob/{ref}/{rest...}", gitHandler.Blob)
347-	mux.HandleFunc("/{name}/log/{ref}", gitHandler.Log)
348-	mux.HandleFunc("/config", configHander)
349-	mux.HandleFunc("/about", aboutHandler.About)
350-	mux.HandleFunc("/", gitHandler.List)
351+	mux.HandleFunc("/static/{file}", m(staticHandler))
352+	mux.HandleFunc("/{name}/about/{$}", m(gitHandler.About))
353+	mux.HandleFunc("/{name}", m(gitHandler.Summary))
354+	mux.HandleFunc("/{name}/refs/{$}", m(gitHandler.Refs))
355+	mux.HandleFunc("/{name}/tree/{ref}/{rest...}", m(gitHandler.Tree))
356+	mux.HandleFunc("/{name}/blob/{ref}/{rest...}", m(gitHandler.Blob))
357+	mux.HandleFunc("/{name}/log/{ref}", m(gitHandler.Log))
358+	mux.HandleFunc("/config", m(configHander))
359+	mux.HandleFunc("/about", m(aboutHandler.About))
360+	mux.HandleFunc("/", m(gitHandler.List))
361 	return mux, nil
362 }
363+
364+func m(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
365+	return ext.Compress(next)
366+}
367diff --git a/pkg/handler/static/handler.go b/pkg/handler/static/handler.go
368index a8b458324580082c2424b21a65d92c2849e6a836..5155068306666b2303de7f129585108f32ea5dfa 100644
369--- a/pkg/handler/static/handler.go
370+++ b/pkg/handler/static/handler.go
371@@ -2,8 +2,11 @@ package static
372 
373 import (
374 	"io/fs"
375+	"mime"
376 	"net/http"
377+	"path/filepath"
378 
379+	"git.gabrielgio.me/cerrado/pkg/ext"
380 	"git.gabrielgio.me/cerrado/static"
381 )
382 
383@@ -14,8 +17,12 @@ 		return nil, err
384 	}
385 
386 	return func(w http.ResponseWriter, r *http.Request) {
387-		f := r.PathValue("file")
388-
389+		var (
390+			f = r.PathValue("file")
391+			e = filepath.Ext(f)
392+			m = mime.TypeByExtension(e)
393+		)
394+		ext.SetMIME(w, m)
395 		http.ServeFileFS(w, r, staticFs, f)
396 	}, nil
397 }
398diff --git a/pkg/u/list.go b/pkg/u/list.go
399index 34eafd11492cecb0eddfe8cc7ad4c4df8bf96511..cf71909e439d7726b4c6d9c6fcea771ad776a5bf 100644
400--- a/pkg/u/list.go
401+++ b/pkg/u/list.go
402@@ -8,6 +8,14 @@ 	}
403 	return v[0], true
404 }
405 
406+func FirstOrZero[T any](v []T) T {
407+	if len(v) == 0 {
408+		var zero T
409+		return zero
410+	}
411+	return v[0]
412+}
413+
414 func ChunkBy[T any](items []T, chunkSize int) [][]T {
415 	var chunks = make([][]T, 0, (len(items)/chunkSize)+1)
416 	for chunkSize < len(items) {
417diff --git a/pkg/u/list_test.go b/pkg/u/list_test.go
418index a6d84c7eaa6ceb401da5f22978f0f285ea1b7478..805a2091b59ce4c644f748b4b6c49a28aec50cdb 100644
419--- a/pkg/u/list_test.go
420+++ b/pkg/u/list_test.go
421@@ -94,3 +94,38 @@ 			}
422 		})
423 	}
424 }
425+
426+func TestFirstOrZero(t *testing.T) {
427+	testCases := []struct {
428+		name  string
429+		slice []int
430+		first int
431+	}{
432+		{
433+			name:  "multiple items slice",
434+			slice: []int{1, 2, 3},
435+			first: 1,
436+		},
437+		{
438+			name:  "single item slice",
439+			slice: []int{1},
440+			first: 1,
441+		},
442+		{
443+			name:  "empty slice",
444+			slice: []int{},
445+			first: 0,
446+		},
447+	}
448+	for _, tc := range testCases {
449+		t.Run(tc.name, func(t *testing.T) {
450+
451+			first := FirstOrZero(tc.slice)
452+
453+			if first != tc.first {
454+				t.Errorf("Error first, want %d got %d", tc.first, first)
455+			}
456+
457+		})
458+	}
459+}
460diff --git a/templates/base.qtpl b/templates/base.qtpl
461index 180b1ab428bc964511451c36569b198be2f5d3ee..497aa6d59d5fa49671e8ad4da524712bf62c40e8 100644
462--- a/templates/base.qtpl
463+++ b/templates/base.qtpl
464@@ -37,12 +37,14 @@ %}
465 
466 Page prints a page implementing Page interface.
467 {% func PageTemplate(p Page) %}
468+<!DOCTYPE html>
469 <html lang="en">
470     <head>
471         <meta charset="utf-8">
472         <link rel="icon" href="data:,">
473         <title>cerrado | {%= p.Title() %}</title> 
474         <link rel="stylesheet" href="/static/main{%s Slug%}.css">
475+        <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
476         <meta name="viewport" content="width=device-width, initial-scale=1" />
477     </head>
478     <body>
479diff --git a/templates/base.qtpl.go b/templates/base.qtpl.go
480index c5570c8ee4119c1861d86e15aec1f7e24a4a35a9..5f39e8dd4040b5dbe896fc5c44dc5b423636f9eb 100644
481--- a/templates/base.qtpl.go
482+++ b/templates/base.qtpl.go
483@@ -82,168 +82,170 @@ //line base.qtpl:39
484 func StreamPageTemplate(qw422016 *qt422016.Writer, p Page) {
485 //line base.qtpl:39
486 	qw422016.N().S(`
487+<!DOCTYPE html>
488 <html lang="en">
489     <head>
490         <meta charset="utf-8">
491         <link rel="icon" href="data:,">
492         <title>cerrado | `)
493-//line base.qtpl:44
494+//line base.qtpl:45
495 	p.StreamTitle(qw422016)
496-//line base.qtpl:44
497+//line base.qtpl:45
498 	qw422016.N().S(`</title> 
499         <link rel="stylesheet" href="/static/main`)
500-//line base.qtpl:45
501+//line base.qtpl:46
502 	qw422016.E().S(Slug)
503-//line base.qtpl:45
504+//line base.qtpl:46
505 	qw422016.N().S(`.css">
506+        <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
507         <meta name="viewport" content="width=device-width, initial-scale=1" />
508     </head>
509     <body>
510         `)
511-//line base.qtpl:49
512+//line base.qtpl:51
513 	p.StreamNavbar(qw422016)
514-//line base.qtpl:49
515+//line base.qtpl:51
516 	qw422016.N().S(`
517         <div class="container">
518             `)
519-//line base.qtpl:51
520+//line base.qtpl:53
521 	p.StreamContent(qw422016)
522-//line base.qtpl:51
523+//line base.qtpl:53
524 	qw422016.N().S(`
525         </div>
526     </body>
527     `)
528-//line base.qtpl:54
529+//line base.qtpl:56
530 	p.StreamScript(qw422016)
531-//line base.qtpl:54
532+//line base.qtpl:56
533 	qw422016.N().S(`
534 </html>
535 `)
536-//line base.qtpl:56
537+//line base.qtpl:58
538 }
539 
540-//line base.qtpl:56
541+//line base.qtpl:58
542 func WritePageTemplate(qq422016 qtio422016.Writer, p Page) {
543-//line base.qtpl:56
544+//line base.qtpl:58
545 	qw422016 := qt422016.AcquireWriter(qq422016)
546-//line base.qtpl:56
547+//line base.qtpl:58
548 	StreamPageTemplate(qw422016, p)
549-//line base.qtpl:56
550+//line base.qtpl:58
551 	qt422016.ReleaseWriter(qw422016)
552-//line base.qtpl:56
553+//line base.qtpl:58
554 }
555 
556-//line base.qtpl:56
557+//line base.qtpl:58
558 func PageTemplate(p Page) string {
559-//line base.qtpl:56
560+//line base.qtpl:58
561 	qb422016 := qt422016.AcquireByteBuffer()
562-//line base.qtpl:56
563+//line base.qtpl:58
564 	WritePageTemplate(qb422016, p)
565-//line base.qtpl:56
566+//line base.qtpl:58
567 	qs422016 := string(qb422016.B)
568-//line base.qtpl:56
569+//line base.qtpl:58
570 	qt422016.ReleaseByteBuffer(qb422016)
571-//line base.qtpl:56
572+//line base.qtpl:58
573 	return qs422016
574-//line base.qtpl:56
575+//line base.qtpl:58
576 }
577 
578-//line base.qtpl:58
579+//line base.qtpl:60
580 type BasePage struct{}
581 
582-//line base.qtpl:59
583+//line base.qtpl:61
584 func (p *BasePage) StreamTitle(qw422016 *qt422016.Writer) {
585-//line base.qtpl:59
586+//line base.qtpl:61
587 	qw422016.N().S(`Empty`)
588-//line base.qtpl:59
589+//line base.qtpl:61
590 }
591 
592-//line base.qtpl:59
593+//line base.qtpl:61
594 func (p *BasePage) WriteTitle(qq422016 qtio422016.Writer) {
595-//line base.qtpl:59
596+//line base.qtpl:61
597 	qw422016 := qt422016.AcquireWriter(qq422016)
598-//line base.qtpl:59
599+//line base.qtpl:61
600 	p.StreamTitle(qw422016)
601-//line base.qtpl:59
602+//line base.qtpl:61
603 	qt422016.ReleaseWriter(qw422016)
604-//line base.qtpl:59
605+//line base.qtpl:61
606 }
607 
608-//line base.qtpl:59
609+//line base.qtpl:61
610 func (p *BasePage) Title() string {
611-//line base.qtpl:59
612+//line base.qtpl:61
613 	qb422016 := qt422016.AcquireByteBuffer()
614-//line base.qtpl:59
615+//line base.qtpl:61
616 	p.WriteTitle(qb422016)
617-//line base.qtpl:59
618+//line base.qtpl:61
619 	qs422016 := string(qb422016.B)
620-//line base.qtpl:59
621+//line base.qtpl:61
622 	qt422016.ReleaseByteBuffer(qb422016)
623-//line base.qtpl:59
624+//line base.qtpl:61
625 	return qs422016
626-//line base.qtpl:59
627+//line base.qtpl:61
628 }
629 
630-//line base.qtpl:60
631+//line base.qtpl:62
632 func (p *BasePage) StreamBody(qw422016 *qt422016.Writer) {
633-//line base.qtpl:60
634+//line base.qtpl:62
635 	qw422016.N().S(`HelloWorld`)
636-//line base.qtpl:60
637+//line base.qtpl:62
638 }
639 
640-//line base.qtpl:60
641+//line base.qtpl:62
642 func (p *BasePage) WriteBody(qq422016 qtio422016.Writer) {
643-//line base.qtpl:60
644+//line base.qtpl:62
645 	qw422016 := qt422016.AcquireWriter(qq422016)
646-//line base.qtpl:60
647+//line base.qtpl:62
648 	p.StreamBody(qw422016)
649-//line base.qtpl:60
650+//line base.qtpl:62
651 	qt422016.ReleaseWriter(qw422016)
652-//line base.qtpl:60
653+//line base.qtpl:62
654 }
655 
656-//line base.qtpl:60
657+//line base.qtpl:62
658 func (p *BasePage) Body() string {
659-//line base.qtpl:60
660+//line base.qtpl:62
661 	qb422016 := qt422016.AcquireByteBuffer()
662-//line base.qtpl:60
663+//line base.qtpl:62
664 	p.WriteBody(qb422016)
665-//line base.qtpl:60
666+//line base.qtpl:62
667 	qs422016 := string(qb422016.B)
668-//line base.qtpl:60
669+//line base.qtpl:62
670 	qt422016.ReleaseByteBuffer(qb422016)
671-//line base.qtpl:60
672+//line base.qtpl:62
673 	return qs422016
674-//line base.qtpl:60
675+//line base.qtpl:62
676 }
677 
678-//line base.qtpl:61
679+//line base.qtpl:63
680 func (p *BasePage) StreamScript(qw422016 *qt422016.Writer) {
681-//line base.qtpl:61
682+//line base.qtpl:63
683 }
684 
685-//line base.qtpl:61
686+//line base.qtpl:63
687 func (p *BasePage) WriteScript(qq422016 qtio422016.Writer) {
688-//line base.qtpl:61
689+//line base.qtpl:63
690 	qw422016 := qt422016.AcquireWriter(qq422016)
691-//line base.qtpl:61
692+//line base.qtpl:63
693 	p.StreamScript(qw422016)
694-//line base.qtpl:61
695+//line base.qtpl:63
696 	qt422016.ReleaseWriter(qw422016)
697-//line base.qtpl:61
698+//line base.qtpl:63
699 }
700 
701-//line base.qtpl:61
702+//line base.qtpl:63
703 func (p *BasePage) Script() string {
704-//line base.qtpl:61
705+//line base.qtpl:63
706 	qb422016 := qt422016.AcquireByteBuffer()
707-//line base.qtpl:61
708+//line base.qtpl:63
709 	p.WriteScript(qb422016)
710-//line base.qtpl:61
711+//line base.qtpl:63
712 	qs422016 := string(qb422016.B)
713-//line base.qtpl:61
714+//line base.qtpl:63
715 	qt422016.ReleaseByteBuffer(qb422016)
716-//line base.qtpl:61
717+//line base.qtpl:63
718 	return qs422016
719-//line base.qtpl:61
720+//line base.qtpl:63
721 }