summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--go.mod13
-rw-r--r--go.sum61
-rw-r--r--http/get.go36
-rw-r--r--http/index.go12
-rw-r--r--http/put.go50
-rw-r--r--http/router.go23
-rw-r--r--http/type.go5
-rw-r--r--http/ua_detector.go23
-rw-r--r--main.go45
-rw-r--r--storage/redis.go40
-rw-r--r--storage/type.go20
-rw-r--r--template/code.qtpl28
-rw-r--r--template/index.qtpl58
-rw-r--r--template/layout.qtpl49
15 files changed, 466 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index a1c573e..23e9582 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
/doc/brpaste.1
/brpaste
+
+# must be added manually for releases
+/template/*.go
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1cd1ad7
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,13 @@
+module toast.cafe/x/brpaste/v2
+
+go 1.13
+
+require (
+ github.com/fasthttp/router v0.5.2
+ github.com/go-redis/redis/v7 v7.0.0-beta.4
+ github.com/golang/protobuf v1.3.1 // indirect
+ github.com/twmb/murmur3 v1.0.0
+ github.com/valyala/fasthttp v1.6.0
+ github.com/valyala/quicktemplate v1.4.1
+ golang.org/x/text v0.3.2 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..0fc0fac
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,61 @@
+github.com/fasthttp/router v0.5.2 h1:xdmx8uYc9IFDtlbG2/FhE1Gyowv7/sqMgMonRjoW0Yo=
+github.com/fasthttp/router v0.5.2/go.mod h1:Y5JAeRTSPwSLoUgH4x75UnT1j1IcAgVshMDMMrnNmKQ=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/go-redis/redis/v7 v7.0.0-beta.4 h1:p6z7Pde69EGRWvlC++y8aFcaWegyrKHzOBGo0zUACTQ=
+github.com/go-redis/redis/v7 v7.0.0-beta.4/go.mod h1:xhhSbUMTsleRPur+Vgx9sUHtyN33bdjxY+9/0n9Ig8s=
+github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs=
+github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
+github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500 h1:9Pi10H7E8E79/x2HSe1FmMGd7BJ1WAqDKzwjpv+ojFg=
+github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY=
+github.com/twmb/murmur3 v1.0.0 h1:MLMwMEQRKsu94uJnoveYjjHmcLwI3HNcWXP4LJuNe3I=
+github.com/twmb/murmur3 v1.0.0/go.mod h1:5Y5m8Y8WIyucaICVP+Aep5C8ydggjEuRQHDq1icoOYo=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
+github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY=
+github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
+github.com/valyala/quicktemplate v1.4.1 h1:tEtkSN6mTCJlYVT7As5x4wjtkk2hj2thsb0M+AcAVeM=
+github.com/valyala/quicktemplate v1.4.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/http/get.go b/http/get.go
new file mode 100644
index 0000000..459d18f
--- /dev/null
+++ b/http/get.go
@@ -0,0 +1,36 @@
+package http
+
+import (
+ "github.com/valyala/fasthttp"
+ "toast.cafe/x/brpaste/v2/storage"
+ "toast.cafe/x/brpaste/v2/template"
+)
+
+func Get(store storage.CHR) handler {
+ return func(ctx *fasthttp.RequestCtx) {
+ ukey := ctx.UserValue("key")
+ ulang := ctx.UserValue("lang")
+
+ var key, lang string
+ key = ukey.(string) // there's no recovering otherwise
+ if ulang != nil {
+ lang = ulang.(string)
+ }
+
+ res, err := store.Read(key)
+ switch err {
+ case storage.Unhealthy:
+ ctx.Error("Backend did not respond", fasthttp.StatusInternalServerError)
+ case nil: // all good
+ if lang == "raw" {
+ ctx.SuccessString("text/plain", res)
+ } else {
+ //b := new(bytes.Buffer)
+ //template.WriteCode(b, lang, res)
+ ctx.SuccessString("text/html", template.Code(lang, res)) // render template
+ }
+ default:
+ ctx.NotFound()
+ }
+ }
+}
diff --git a/http/index.go b/http/index.go
new file mode 100644
index 0000000..4bb9567
--- /dev/null
+++ b/http/index.go
@@ -0,0 +1,12 @@
+package http
+
+import (
+ "github.com/valyala/fasthttp"
+ "toast.cafe/x/brpaste/v2/template"
+)
+
+func Index(ctx *fasthttp.RequestCtx) {
+ //b := new(bytes.Buffer)
+ //template.Index(b)
+ ctx.SuccessString("text/html", template.Index()) // render template
+}
diff --git a/http/put.go b/http/put.go
new file mode 100644
index 0000000..bc49e74
--- /dev/null
+++ b/http/put.go
@@ -0,0 +1,50 @@
+package http
+
+import (
+ "encoding/base64"
+ "fmt"
+
+ "github.com/twmb/murmur3"
+ "github.com/valyala/fasthttp"
+ "toast.cafe/x/brpaste/v2/storage"
+)
+
+func Put(store storage.CHR, put bool) handler {
+ return func(ctx *fasthttp.RequestCtx) {
+ data := ctx.FormValue("data")
+ if len(data) == 0 { // works with nil
+ ctx.Error("Missing data field", fasthttp.StatusBadRequest)
+ return
+ }
+
+ ukey := ctx.UserValue("key")
+ var key string
+ if ukey != nil {
+ key = ukey.(string)
+ } else {
+ hasher := murmur3.New32()
+ hasher.Write(data)
+ keybuf := hasher.Sum(nil)
+ key = base64.RawURLEncoding.EncodeToString(keybuf)
+ }
+ val := string(data)
+
+ err := store.Create(key, val, put)
+
+ switch err {
+ case storage.Collision:
+ ctx.Error("Collision detected when undesired", fasthttp.StatusConflict)
+ case storage.Unhealthy:
+ ctx.Error("Backend did not respond", fasthttp.StatusInternalServerError)
+ case nil: // everything succeeded
+ if isBrowser(string(ctx.UserAgent())) {
+ ctx.Redirect(fmt.Sprintf("/%s", key), fasthttp.StatusSeeOther)
+ } else {
+ ctx.SetStatusCode(fasthttp.StatusCreated)
+ ctx.SetBodyString(key)
+ }
+ default:
+ ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
+ }
+ }
+}
diff --git a/http/router.go b/http/router.go
new file mode 100644
index 0000000..f1a0b47
--- /dev/null
+++ b/http/router.go
@@ -0,0 +1,23 @@
+package http
+
+import (
+ "github.com/fasthttp/router"
+ "github.com/valyala/fasthttp"
+ "toast.cafe/x/brpaste/v2/storage"
+)
+
+// GenHandler generates the brpaste handler
+func GenHandler(store storage.CHR) func(ctx *fasthttp.RequestCtx) {
+ get := Get(store)
+ post := Put(store, false)
+ put := Put(store, true)
+
+ r := router.New()
+ r.GET("/", Index)
+ r.GET("/:key", get)
+ r.GET("/:key/:lang", get)
+ r.POST("/", post)
+ r.PUT("/:key", put)
+
+ return r.Handler
+}
diff --git a/http/type.go b/http/type.go
new file mode 100644
index 0000000..22aab0d
--- /dev/null
+++ b/http/type.go
@@ -0,0 +1,5 @@
+package http
+
+import "github.com/valyala/fasthttp"
+
+type handler = fasthttp.RequestHandler
diff --git a/http/ua_detector.go b/http/ua_detector.go
new file mode 100644
index 0000000..aa25025
--- /dev/null
+++ b/http/ua_detector.go
@@ -0,0 +1,23 @@
+package http
+
+import "strings"
+
+var (
+ browsers = []string{
+ "Firefox/",
+ "Chrome/",
+ "Safari/",
+ "OPR/",
+ "Edge/",
+ "Trident/",
+ }
+)
+
+func isBrowser(ua string) bool {
+ for _, el := range browsers {
+ if strings.Contains(ua, el) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..adce93e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "github.com/go-redis/redis/v7"
+ "github.com/valyala/fasthttp"
+ "toast.cafe/x/brpaste/v2/http"
+ "toast.cafe/x/brpaste/v2/storage"
+)
+
+var S settings
+
+type settings struct {
+ Bind string
+ Redis string
+}
+
+func main() {
+ // ---- Flags
+ flag.StringVar(&S.Bind, "bind", ":8080", "address to bind to")
+ flag.StringVar(&S.Redis, "redis", "redis://localhost:6379", "redis connection string")
+ flag.Parse()
+
+ // ---- Storage system
+ redisOpts, err := redis.ParseURL(S.Redis)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not parse redis connection string %s\n", S.Redis)
+ os.Exit(1)
+ }
+ client := redis.NewClient(redisOpts)
+ storage := (*storage.Redis)(client)
+
+ // ---- Is storage healthy?
+ if !storage.Healthy() {
+ fmt.Fprintf(os.Stderr, "Storage is unhealthy, cannot proceed.\n")
+ os.Exit(1)
+ }
+
+ // ---- Start!
+ handler := http.GenHandler(storage)
+ fasthttp.ListenAndServe(S.Bind, handler)
+}
diff --git a/storage/redis.go b/storage/redis.go
new file mode 100644
index 0000000..865c0bb
--- /dev/null
+++ b/storage/redis.go
@@ -0,0 +1,40 @@
+package storage
+
+import "github.com/go-redis/redis/v7"
+
+// Redis storage engine
+type Redis redis.Client
+
+// Create an entry in redis
+func (r *Redis) Create(key, value string, checkcollision bool) error {
+ if !r.Healthy() {
+ return Unhealthy
+ }
+ if checkcollision {
+ col, err := r.Exists(key).Result()
+ if err != nil {
+ return Unhealthy
+ }
+ if col > 0 {
+ return Collision
+ }
+ }
+ _, err := r.Set(key, value, 0).Result()
+ return err
+}
+
+func (r *Redis) Read(key string) (string, error) {
+ if !r.Healthy() {
+ return "", Unhealthy
+ }
+ return r.Get(key).Result()
+}
+
+// Healthy determines whether redis is responding to pings
+func (r *Redis) Healthy() bool {
+ _, err := r.Ping().Result()
+ if err != nil {
+ return false
+ }
+ return true
+}
diff --git a/storage/type.go b/storage/type.go
new file mode 100644
index 0000000..4f78a46
--- /dev/null
+++ b/storage/type.go
@@ -0,0 +1,20 @@
+package storage
+
+const (
+ // Collision is a fatal collision error, only triggered when it fails
+ Collision = Error("collision detected")
+ // Unhealthy is a fatal error when the backend ceases to be healthy
+ Unhealthy = Error("backend unhealthy")
+)
+
+// Error is a sentinel error type for storage engines
+type Error string
+
+func (e Error) Error() string { return string(e) }
+
+// CHR - Create, Health, Read
+type CHR interface {
+ Create(key, value string, checkcollision bool) error
+ Healthy() bool
+ Read(key string) (string, error)
+}
diff --git a/template/code.qtpl b/template/code.qtpl
new file mode 100644
index 0000000..426da8e
--- /dev/null
+++ b/template/code.qtpl
@@ -0,0 +1,28 @@
+The code layout.
+{% func Code(lang, data string) %}
+ {%= layout(" ", code_scripts(lang), "", code_contents(lang, data), code_bodyscripts(lang)) %}
+{% endfunc %}
+
+{% code
+ const prefix = "https://unpkg.com/prismjs"
+%}
+
+The code scripts.
+{% func code_scripts(lang string) %}
+ <link rel='stylesheet' crossorigin='anonymous' href='{%s prefix %}/themes/prism.css' />
+{% endfunc %}
+
+The code bodyscripts.
+{% func code_bodyscripts(lang string) %}
+ {% stripspace %}
+ <script src='{%s prefix %}/prism.js'></script>
+ {% if lang != "" && lang != "none" %}
+ <script src='{%s prefix %}/components/prism-{%s lang %}.js'></script>
+ {% endif %}
+ {% endstripspace %}
+{% endfunc %}
+
+The code contents.
+{% func code_contents(lang, data string) %}
+ <pre><code class='language-{%s lang %}'>{%s data %}</code></pre>
+{% endfunc %}
diff --git a/template/index.qtpl b/template/index.qtpl
new file mode 100644
index 0000000..b3b9721
--- /dev/null
+++ b/template/index.qtpl
@@ -0,0 +1,58 @@
+Index's contents.
+{% func index_contents() %}
+ {% stripspace %}
+ <h1>Burning Rubber Paste</h1>
+ <h2>Usage</h2>
+ <table border=1>
+ <thead>
+ <tr>
+ <th>Method - Endpoint</th>
+ <th>Effect</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><pre><code>POST / data=foo</code></pre></td>
+ <td>Pastebin foo</td>
+ </tr>
+ <tr>
+ <td><pre><code>PUT /id data=foo</code></pre></td>
+ <td>Write foo into /id. Collisions disallowed. If a POST id coincides with your PUT content, it will be overwritten.</td>
+ </tr>
+ <tr>
+ <td><pre><code>GET /id</code></pre></td>
+ <td>Read paste with ID "id"</td>
+ </tr>
+ <tr>
+ <td><pre><code>GET /id/raw</code></pre></td>
+ <td>Get the raw contents of paste with ID "id"</td>
+ </tr>
+ <tr>
+ <td><pre><code>GET /id/lang</code></pre></td>
+ <td>Read paste with ID "id", and highlight it as "lang"</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h2>Examples</h2>
+ {% endstripspace %}
+ <pre><code class='language-sh'>http -f https://brpaste.example.com data=@file.txt
+http -f https://brpaste.example.com data=abcd
+http -f PUT https://brpaste.example.com/myPaste data=contents
+http https://brpaste.example.com/some_id/raw
+xdg-open https://brpaste.example.com/some_id/cpp</code></pre>
+ {% stripspace %}
+
+ <h2>Paste from a browser</h2>
+ <form action='/' method='post'>
+ <textarea name='data' autocomplete='off' required autofocus cols='80' rows='27'></textarea>
+ <br>
+ <button type='submit'>Paste it!</button>
+ </form>
+ {% endstripspace %}
+{% endfunc %}
+
+The index layout.
+{% func Index() %}
+ {%= layout("", "", "", index_contents(), "") %}
+{% endfunc %}
diff --git a/template/layout.qtpl b/template/layout.qtpl
new file mode 100644
index 0000000..ffb31d2
--- /dev/null
+++ b/template/layout.qtpl
@@ -0,0 +1,49 @@
+The main layout function.
+{% func layout(css, scripts, title, contents, bodyscripts string) %}
+ <!DOCTYPE html>
+ {% stripspace %}
+ <html lang='en'>
+ <head>
+ <meta charset='utf-8' />
+ <meta name='viewport' content='width=device-width, initial-scale=1' />
+
+ {% if len(css) == 0 %}
+ <style>
+ body {
+ margin: 40px auto;
+ max-width: 650px;
+ line-height: 1.6;
+ font-size: 18px;
+ color: #444;
+ padding: 0 10px;
+ }
+ h1, h2, h3 { line-length: 1.2; }
+ td { text-align: left; }
+ </style>
+ {% else %}
+ {%s= css %}
+ {% endif %}
+
+ {% if len(scripts) != 0 %}
+ {%s= scripts %}
+ {% endif %}
+
+ {% if len(title) == 0 %}
+ <title>Burning Rubber Paste</title>
+ {% else %}
+ {%s= title %}
+ {% endif %}
+ </head>
+ <body>
+ <div id='main'>
+ {% if len(contents) != 0 %}
+ {%s= contents %}
+ {% endif %}
+ </div>
+ {% if len(bodyscripts) != 0 %}
+ {%s= bodyscripts %}
+ {% endif %}
+ </body>
+ </html>
+ {% endstripspace %}
+{% endfunc %}