1package main23import (4 "bytes"5 "fmt"6 "html/template"7 "io"8 "log/slog"9 "net/http"10 "strings"1112 "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)1819type Render struct {20 mdRender goldmark.Markdown21 orgRender *org.Configuration22}2324func 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 )3536 orgconv := org.New()37 orgconv.ReadFile = func(string) ([]byte, error) {38 return nil, fmt.Errorf("include is not allowed")39 }4041 return &Render{42 mdRender: mdconv,43 orgRender: orgconv,44 }4546}4748const 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`8788const 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>128129 <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`153154var 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))159160var indextmpl = template.Must(template.New("").Parse(INDEX_TEMPLATE))161162func (r *Render) Render(w http.ResponseWriter, req *http.Request, key string, note Note) {163 var (164 b bytes.Buffer165 err error166 )167168 if strings.HasPrefix(note.MimeType, "text/markdown") {169 var out bytes.Buffer170 err = r.mdRender.Convert(note.Payload, &out)171 if err == nil {172 note.Payload = out.Bytes()173 err = notetmpl.Execute(&b, struct {174 Key string175 Note *Note176 Req *http.Request177 }{key, ¬e, req})178 }179180 } else if strings.HasPrefix(note.MimeType, "text/org") {181 var out string182 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 string188 Note *Note189 Req *http.Request190 }{key, ¬e, req})191 }192 } else if strings.HasPrefix(note.MimeType, "text/") {193 var escaped, out bytes.Buffer194 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 string200 Note *Note201 Req *http.Request202 }{key, ¬e, req})203 }204 } else {205 _, err = b.Write(note.Payload)206 }207208 if err != nil {209 w.WriteHeader(http.StatusInternalServerError)210 slog.Warn(err.Error())211 w.Write([]byte("note render failed\n"))212 return213 } 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}219220func (r *Render) Index(w http.ResponseWriter, motdkey string) {221 var buf bytes.Buffer222 slog.Debug(motdkey)223 if err := indextmpl.Execute(&buf, struct {224 MOTD string225 }{motdkey}); err != nil {226 w.WriteHeader(http.StatusInternalServerError)227 w.Write([]byte(err.Error()))228 return229 }230 w.WriteHeader(http.StatusOK)231 io.Copy(w, &buf)232}