kpaste

git clone git://git.lin.moe/go/kpaste.git

  1package main
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"html/template"
  7	"io"
  8	"log/slog"
  9	"net/http"
 10	"strings"
 11
 12	"github.com/niklasfasching/go-org/org"
 13	"github.com/yuin/goldmark"
 14	"github.com/yuin/goldmark/extension"
 15	"github.com/yuin/goldmark/parser"
 16	"github.com/yuin/goldmark/renderer/html"
 17)
 18
 19type Render struct {
 20	mdRender  goldmark.Markdown
 21	orgRender *org.Configuration
 22}
 23
 24func NewRender() *Render {
 25	mdconv := goldmark.New(
 26		goldmark.WithExtensions(extension.GFM),
 27		goldmark.WithParserOptions(
 28			parser.WithAutoHeadingID(),
 29		),
 30		goldmark.WithRendererOptions(
 31			html.WithHardWraps(),
 32			html.WithXHTML(),
 33		),
 34	)
 35
 36	orgconv := org.New()
 37	orgconv.ReadFile = func(string) ([]byte, error) {
 38		return nil, fmt.Errorf("include is not allowed")
 39	}
 40
 41	return &Render{
 42		mdRender:  mdconv,
 43		orgRender: orgconv,
 44	}
 45
 46}
 47
 48const NOTE_TEMPLATE = `
 49<!DOCTYPE html>
 50<html>
 51<head>
 52  <meta charset="UTF-8">
 53  <meta NAME="robots" CONTENT="noindex,nofollow">
 54  <meta http-equiv="X-UA-Compatible" content="IE=edge">
 55  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 56  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🫓</text></svg>">
 57  <link rel="stylesheet" type="text/css" href="/-/css/orgmode.min.css">
 58  <title>{{ .Note.Filename }} - kpaste</title>
 59</head>
 60<script>
 61  if (window.location.pathname == "/") {
 62    history.replaceState(null, "", "/{{.Key}}");
 63  }
 64</script>
 65<body>
 66<header>
 67<small>
 68  Create: <strong>{{ .Note.CreateAt.Format "2006-01-02 15:04"}}</strong>.
 69  {{ if not .Note.ExpireAt.IsZero }}Expire: <strong>{{ .Note.ExpireAt.Format "2006-01-02 15:04" }}</strong>. {{end}}
 70  Have been read: {{ if ne .Note.MaxRead -1 }}
 71    <strong {{if ge .Note.Readed .Note.MaxRead }}style="color:red"{{end}}>{{ .Note.Readed }}/{{.Note.MaxRead}}</strong>
 72  {{else}}
 73    <strong>{{ .Note.Readed }}</strong>
 74  {{end}} times.
 75  <a href="/!/{{.Key}}">Raw</a>
 76</small>
 77</header>
 78{{ if eq .Req.Method "POST"}}
 79<noscript><p class="box">Created link: <a href="/{{.Key}}">{{.Req.Host}}/{{.Key}}</a></p></noscript>
 80{{end}}
 81  <main>
 82    {{ .Note.Payload | htmlize }}
 83  </main>
 84</body>
 85</html>
 86`
 87
 88const INDEX_TEMPLATE = `
 89<!DOCTYPE html>
 90<html>
 91  <head>
 92    <meta charset="UTF-8">
 93    <meta NAME="robots" CONTENT="noindex,nofollow">
 94    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 95    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 96    <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🫓</text></svg>">
 97    <link rel="stylesheet" type="text/css" href="/-/css/orgmode.min.css">
 98    <title>kpaste</title>
 99    <style>
100    .tab-folder > .tab-content:target ~ .tab-content:last-child, .tab-folder > .tab-content {
101        display: none;
102    }
103    .tab-folder > :last-child, .tab-folder > .tab-content:target {
104        display: block;
105    }
106</style>
107  </head>
108  <body>
109    <main>
110      <header><nav>
111      <a href="#text"> text </a>
112      <a href="#file"> file </a>
113      </nav></header>
114      <div class="tab-folder">
115      <div id="file" class="tab-content"><form action="/" method="post" enctype="multipart/form-data">
116        <input name="f" type="file">
117	<p>
118	  <label for="read"> Max Red Times <a title="-1 meaning unlimit" href="#">?</a></label>
119	  <input type="number" name="read" min="-1" value="-1" title="demo">
120	</p>
121	<p>
122 	  <label for="expire">Expire Days </label>
123	  <input type="number" name="expire" min="-1" value="30">
124	</p>
125	<button>Submit</button>
126        <button type="reset">Reset</button>
127      </form></div>
128
129      <div id="text" class="tab-content"><form action="/" method="post" enctype="multipart/form-data">
130        <textarea name="text" rows="30" style="font-family: monospace" ></textarea>
131	<p>
132	  <label for="read"> Max Red Times <a title="-1 meaning unlimit" href="#">?</a></label>
133	  <input type="number" name="read" min="-1" value="-1" title="demo">
134	</p>
135	<p>
136 	  <label for="expire">Expire Days </label>
137	  <input type="number" name="expire" min="-1" value="30">
138	</p>
139	<button>Submit</button>
140        <button type="reset">Reset</button>
141      </form></div>
142      </div>
143    </main>
144    {{if ne .MOTD ""}}
145    <section><object id="motd" style="height: 30rem;width: 100%;" data="/!/{{.MOTD}}"></object></section>
146    {{end}}
147    <footer>
148      <a href="https://git.lin.moe/go/kpaste">Source Code</a>
149    </footer>
150  </body>
151</html>
152`
153
154var notetmpl = template.Must(template.New("").Funcs(template.FuncMap{
155	"htmlize": func(raw []byte) template.HTML {
156		return template.HTML(raw)
157	},
158}).Parse(NOTE_TEMPLATE))
159
160var indextmpl = template.Must(template.New("").Parse(INDEX_TEMPLATE))
161
162func (r *Render) Render(w http.ResponseWriter, req *http.Request, key string, note Note) {
163	var (
164		b   bytes.Buffer
165		err error
166	)
167
168	if strings.HasPrefix(note.MimeType, "text/markdown") {
169		var out bytes.Buffer
170		err = r.mdRender.Convert(note.Payload, &out)
171		if err == nil {
172			note.Payload = out.Bytes()
173			err = notetmpl.Execute(&b, struct {
174				Key  string
175				Note *Note
176				Req  *http.Request
177			}{key, &note, req})
178		}
179
180	} else if strings.HasPrefix(note.MimeType, "text/org") {
181		var out string
182		writer := org.NewHTMLWriter()
183		out, err = r.orgRender.Parse(bytes.NewBuffer(note.Payload), "").Write(writer)
184		if err == nil {
185			note.Payload = []byte(out)
186			err = notetmpl.Execute(&b, struct {
187				Key  string
188				Note *Note
189				Req  *http.Request
190			}{key, &note, req})
191		}
192	} else if strings.HasPrefix(note.MimeType, "text/") {
193		var escaped, out bytes.Buffer
194		template.HTMLEscape(&escaped, note.Payload)
195		_, err = fmt.Fprintf(&out, "<pre class=\"plaintext\">%s</pre>\n", escaped.String())
196		if err == nil {
197			note.Payload = out.Bytes()
198			err = notetmpl.Execute(&b, struct {
199				Key  string
200				Note *Note
201				Req  *http.Request
202			}{key, &note, req})
203		}
204	} else {
205		_, err = b.Write(note.Payload)
206	}
207
208	if err != nil {
209		w.WriteHeader(http.StatusInternalServerError)
210		slog.Warn(err.Error())
211		w.Write([]byte("note render failed\n"))
212		return
213	} else {
214		w.Header().Set("Content-Disposition", fmt.Sprintf("filename=%q", note.Filename))
215		w.WriteHeader(http.StatusOK)
216		io.Copy(w, &b)
217	}
218}
219
220func (r *Render) Index(w http.ResponseWriter, motdkey string) {
221	var buf bytes.Buffer
222	slog.Debug(motdkey)
223	if err := indextmpl.Execute(&buf, struct {
224		MOTD string
225	}{motdkey}); err != nil {
226		w.WriteHeader(http.StatusInternalServerError)
227		w.Write([]byte(err.Error()))
228		return
229	}
230	w.WriteHeader(http.StatusOK)
231	io.Copy(w, &buf)
232}