Improve whitespace handling in result presentation
Update on top of rebase, make template arguments explicit, support dynamic template reloads
Styling improvements
---
backend.rkt | 14 +-
frontends/discord.rkt | 13 +-
frontends/http.rkt | 321 ------------------------------
frontends/http/base.html | 43 ++++
frontends/http/favicon.ico | Bin 0 -> 67646 bytes
frontends/http/home.html | 5 +
frontends/http/icon.svg | 194 ++++++++++++++++++
frontends/http/main.rkt | 265 ++++++++++++++++++++++++
frontends/http/raw.html | 23 +++
frontends/http/result.html | 40 ++++
frontends/http/simple.html | 4 +
frontends/http/style-in.css | 20 ++
frontends/http/style.css | 1 +
frontends/http/tailwind.config.js | 5 +
frontends/http/tricks.html | 109 ++++++++++
interface.rkt | 27 ++-
result.rkt | 16 +-
17 files changed, 759 insertions(+), 341 deletions(-)
delete mode 100644 frontends/http.rkt
create mode 100644 frontends/http/base.html
create mode 100644 frontends/http/favicon.ico
create mode 100644 frontends/http/home.html
create mode 100644 frontends/http/icon.svg
create mode 100644 frontends/http/main.rkt
create mode 100644 frontends/http/raw.html
create mode 100644 frontends/http/result.html
create mode 100644 frontends/http/simple.html
create mode 100644 frontends/http/style-in.css
create mode 100644 frontends/http/style.css
create mode 100644 frontends/http/tailwind.config.js
create mode 100644 frontends/http/tricks.html
diff --git a/backend.rkt b/backend.rkt
index 0e67637..b8104f9 100644
--- a/backend.rkt
@@ -91,7 +91,7 @@
(define code (trick-body trick-obj))
(cons 'ok (ev:run code ev-ctx response?))]
[else
- (cons 'err (~a "Trick " name " doesn't exist!"))]))
+ (list 'err (~a "Trick " name " doesn't exist!") 'no-such-trick)]))
(define/public (delete name)
(define ctx-id (current-context-id))
@@ -99,18 +99,18 @@
(define frontend (current-frontend))
(cond
[(not trick-obj)
- (cons 'err (~a "Trick " name " doesn't exist!"))]
+ (list 'err (~a "Trick " name " doesn't exist!") 'no-such-trick)]
[(db:remove-trick!
db ctx-id name
(lambda (t) (send frontend can-modify? t)))
(cons 'ok (~a "Successfully removed trick " name "!"))]
[else
- (cons 'err (~a "You cannot modify trick " name "!"))]))
+ (list 'err (~a "You cannot modify trick " name "!") 'missing-permissions)]))
(define/public (register name code author timestamp)
(cond
[(zero? (string-length code))
- (cons 'err (~a "Trick " name " needs a body!"))]
+ (list 'err (~a "Trick " name " needs a body!") 'needs-body)]
[(db:add-trick!
db (current-context-id) name
(thunk (trick author code timestamp (make-hash) 0)))
@@ -123,9 +123,9 @@
(define frontend (current-frontend))
(cond
[(not trick-obj)
- (cons 'err (~a "Trick " name " doesn't exist!"))]
+ (list 'err (~a "Trick " name " doesn't exist!") 'no-such-trick)]
[(zero? (string-length code))
- (cons 'err (~a "Trick " name " needs a body!"))]
+ (list 'err (~a "Trick " name " needs a body!") 'needs-body)]
[(db:update-trick!
db ctx-id name
(lambda (trick-obj)
@@ -138,7 +138,7 @@
(send frontend can-modify? t)))
(cons 'ok (~a "Successfully updated trick " name "!"))]
[else
- (cons 'err (~a "You cannot modify trick " name "!"))]))
+ (list 'err (~a "You cannot modify trick " name "!") 'missing-permissions)]))
(define/public (lookup name)
(db:get-trick db (current-context-id) name))
diff --git a/frontends/discord.rkt b/frontends/discord.rkt
index be00918..19bd541 100644
--- a/frontends/discord.rkt
+++ b/frontends/discord.rkt
@@ -447,13 +447,16 @@
(define (codeblock-quote result)
(~a "```scheme\n" result "```"))
+ (define (error-response err)
+ (list (car err)))
+
(define/command (call-snippet text)
" [_code_]: evaluate [_code_] as a Racket form"
(with-typing-indicator
(thunk
(define result
(send (current-backend) evaluate (strip-backticks text)))
- (result-case format-run-result list result))))
+ (result-case format-run-result error-response result))))
(define/command/trick (call-trick name body)
" [_name_] ...: invoke the trick [_name_], evaluating its source code in a fresh sandbox"
@@ -461,7 +464,7 @@
(thunk
(define result
(send (current-backend) call name body))
- (result-case format-run-result list result))))
+ (result-case format-run-result error-response result))))
(define/command/trick (register-trick name body)
" [_name_] [_code_]: register [_code_] as a trick with name [_name_]"
@@ -470,7 +473,7 @@
name (strip-backticks body)
(message-author-id (current-message))
(hash-ref (current-message) 'timestamp)))
- (list (cdr result)))
+ (list (result-case cdr cadr result)))
(define/command/trick (show-trick name _body)
" [_name_]: show metadata and source for the trick [_name_]"
@@ -491,11 +494,11 @@
(define/command/trick (update-trick name body)
" [_name_] [_code_]: change the source of the trick [_name_]; requires ownership or administrator"
- (list (cdr (send (current-backend) update name (strip-backticks body)))))
+ (list (result-case cdr cadr (send (current-backend) update name (strip-backticks body)))))
(define/command/trick (delete-trick name _body)
" [_name_]: delete the trick [_name_]; requires ownership or administrator and cannot be undone!"
- (list (cdr (send (current-backend) delete name))))
+ (list (result-case cdr cadr (send (current-backend) delete name))))
(define/command (popular text)
": show a leaderboard of popular tricks"
diff --git a/frontends/http.rkt b/frontends/http.rkt
deleted file mode 100644
index a779489..0000000
--- a/frontends/http.rkt
@@ -1,321 +0,0 @@
-#lang racket/base
-
-(require
- racket/class
- racket/list
- racket/match
- racket/function
- racket/string
- "../common.rkt"
- "../config.rkt"
- "../log.rkt"
- (prefix-in ev: "../evaluator.rkt")
- "../interface.rkt"
-
- net/base64
- file/sha1
- net/url-structs
- web-server/web-server
- web-server/servlet-dispatch
- web-server/http/xexpr
- web-server/http/request-structs
- web-server/http/response-structs)
-
-(provide r16-make-frontend)
-
-(struct image (bytes))
-
-(define (simple-response code msg body)
- (response/full
- code msg
- (current-seconds) TEXT/HTML-MIME-TYPE
- null
- (list body)))
-
-(define (format-for-html str)
- (define url-regex #px"(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\\?([^#]*))?(#(.*))?")
- `(div
- ((class "content"))
- ,@(for/list ([line (in-list (string-split str "\n"))])
- (define urls (regexp-match-positions* url-regex line #:match-select car))
- `(div
- ((class "block"))
- ,@(cond
- [(null? urls) (list line)]
- [else
- (define positions
- (append*
- (for/list ([url-pos (in-list urls)])
- (list (car url-pos) (cdr url-pos)))))
- (for/list ([start (in-list `(0 ,@positions))]
- [end (in-list `(,@positions ,(string-length line)))]
- [is-link? (in-cycle (in-list '(#f #t)))])
- (define v (substring line start end))
- (if is-link?
- `(a ((href ,v)) ,v)
- v))])))))
-
-(define HTML-DOCTYPE #"<!DOCTYPE html>")
-
-(define (response/html
- #:title title
- #:navbar? [navbar? #f]
- . body)
- (response/xexpr
- #:preamble HTML-DOCTYPE
- `(html (head
- (meta ((charset "utf-8")))
- (meta ((name "viewport") (content "width=device-width, initial-scale=1")))
- (title ,title)
- (link ((rel "stylesheet") (href "https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css")))
- (link ((rel "stylesheet") (href "https://fonts.googleapis.com/icon?family=Material+Icons")))
- (style
- ".code-input { font-family: mono; }"
- ".toggled .togglable { display: none; }"
- ".togglable-inverse { display: none; }"
- ".toggled .togglable-inverse { display: inline; }"))
- (body
- ,@(if navbar?
- `((navbar
- ((class "navbar is-dark")
- (role "navigation")
- (aria-label "main navigation"))
- (div
- ((class "navbar-brand has-background-dark"))
- (p ((class "navbar-item")) "R16")
- (a ((role "button")
- (class "navbar-burger")
- (aria-label "menu")
- (aria-expanded "false")
- (onclick "this.classList.toggle(\"is-active\");document.getElementById(\"navmenu\").classList.toggle(\"is-active\")"))
- (span ((aria-hidden "true")))
- (span ((aria-hidden "true")))
- (span ((aria-hidden "true")))))
- (div
- ((class "navbar-menu") (id "navmenu"))
- (div
- ((class "navbar-start"))
- (a ((class "navbar-item") (href "/")) "Home")
- (a ((class "navbar-item") (href "/tricks")) "Tricks"))
- (div
- ((class "navbar-end"))
- (a ((class "navbar-item") (href "https://sr.ht/~williewillus/r16")) "Source")))))
- null)
- (div ((class "container")) ,@body)
- (footer
- ((class "footer"))
- (div
- ((class "container has-text-centered"))
- ,(format-for-html (send (current-backend) about))))))))
-
-(define http-frontend
- (class* object% [r16-frontend<%>]
- (init-field port)
-
- (define mutex (make-semaphore 1))
- (define current-hashed (make-parameter #f))
- (define/public (response? v) (image? v))
- (define/public (get-enrich-context)
- (define (enrich-context base _trick _args _parent-ctx)
- (define (make-image png-bytes)
- (image png-bytes))
- `(((make-image . ,make-image)
- ,@(car base))
- ,@(cdr base)))
- enrich-context)
- (define/public (can-modify? trick)
- (equal? (trick-author trick) (current-hashed)))
- (define/public (start)
- (log-r16-debug "Starting server on port ~a" port)
- (serve
- #:dispatch (dispatch/servlet handle-request)
- #:port port)
- (do-not-return))
-
- (define (handle-request req)
- (define req-uri (request-uri req))
- (define path (takef (url-path req-uri) (compose1 non-empty-string? path/param-path)))
- (match* [(request-method req) path]
- [[#"POST" (list (path/param "tricks" _)
- (path/param "submit" _))]
- (define bindings (request-bindings/raw req))
- (match* [(bindings-assq #"name" bindings)
- (bindings-assq #"code" bindings)
- (bindings-assq #"password" bindings)]
- [[(binding:form _ (and name-bytes
- (pregexp "^[a-zA-Z_\\-0-9]+$")
- (app bytes->string/utf-8 name)))
- (binding:form _ (app bytes->string/utf-8 code))
- (binding:form _ password)]
- ;; yeah, it's not secure at all
- (call-with-semaphore
- mutex
- (thunk
- (define hashed (bytes->hex-string (sha256-bytes (bytes-append (sha256-bytes name-bytes) password))))
- (parameterize ([current-hashed hashed])
- (cond
- [(zero? (string-length code))
- (match (send (current-backend) delete name)
- [(cons 'ok msg) (simple-response 200 #"OK" (string->bytes/utf-8 msg))]
- [(cons 'err msg) (simple-response 400 #"Bad Request" (string->bytes/utf-8 msg))])]
- [else
- (match (send (current-backend) register name code hashed (number->string (current-seconds)))
- [(cons 'ok _)
- (response/full
- 303 #"See Other"
- (current-seconds) TEXT/HTML-MIME-TYPE
- (list (make-header #"Location" (string->bytes/utf-8 (format "/tricks/~a.rkt" name))))
- (list #"Redirecting..."))]
- [(cons 'err msg)
- (simple-response 400 #"Bad Request" (string->bytes/utf-8 msg))])]))))]
- [[_ _ _]
- (simple-response 400 #"Bad Request" #"400 Bad Request")])]
-
- [[#"GET" (list (path/param "tricks" _))]
- (define popular (send (current-backend) popular))
- (response/html
- #:title "Tricks"
- #:navbar? #t
- `(div
- ((class "section"))
- (h1 ((class "title")) "Add trick")
- (form
- ((action "/tricks/submit")
- (method "post")
- (target "_blank")
- (enctype "multipart/form-data"))
-
- (div
- ((class "field"))
- (div
- ((class "columns"))
- (div
- ((class "column"))
- (label ((class "label") (for "name")) "Trick Name")
- (div
- ((class "control"))
- (input ((class "input") (type "text") (id "name") (name "name")))))
- (div
- ((class "column"))
- (label ((class "label") (for "password")) "Password")
- (div
- ((class "control"))
- (input ((class "input") (type "password") (id "password") (name "password")))))))
- (div
- ((class "field"))
- (label ((class "label") (for "code")) "Code")
- (div
- ((class "control"))
- (textarea
- ((class "textarea code-input")
- (id "code")
- (name "code")
- (rows "5")
- (cols "60")
- (placeholder "Enter code")
- (spellcheck "false")))))
- (div
- ((class "field"))
- (input ((class "button is-success") (type "submit") (value "Add"))))))
- `(div
- ((class "section"))
- (h1 ((class "title")) "Registered tricks")
- ,@(for/list ([trick-pair (in-list popular)])
- (define name (car trick-pair))
- (define trick-v (cdr trick-pair))
- `(div
- ((class "block"))
- (div
- ((class "card toggled"))
- (header
- ((class "card-header has-background-dark has-text-white")
- (onclick "this.parentElement.classList.toggle(\"toggled\")")
- (title ,(format "Invocations: ~a" (trick-invocations trick-v))))
- (p ((class "card-header-title has-text-white")) ,name)
- (button
- ((class "card-header-icon")
- (aria-label "more options"))
- (span
- ((class "material-icons togglable-inverse") (aria-hidden "true"))
- "arrow_drop_down")
- (span
- ((class "material-icons togglable") (aria-hidden "true"))
- "arrow_drop_up")))
- (div
- ((class "card-content togglable"))
- (form
- ((action ,(format "/tricks/~a" name))
- (target "_blank")
- (method "get"))
- (div
- ((class "field"))
- (textarea ((class "textarea code-input")
- (name "args")
- (spellcheck "false")
- (rows "1")
- (placeholder "Enter arguments"))))
- (input ((class "button is-primary") (type "submit") (value "Call")))))
- (footer
- ((class "card-footer togglable"))
- (a ((class "card-footer-item") (href ,(format "/tricks/~a.rkt" name)) (target "_blank")) "Source")))))))]
-
- [[#"GET" (list (path/param "tricks" _)
- (path/param (pregexp "^([a-zA-Z_\\-0-9]+)\\.rkt$" (list _ trick-name)) _))]
- (define trick-v (send (current-backend) lookup trick-name))
- (response/full
- 200 #"OK"
- (current-seconds) #"text/plain; charset=utf-8"
- null
- (list (string->bytes/utf-8 (trick-body trick-v))))]
-
- [[#"GET" (list (path/param "tricks" _)
- (path/param (pregexp "^[a-zA-Z_\\-0-9]+$" (list trick-name)) _))]
- (define args (assoc 'args (url-query req-uri)))
- (match (send (current-backend) call trick-name (if args (cdr args) ""))
- [(cons 'ok res)
- (define stderr (ev:run-result-stderr res))
- (define stdout (ev:run-result-stdout res))
- (define results (ev:run-result-results res))
- (match* [stderr stdout results]
- [[#f "" (list (image raw-bytes))]
- (response/full
- 200 #"OK"
- (current-seconds) #"image/png"
- null
- (list raw-bytes))]
- [[_ _ _]
- (response/xexpr
- #:preamble HTML-DOCTYPE
- `(html
- (head (title ,trick-name))
- (body
- ,@(if stderr `((pre ,stderr)) null)
- ,@(if (zero? (string-length stdout)) null `((pre ,stdout)))
- ,@(for/list ([r results])
- (cond
- [(image? r)
- `(img
- ((src ,(format "data:image/png;base64,~a" (base64-encode (image-bytes r))))))]
- [else
- `(pre ,r)])))))])]
- [(cons 'err msg)
- (simple-response 400 #"Bad Request" (string->bytes/utf-8 msg))])]
-
- [[#"GET" (list)]
- (response/html
- #:title "r16"
- #:navbar? #t
- `(div
- ((class "section"))
- (h1 ((class "title")) "Stats")
- ,(format-for-html (send (current-backend) stats))))]
-
- [[#"GET" _]
- (simple-response 404 #"Not Found" #"404 Not Found")]))
-
- (super-new)))
-
-(define (r16-make-frontend raw-config)
- (define port (check-config exact-positive-integer? (hash-ref raw-config 'port 8080)))
- (new http-frontend
- [port port]))
diff --git a/frontends/http/base.html b/frontends/http/base.html
new file mode 100644
index 0000000..095b55e
--- /dev/null
+++ b/frontends/http/base.html
@@ -0,0 +1,43 @@
+@; Inputs: 'title 'navbar? 'body
+<!DOCTYPE html>
+<html lang="en" class="text-[16px] text-slate-700 bg-neutral-50">
+ <head>
+ <meta charset="utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
+ <title>@|title|</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
+ <link rel="stylesheet" href="/style.css"></style>
+ </head>
+ <body>
+ @when[navbar?]{
+ @list{
+ <navbar class="bg-neutral-700 text-white flex items-stretch min-h-[3.25rem]"
+ role="navigation"
+ aria-label="main navigation">
+ <div class="flex items-stretch">
+ <p class="flex items-center py-2 px-3">
+ <img class="w-8 h-8" src="/icon.svg"></img>
+ </p>
+ </div>
+ <div class="flex items-stretch grow">
+ <div class="flex items-stretch justify-start mr-auto">
+ <a class="flex items-center py-2 px-3 hover:bg-neutral-800" href="/">Home</a>
+ <a class="flex items-center py-2 px-3 hover:bg-neutral-800" href="/tricks">Tricks</a>
+ </div>
+ <div class="flex items-stretch justify-end ml-auto">
+ <a class="flex items-center py-2 px-3 hover:bg-neutral-800" href="https://sr.ht/~williewillus/r16">Source</a>
+ </div>
+ </div>
+ </navbar>
+ }
+ }
+ <div class="w-full bg-white">
+ <div class="mx-auto 2xl:max-w-screen-xl xl:max-w-screen-lg lg:max-w-screen-md">@|body|</div>
+ </div>
+ <footer class="bg-neutral-50">
+ <div class="p-12 text-center">
+ @(format-for-html (send (current-backend) about))
+ </div>
+ </footer>
+ </body>
+</html>
diff --git a/frontends/http/favicon.ico b/frontends/http/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..de7078f233317ef74eb70a5d308bce115f1588ed
GIT binary patch
literal 67646
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
Vgv}XJ@5V{KA;@x3?s6~!{||>D@CpC`
literal 0
HcmV?d00001
diff --git a/frontends/http/home.html b/frontends/http/home.html
new file mode 100644
index 0000000..0ee8a2d
--- /dev/null
+++ b/frontends/http/home.html
@@ -0,0 +1,5 @@
+@; No inputs
+<div class="p-12">
+ <h1 class="text-3xl font-bold mb-6">Stats</h1>
+ @(format-for-html (send (current-backend) stats))
+</div>
diff --git a/frontends/http/icon.svg b/frontends/http/icon.svg
new file mode 100644
index 0000000..ce74571
--- /dev/null
+++ b/frontends/http/icon.svg
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="48"
+ height="48"
+ viewBox="0 0 12.7 12.7"
+ version="1.1"
+ id="svg12"
+ inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
+ sodipodi:docname="icon.svg"
+ xml:space="preserve"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+ id="namedview14"
+ pagecolor="#505050"
+ bordercolor="#eeeeee"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#505050"
+ inkscape:document-units="mm"
+ showgrid="false"
+ inkscape:zoom="11.313709"
+ inkscape:cx="29.168155"
+ inkscape:cy="21.655145"
+ inkscape:window-width="1920"
+ inkscape:window-height="1005"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1"
+ showguides="false" /><defs
+ id="defs9" /><g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"><g
+ id="g2355"
+ transform="matrix(0.91804629,1.019921,-0.99884202,0.94043439,6.7377364,-6.9958554)"><g
+ id="g2582"
+ transform="matrix(0.61546137,-0.63649361,0.63617398,0.58694288,-1.8558291,6.9648282)"><path
+ style="fill:#2b1100;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 7.8446378,8.6920138 9.7349272,8.006514 11.707549,6.9120432 11.843408,6.6855273 11.636088,6.5130031 6.6632938,7.4475905 Z"
+ id="path1537"
+ sodipodi:nodetypes="ccccccc" /><g
+ id="g1482"><ellipse
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.030263;stroke-dasharray:none;stop-color:#000000"
+ id="path1266-3-6"
+ cx="8.3380928"
+ cy="8.7987413"
+ rx="0.72943509"
+ ry="1.2260661"
+ transform="matrix(1,0,-0.18171274,0.98335166,0,0)" /><ellipse
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.030263;stroke-dasharray:none;stop-color:#000000"
+ id="path1266-3"
+ cx="8.672637"
+ cy="8.9571257"
+ rx="0.72943509"
+ ry="1.2260661"
+ transform="matrix(1,0,-0.18171274,0.98335166,0,0)" /><ellipse
+ style="opacity:1;fill:#666666;stroke:none;stroke-width:0.0192557;stroke-dasharray:none;stop-color:#000000"
+ id="path1266"
+ cx="8.7366724"
+ cy="8.9439192"
+ rx="0.46412322"
+ ry="0.78011847"
+ transform="matrix(1,0,-0.18171274,0.98335166,0,0)" /></g><g
+ id="g1482-2"
+ transform="matrix(0.53446691,-0.04360994,0.04360994,0.53446691,7.1804071,2.9320907)"><ellipse
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.030263;stroke-dasharray:none;stop-color:#000000"
+ id="path1266-3-6-7"
+ cx="8.3380928"
+ cy="8.7987413"
+ rx="0.72943509"
+ ry="1.2260661"
+ transform="matrix(1,0,-0.18171274,0.98335166,0,0)" /><ellipse
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.030263;stroke-dasharray:none;stop-color:#000000"
+ id="path1266-3-0"
+ cx="8.672637"
+ cy="8.9571257"
+ rx="0.72943509"
+ ry="1.2260661"
+ transform="matrix(1,0,-0.18171274,0.98335166,0,0)" /><ellipse
+ style="opacity:1;fill:#666666;stroke:none;stroke-width:0.0192557;stroke-dasharray:none;stop-color:#000000"
+ id="path1266-9"
+ cx="8.7366724"
+ cy="8.9439192"
+ rx="0.46412322"
+ ry="0.78011847"
+ transform="matrix(1,0,-0.18171274,0.98335166,0,0)" /></g><path
+ style="opacity:1;fill:#ff0000"
+ d="m 1.6793011,6.7156723 c 0,0 0.3177174,-0.2906969 0.9140617,-0.4926469 1.149243,-0.3892598 2.4775849,-0.6064402 2.4775849,-0.6064402 0,0 0.7743494,-1.1514973 0.8581033,-1.2611017 0.093078,-0.040247 0.2475159,-0.162861 0.7825183,-0.1980318 L 8.1167726,4.094834 c 0,0 0.5854591,0.024663 0.855628,0.01889 0.2456737,0.035511 1.9753494,0.1325717 1.9753494,0.1325717 0.126721,0.058449 0.358642,0.3530208 0.593064,0.6842311 0.211791,0.2992349 0.427359,0.6207368 0.538132,0.9618418 l -0.09787,0.507183 c 0,0 -0.03144,0.3994155 -0.251952,0.5214519 0,0 0.03848,-0.1807453 0.02625,-0.1865008 -0.04607,-0.063333 -0.241435,0.00564 -0.42402,0.1280836 l -0.233764,0.4744478 -0.322527,0.1542192 -0.03342,0.079625 c 0,0 -0.553217,0.2594952 -0.8304199,0.3828551 -0.2778259,0.1454976 -1.8355055,0.7237729 -1.8355055,0.7237729 0,0 0.054761,-0.2437003 -0.020319,-0.6892414 C 7.9751278,7.6853338 7.904724,7.6899834 7.7898714,7.5605763 7.6130522,7.5151007 7.6166514,7.4989813 7.2289203,7.6028733 6.9352325,7.701357 6.815883,7.8193039 6.7471974,7.8748407 6.6676054,7.9737701 6.5335808,8.1428416 6.444312,8.3335706 6.2762497,8.692648 6.1658293,9.1049462 6.1658293,9.1049462 6.0719796,9.1917106 5.7580414,9.2107728 5.4577127,9.2481915 5.2646052,9.2424647 4.1430664,8.9400389 4.1430664,8.9400389 L 1.9680034,7.7366812 Z"
+ id="path215"
+ sodipodi:nodetypes="ccccccccsccccccccccccccsccccc" /><path
+ style="fill:#00ffff;stroke:none;stroke-width:0.0269875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+ d="m 5.2170041,5.4772501 c -0.032036,0.024234 0.072186,0.037374 0.072186,0.037374 L 8.396629,5.8703478 c 0,0 0.1032609,0.014243 0.2167412,-0.036999 C 8.694435,5.7802523 8.709779,5.7165541 8.709779,5.7165541 8.7784888,5.3652764 8.9315902,4.6965787 8.9066371,4.5556902 8.9244951,4.3494912 7.7790753,4.2625488 7.3050775,4.2355251 6.8310796,4.2085011 6.0599217,4.2798231 5.9535875,4.3753143 Z"
+ id="path991"
+ sodipodi:nodetypes="ccccccscc" /><path
+ style="opacity:1;fill:#f9f9f9;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 1.6724805,7.9477631 c 0,0 0.4057261,0.213695 0.5742953,0.2799417 0.2795746,0.089547 1.5015504,0.4430406 1.6370133,0.4794593 0.1480695,0.040753 1.2843597,0.3028603 1.502017,0.3092193 0.2176574,0.00636 0.8416384,-0.1185482 0.8416384,-0.1185482 L 6.3843746,8.4744811 c 0,0 -0.6404257,0.1739944 -0.8479998,0.1676334 C 5.3288006,8.6357544 4.2053624,8.3920796 4.1033871,8.3640003 3.9198169,8.3050712 2.5772727,7.8798022 2.5772727,7.8798022 c 0,0 -0.6500039,-0.1813305 -0.6857419,-0.2183323 -0.012409,-0.027689 0.014248,-0.084786 0.014248,-0.084786 0,0 -0.1796452,0.07146 -0.1977973,0.085352 -0.024997,0.021449 -0.061778,0.080706 -0.073263,0.1110017 -1.762e-4,0.059261 0.037762,0.1747256 0.037762,0.1747255 z"
+ id="path1571"
+ sodipodi:nodetypes="cccsccsccccccc" /><path
+ style="fill:#f9f9f9;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 11.881503,6.751299 c 0,0 0.168045,-0.051677 0.269607,-0.1283385 0.02541,-0.031872 0.022,-0.1303578 0.01311,-0.158809 -0.03134,-0.034961 -0.179553,-0.080683 -0.179553,-0.080683 l -0.022,0.1339263 c -0.01281,0.051058 -0.08117,0.2339042 -0.08117,0.2339042 z"
+ id="path1711"
+ sodipodi:nodetypes="ccccccc" /><path
+ style="fill:#00ffff;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 8.8397451,5.9005251 C 8.8062191,5.8506731 9.018576,4.6104521 9.1571362,4.5433786 9.2956964,4.4763056 10.790622,4.3984947 10.940027,4.5016569 c 0.149405,0.1031621 0.58263,0.8517712 0.560054,0.9432753 -0.02258,0.091504 -2.6102659,0.477233 -2.6603359,0.4555929 z"
+ id="path1713"
+ sodipodi:nodetypes="csscc" /><path
+ style="opacity:1;fill:#999999;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 1.6686261,6.7122063 c -0.027033,0.049278 0.00971,0.4474938 0.1010088,0.5713854 0.091296,0.1238917 1.1193243,0.5101368 1.2845769,0.5456182 0.1652527,0.035482 2.0861946,0.4227248 2.2456336,0.3833746 0.159439,-0.03935 0.1692166,-0.2965293 0.1692166,-0.403987 0,-0.1074578 0.012975,-0.3106562 -0.1897377,-0.3885781 C 5.0766114,7.3420974 3.4206061,7.11768 3.3201072,7.130366 3.2196084,7.1430527 3.1717567,7.2834211 3.1717567,7.2834211 L 2.8212528,7.2074682 c 0,0 -0.068139,-0.1837381 -0.1243904,-0.2176373 C 2.6406115,6.9559317 1.6949493,6.6615658 1.6686261,6.7122063 Z"
+ id="path1715"
+ sodipodi:nodetypes="cssssssccsc" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 2.3412966,7.4363582 2.8625898,7.6262592 2.8607641,7.5646398 2.3397838,7.3758697 Z"
+ id="path1717" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 2.3512177,7.338614 2.860323,7.528515 2.8484446,7.4639146 2.352777,7.2789483 Z"
+ id="path1717-3"
+ sodipodi:nodetypes="ccccc" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 2.3505641,7.2493669 2.8421551,7.424954 2.8207549,7.3595962 2.3464685,7.1881837 Z"
+ id="path1717-6"
+ sodipodi:nodetypes="ccccc" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 2.3477005,7.156058 2.8203138,7.3234714 2.7989169,7.2492185 2.3353086,7.0810409 Z"
+ id="path1717-3-0"
+ sodipodi:nodetypes="ccccc" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 2.3275896,7.0510161 2.7378866,7.200189 2.7017376,7.1214037 2.3138811,6.98722 Z"
+ id="path1790"
+ sodipodi:nodetypes="ccccc" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.2870315,6.8925259 0.021131,0.074939 0.382772,0.1313778 C 2.6081351,6.9945736 2.4462478,6.9444643 2.2870315,6.8925259 Z"
+ id="path1792"
+ sodipodi:nodetypes="cccc" /><path
+ style="fill:#ffffff;stroke:none;stroke-width:0.284693px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 1.7750972,6.8257544 c 0,0 -0.056726,0.3485276 0.016905,0.4141901 0.073631,0.065662 0.4375705,0.1795598 0.4853653,0.1386119 0.047795,-0.040948 8.04e-5,-0.385499 -0.054299,-0.4462441 C 2.1686897,6.8715673 1.8343248,6.7699549 1.7750972,6.8257544 Z"
+ id="path1794" /><path
+ style="fill:#ffffff;stroke:none;stroke-width:0.280763px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.4626337,7.4216451 c -0.085319,0.087047 -0.00946,0.4634482 0.045638,0.5071091 0.055098,0.043661 0.6431375,0.1723766 0.7107567,0.1012747 0.067619,-0.071102 0.02007,-0.4149322 -0.043643,-0.4883863 C 5.1116721,7.468188 4.5370195,7.3777087 4.4626337,7.4216451 Z"
+ id="path1796" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 3.3304136,7.7598592 4.386585,7.9496247 4.3489771,7.8774604 3.3105463,7.6922923 Z"
+ id="path1798" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 3.3058142,7.581225 0.00198,0.066978 1.0209725,0.1884979 -0.030352,-0.069747 z"
+ id="path1800"
+ sodipodi:nodetypes="ccccc" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 3.2985572,7.4924207 0.00285,0.061813 0.9689086,0.1670145 -0.016182,-0.066872 z"
+ id="path1802" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 3.3016433,7.3875288 0.00146,0.065372 0.944712,0.1634244 -0.00857,-0.079127 z"
+ id="path1804" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 3.3484745,7.2901628 -0.039855,0.060815 0.9288535,0.1615644 0.00677,-0.077175 z"
+ id="path1806"
+ sodipodi:nodetypes="ccccc" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 3.3648795,7.2679357 c 0,0 0.040296,-0.046604 0.087686,-0.055869 l 0.8217251,0.1118989 -0.011187,0.077862 z"
+ id="path1808"
+ sodipodi:nodetypes="ccccc" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.9707895,7.6742207 0.030687,0.088502 0.135015,0.028198 0.00581,-0.062412 z"
+ id="path1810" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.9447262,7.5745318 0.017148,0.073099 0.1766035,0.045978 0.011298,-0.06685 z"
+ id="path1812" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.9218448,7.4777636 0.014621,0.064662 0.2138113,0.054541 2.382e-4,-0.068689 z"
+ id="path1814" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.9005535,7.365861 0.015081,0.080423 0.2358218,0.051363 0.00887,-0.068393 z"
+ id="path1816" /><path
+ style="fill:#333333;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.8886062,7.2539015 0.012914,0.088634 0.2566856,0.06281 0.0081,-0.090248 z"
+ id="path1818" /><path
+ style="fill:#999999;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.3945851,8.1214997 0.03233,0.4260111 c 0,0 0.4031754,0.1667788 0.5031548,0.1996104 0.099979,0.032831 0.7856493,0.1923843 0.7856493,0.1923843 l -0.00369,-0.4310996 c 0,0 -0.6549766,-0.1140655 -0.7702664,-0.1595953 C 2.8569882,8.3153321 2.3945858,8.1214997 2.3945851,8.1214997 Z"
+ id="path1854"
+ sodipodi:nodetypes="ccsccsc" /><path
+ style="fill:#1a1a1a;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.2037278,8.5415849 c 0,0 -0.1896868,-0.6024502 -0.1689952,-0.6991277 0.031298,-0.043065 0.1297596,-0.098844 0.1297596,-0.098844 l 0.2071374,0.077509 0.069085,0.7135511 c 0,0 -0.060219,0.052667 -0.1238306,0.044235 -0.063612,-0.00843 -0.1131567,-0.037323 -0.1131562,-0.037323 z"
+ id="path1820"
+ sodipodi:nodetypes="ccccccc" /><path
+ style="fill:#1a1a1a;stroke:none;stroke-width:0.277277px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 3.7419399,8.9877574 c 0,0 -0.2135569,-0.5742229 -0.189601,-0.6659321 0.036236,-0.040852 0.1441756,-0.091031 0.1441756,-0.091031 l 0.2398154,0.073526 0.079984,0.676881 c 0,0 -0.069719,0.04996 -0.1433662,0.041962 -0.073647,-0.008 -0.1310083,-0.035405 -0.1310078,-0.035406 z"
+ id="path1820-6"
+ sodipodi:nodetypes="cccccccc" /></g></g></g></svg>
diff --git a/frontends/http/main.rkt b/frontends/http/main.rkt
new file mode 100644
index 0000000..2ddec2b
--- /dev/null
+++ b/frontends/http/main.rkt
@@ -0,0 +1,265 @@
+#lang racket/base
+
+(require
+ racket/class
+ racket/list
+ racket/match
+ racket/function
+ racket/string
+ racket/include
+ racket/port
+
+ "../../common.rkt"
+ "../../config.rkt"
+ "../../log.rkt"
+ (prefix-in ev: "../../evaluator.rkt")
+ "../../interface.rkt"
+
+ xml
+ net/base64
+ file/sha1
+ net/url-structs
+ web-server/web-server
+ web-server/servlet-dispatch
+ web-server/templates
+ web-server/http/request-structs
+ web-server/http/response-structs
+
+ (for-syntax racket/base
+ racket/port
+ syntax/location))
+
+(provide r16-make-frontend)
+
+(define-for-syntax use-reincludes?
+ (environment-variables-ref
+ (current-environment-variables)
+ #"R16_REINCLUDE_TEMPLATES"))
+
+(define-syntax (syntax-relative-path stx)
+ (syntax-case stx ()
+ [(_ path)
+ (quasisyntax/loc stx
+ (build-path
+ #,(syntax-source-directory stx)
+ path))]))
+
+(define-syntax (include-bytes stx)
+ (syntax-case stx ()
+ [(_ path)
+ (if use-reincludes?
+ (quasisyntax/loc stx
+ (with-input-from-file (syntax-relative-path path) port->bytes))
+ (quasisyntax/loc stx
+ (include/reader
+ path
+ (lambda (_ port)
+ (if (port-closed? port)
+ eof
+ (datum->syntax
+ #'#,stx
+ (port->bytes port #:close? #t)))))))]))
+
+(define-syntax (maybe-define-namespace-anchor stx)
+ (syntax-case stx ()
+ [(_ name)
+ (if use-reincludes?
+ (quasisyntax/loc stx
+ (define-namespace-anchor name))
+ #'(begin))]))
+
+(maybe-define-namespace-anchor anchor)
+
+(define-syntax (include-template* stx)
+ (syntax-case stx ()
+ [(_ path args ...)
+ (if use-reincludes?
+ (quasisyntax/loc stx
+ (let ([namespace (namespace-anchor->namespace anchor)])
+ (namespace-set-variable-value!
+ 'args args
+ #f namespace) ...
+ (eval #'(include-template path)
+ namespace)))
+ (quasisyntax/loc stx
+ (include-template path)))]))
+
+(struct image (bytes))
+
+(define (simple-response code msg)
+ (define title msg)
+ (define navbar? #t)
+ (define body (include-template* "simple.html" msg))
+ (response/full
+ code #f
+ (current-seconds) TEXT/HTML-MIME-TYPE
+ null
+ (list
+ (string->bytes/utf-8
+ (include-template* "base.html" title navbar? body)))))
+
+(define (format-for-html str)
+ (include-template* "raw.html" str))
+
+(define (response/html #:title title #:navbar? [navbar? #f] . body)
+ (response/full
+ 200 #f
+ (current-seconds) TEXT/HTML-MIME-TYPE
+ null
+ (list (string->bytes/utf-8 (include-template* "base.html" title navbar? body)))))
+
+(define http-frontend
+ (class* object% [r16-frontend<%>]
+ (init-field port)
+
+ (define trick-modify-mutex (make-semaphore 1))
+ (define current-password-hash (make-parameter #f))
+ (define/public (response? v) (image? v))
+ (define/public (get-enrich-context)
+ (define (enrich-context base _trick _args _parent-ctx)
+ (define (make-image png-bytes)
+ (image png-bytes))
+ `(((make-image . ,make-image)
+ ,@(car base))
+ ,@(cdr base)))
+ enrich-context)
+ (define/public (can-modify? trick)
+ (equal? (trick-author trick) (current-password-hash)))
+ (define/public (start)
+ (log-r16-debug "Starting server on port ~a" port)
+ (parameterize ([current-frontend this])
+ (serve
+ #:dispatch (dispatch/servlet handle-request)
+ #:port port)
+ (do-not-return)))
+
+ (define (handle-request req)
+ (define req-uri (request-uri req))
+ (define path (takef (url-path req-uri) (compose1 non-empty-string? path/param-path)))
+ (match* [(request-method req) path]
+ [[#"POST" (list (path/param "tricks" _)
+ (path/param "submit" _))]
+ (define bindings (request-bindings/raw req))
+ (match* [(bindings-assq #"name" bindings)
+ (bindings-assq #"code" bindings)
+ (bindings-assq #"password" bindings)]
+ [[(binding:form _ (and name-bytes
+ (pregexp "^[a-zA-Z_\\-0-9]+$")
+ (app bytes->string/utf-8 name)))
+ (binding:form _ (app bytes->string/utf-8 code))
+ (binding:form _ password)]
+ (call-with-semaphore
+ ;; protect against concurrent modification of the body
+ trick-modify-mutex
+ (thunk
+ ;; hash the password, using the sha256sum of the trick name as salt;
+ ;; this is not completely secure because the salt *should*
+ ;; be generated randomly so as not to be predictable,
+ ;; but the risk factor is estimated to be low;
+ ;; this hash is then used as the "author" field of the trick
+ ;; and checked against for modification perms
+ (define salt (sha256-bytes name-bytes))
+ (define hashed-pass (bytes->hex-string (sha256-bytes (bytes-append salt password))))
+ (parameterize ([current-password-hash hashed-pass])
+ (cond
+ [(zero? (string-length code))
+ (match (send (current-backend) delete name)
+ [(cons 'ok msg) (simple-response 200 (string->bytes/utf-8 msg))]
+ [(list* 'err msg _) (simple-response 400 (string->bytes/utf-8 msg))])]
+ [else
+ (match (send (current-backend) register name code hashed-pass (number->string (current-seconds)))
+ [(cons 'ok _)
+ (response/full
+ 303 #"See Other"
+ (current-seconds) TEXT/HTML-MIME-TYPE
+ (list (make-header #"Location" (string->bytes/utf-8 (format "/tricks/~a.rkt" name))))
+ (list #"Redirecting..."))]
+ [(list* 'err msg _)
+ (simple-response 400 (string->bytes/utf-8 msg))])]))))]
+ [[_ _ _]
+ (simple-response 400 #"400 Bad Request")])]
+
+ [[#"GET" (list (path/param "tricks" _))]
+ (response/html
+ #:title "Tricks"
+ #:navbar? #t
+ (include-template* "tricks.html"))]
+
+ [[#"GET" (list (path/param "tricks" _)
+ (path/param (pregexp "^([a-zA-Z_\\-0-9]+)\\.rkt$" (list _ trick-name)) _))]
+ (define trick-v (send (current-backend) lookup trick-name))
+ (response/full
+ 200 #"OK"
+ (current-seconds) #"text/plain; charset=utf-8"
+ null
+ (list (string->bytes/utf-8 (trick-body trick-v))))]
+
+ [[#"GET" (list (path/param "tricks" _)
+ (path/param (pregexp "^[a-zA-Z_\\-0-9]+$" (list trick-name)) _))]
+ (define args (assoc 'args (url-query req-uri)))
+ (define resp (send (current-backend) call trick-name (if args (cdr args) "")))
+ (match resp
+ [(cons 'ok res)
+ (define stderr (ev:run-result-stderr res))
+ (define stdout (ev:run-result-stdout res))
+ (define results (ev:run-result-results res))
+ (match* [stderr stdout results]
+ [[#f "" (list (image raw-bytes))]
+ (response/full
+ 200 #f
+ (current-seconds) #"image/png"
+ null
+ (list raw-bytes))]
+ [[_ _ _]
+ (response/full
+ 200 #f
+ (current-seconds) TEXT/HTML-MIME-TYPE
+ null
+ (list (string->bytes/utf-8
+ (include-template*
+ "result.html"
+ trick-name
+ stderr
+ stdout
+ results))))])]
+ [(list 'err msg 'no-such-trick)
+ (simple-response 404 (string->bytes/utf-8 msg))]
+ [(list* 'err msg _)
+ (simple-response 400 (string->bytes/utf-8 msg))])]
+
+ [[#"GET" (list (path/param (or "style.css" "error.css") _))]
+ (response/full
+ 200 #f
+ (current-seconds) #"text/css"
+ null
+ (list (include-bytes "style.css")))]
+
+ [[#"GET" (list (path/param "favicon.ico" _))]
+ (response/full
+ 200 #f
+ (current-seconds) #"image/x-icon"
+ null
+ (list (include-bytes "favicon.ico")))]
+
+ [[#"GET" (list (path/param "icon.svg" _))]
+ (response/full
+ 200 #f
+ (current-seconds) #"image/svg+xml"
+ null
+ (list (include-bytes "icon.svg")))]
+
+ [[#"GET" (list)]
+ (response/html
+ #:title "r16"
+ #:navbar? #t
+ (include-template* "home.html"))]
+
+ [[#"GET" _]
+ (simple-response 404 #"404 Not Found")]))
+
+ (super-new)))
+
+(define (r16-make-frontend raw-config)
+ (define port (check-config exact-positive-integer? (hash-ref raw-config 'port 8080)))
+ (new http-frontend
+ [port port]))
diff --git a/frontends/http/raw.html b/frontends/http/raw.html
new file mode 100644
index 0000000..0b85c25
--- /dev/null
+++ b/frontends/http/raw.html
@@ -0,0 +1,23 @@
+@; Inputs: 'str
+@(define url-regex #px"(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\\?([^#]*))?(#(.*))?")
+<div>
+ @in[line (in-list (string-split str "\n"))]{
+ @(define urls (regexp-match-positions* url-regex line #:match-select car))
+ <div class="mb-6 last:mb-0">
+ @(cond
+ [(null? urls) (list line)]
+ [else
+ (define positions
+ (append*
+ (for/list ([url-pos (in-list urls)])
+ (list (car url-pos) (cdr url-pos)))))
+ (for/list ([start (in-list `(0 ,@positions))]
+ [end (in-list `(,@positions ,(string-length line)))]
+ [is-link? (in-cycle (in-list '(#f #t)))])
+ (define v (substring line start end))
+ (if is-link?
+ (xexpr->string `(a ((href ,v) (class "text-blue-500 hover:text-blue-700")) ,v))
+ v))])
+ </div>
+ }
+</div>
diff --git a/frontends/http/result.html b/frontends/http/result.html
new file mode 100644
index 0000000..04079a4
--- /dev/null
+++ b/frontends/http/result.html
@@ -0,0 +1,40 @@
+@; Inputs: 'trick-name 'stderr 'stdout 'results
+@; TODO: limits on output length
+@(define trick-name-esc (xexpr->string trick-name))
+@(local-require racket/port)
+@(define description
+ (with-output-to-string
+ (lambda ()
+ (when stderr
+ (display stderr))
+ (unless (zero? (string-length stdout))
+ (display stdout))
+ (for ([r (in-list results)])
+ (unless (image? r)
+ (displayln r))))))
+@(define head
+ @list{
+ <title>@|trick-name-esc|</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
+ <meta property="og:title" content="@(xml-attribute-encode trick-name)">
+ <meta property="og:type" content="website">
+ <meta property="og:description" content="@(xml-attribute-encode description)">
+ @(when stderr
+ @list{<meta name="theme-color" content="#FF0000">})
+ })
+@(define body
+ (list
+ (when stderr @list{<pre style="color:red;">@(xexpr->string stderr)</pre>})
+ (unless (zero? (string-length stdout)) @list{<pre>@(xexpr->string stdout)</pre>})
+ (for/list ([r (in-list results)])
+ (cond
+ [(image? r) @list{<img src="data:image/png;base64,@(base64-encode (image-bytes r))"></img>}]
+ [else @list{<pre>@(xexpr->string r)</pre>}]))))
+
+<!DOCTYPE html>
+<head>
+@|head|
+</head>
+<body>
+@|body|
+</body>
diff --git a/frontends/http/simple.html b/frontends/http/simple.html
new file mode 100644
index 0000000..7647576
--- /dev/null
+++ b/frontends/http/simple.html
@@ -0,0 +1,4 @@
+@; Inputs: msg
+<div class="p-12">
+ <span class="text-2xl font-bold mb-2">@|msg|</span>
+</div>
diff --git a/frontends/http/style-in.css b/frontends/http/style-in.css
new file mode 100644
index 0000000..e094896
--- /dev/null
+++ b/frontends/http/style-in.css
@@ -0,0 +1,20 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+.toggled .togglable { display: none; }
+.togglable-inverse { display: none; }
+.toggled .togglable-inverse { display: inline; }
+
+.trick > .trick-info {
+ max-width: 0;
+ max-height: 0;
+ overflow: hidden;
+ opacity: 0;
+}
+
+.trick:hover > .trick-info {
+ max-width: 2000px;
+ max-height: 300px;
+ opacity: 1;
+}
diff --git a/frontends/http/style.css b/frontends/http/style.css
new file mode 100644
index 0000000..d974471
--- /dev/null
+++ b/frontends/http/style.css
@@ -0,0 +1 @@
+/*! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:after,:before{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.absolute{position:absolute}.-m-3{margin:-.75rem}.-m-6{margin:-1.5rem}.mx-auto{margin-left:auto}.mr-auto,.mx-auto{margin-right:auto}.ml-auto{margin-left:auto}.mb-6{margin-bottom:1.5rem}.mb-3{margin-bottom:.75rem}.mb-2{margin-bottom:.5rem}.mr-2{margin-right:.5rem}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-1{height:.25rem}.h-5{height:1.25rem}.h-2{height:.5rem}.h-12{height:3rem}.h-4{height:1rem}.h-8{height:2rem}.min-h-\[3\.25rem\]{min-height:3.25rem}.w-full{width:100%}.w-12{width:3rem}.w-4{width:1rem}.w-8{width:2rem}.shrink{flex-shrink:1}.grow{flex-grow:1}.basis-0{flex-basis:0px}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-center{align-content:center}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-neutral-100{--tw-border-opacity:1;border-color:rgb(245 245 245/var(--tw-border-opacity))}.bg-neutral-50{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity))}.bg-neutral-700{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.p-12{padding:3rem}.p-3{padding:.75rem}.p-2{padding:.5rem}.p-6{padding:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[16px\]{font-size:16px}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.font-bold{font-weight:700}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-20{opacity:.2}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-500{transition-duration:.5s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[overflow-wrap\:break-word\]{overflow-wrap:break-word}.togglable-inverse,.toggled .togglable{display:none}.toggled .togglable-inverse{display:inline}.trick>.trick-info{max-width:0;max-height:0;overflow:hidden;opacity:0}.trick:hover>.trick-info{max-width:2000px;max-height:300px;opacity:1}.last\:mb-0:last-child{margin-bottom:0}.hover\:w-full:hover{width:100%}.hover\:grow:hover{flex-grow:1}.hover\:border-neutral-300:hover{--tw-border-opacity:1;border-color:rgb(212 212 212/var(--tw-border-opacity))}.hover\:bg-neutral-800:hover{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.hover\:bg-green-500:hover{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity))}.hover\:bg-neutral-900:hover{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.hover\:bg-green-400:hover{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}@media (min-width:768px){.md\:flex-row{flex-direction:row}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}.md\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:max-w-screen-md{max-width:768px}}@media (min-width:1280px){.xl\:max-w-screen-lg{max-width:1024px}}@media (min-width:1536px){.\32xl\:max-w-screen-xl{max-width:1280px}}
\ No newline at end of file
diff --git a/frontends/http/tailwind.config.js b/frontends/http/tailwind.config.js
new file mode 100644
index 0000000..7c10ed8
--- /dev/null
+++ b/frontends/http/tailwind.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ content: [
+ "*.{html,rkt}"
+ ]
+}
diff --git a/frontends/http/tricks.html b/frontends/http/tricks.html
new file mode 100644
index 0000000..02bb9e3
--- /dev/null
+++ b/frontends/http/tricks.html
@@ -0,0 +1,109 @@
+@; No inputs
+@(define popular (send (current-backend) popular))
+<div class="p-12">
+ <h1 class="text-3xl font-bold mb-6">Add trick</h1>
+ <form action="/tricks/submit" method="post" target="_blank" enctype="multipart/form-data">
+ <div class="mb-3">
+ <div class="flex flex-col md:flex-row -m-3">
+ <div class="grow shrink basis-0 p-3">
+ <label class="mb-2 font-bold block" for="name">Trick Name</label>
+ <input class="p-2 border-neutral-100 hover:border-neutral-300 border rounded w-full shadow-inner"
+ type="text" id="name" name="name"></input>
+ </div>
+ <div class="grow shrink basis-0 p-3">
+ <label class="mb-2 font-bold block" for="password">Password</label>
+ <input class="p-2 border-neutral-100 hover:border-neutral-300 border rounded w-full shadow-inner"
+ type="password" id="password" name="password"></input>
+ </div>
+ </div>
+ </div>
+ <div class="mb-3">
+ <label class="mb-2 font-bold block" for="code">Code</label>
+ <textarea
+ class="font-mono p-2 border-neutral-100 hover:border-neutral-300 border rounded w-full shadow-inner"
+ id="code"
+ name="code"
+ rows="5"
+ cols="60"
+ placeholder="Enter code"
+ spellcheck="false"></textarea>
+ </div>
+ <div>
+ <input class="p-2 bg-green-400 hover:bg-green-500 rounded text-white content-center px-4 py-2 cursor-pointer"
+ type="submit" value="Add"/>
+ </div>
+ </form>
+</div>
+<div class="p-12">
+ <h1 class="text-3xl font-bold mb-6">Registered tricks</h1>
+ <script>
+ let lastTrick = null;
+ function setTrick(trick, soft) {
+ if (trick === null) trick = lastTrick;
+
+ let invocations = trick ? trick.dataset.trickInvocations : "-";
+ document.getElementById("trick-invocations").innerText = invocations;
+
+ let trickSource = document.getElementById("trick-source");
+ let trickName = document.getElementById("trick-name");
+ let trickCall = document.getElementById("trick-call");
+ if (trick) {
+ let name = trick.dataset.trickName;
+ trickSource.href = "/tricks/"+name+".rkt";
+ trickCall.action = "/tricks/"+name;
+ trickName.innerText = name;
+ if (!soft) {
+ if (trick === lastTrick) return;
+ if (lastTrick) {
+ lastTrick.classList.remove("bg-neutral-900");
+ lastTrick.classList.add("bg-neutral-700");
+ }
+ lastTrick = trick;
+ lastTrick.classList.add("bg-neutral-900");
+ lastTrick.classList.remove("bg-neutral-700");
+ }
+ } else {
+ trickSource.href = "";
+ trickCall.action = "";
+ trickName.innerText = "-";
+ }
+ }
+ </script>
+ <div class="mb-6 shadow-lg rounded-lg p-6">
+ <div class="bg-neutral-800 rounded-t-lg w-[calc(100% + 3rem)] -m-6 mb-3 px-6 py-4 text-white md:text-lg font-bold">
+ <span>Trick Name: </span>
+ <code id="trick-name">-</code>
+ </div>
+ <p class="mb-3"><span class="font-bold">Invocations</span>: <span id="trick-invocations">-</span></p>
+ <p class="mb-3"><a class="text-blue-500 hover:text-blue-700 cursor-pointer" id="trick-source">Trick Source</a></p>
+ <form id="trick-call" target="_blank" method="get">
+ <div class="mb-3">
+ <textarea class="font-mono p-2 border-neutral-100 hover:border-neutral-300 border rounded w-full shadow-inner"
+ name="args"
+ spellcheck="false"
+ rows="1"
+ placeholder="Enter arguments"></textarea>
+ </div>
+ <input class="p-2 bg-green-400 hover:bg-green-500 rounded text-white content-center px-4 py-2 cursor-pointer"
+ type="submit" value="Call"/>
+ </form>
+ </div>
+ <div id="trick-list" class="flex flex-wrap">
+ @in[trick-pair (in-list popular)]{
+ @(define name (xexpr->string (car trick-pair)))
+ @(define trick-v (cdr trick-pair))
+ <button class="mr-2 mb-2 shadow px-3 py-2 bg-neutral-700 hover:bg-neutral-800 rounded text-white trick"
+ data-trick-name="@|name|"
+ data-trick-invocations="@(trick-invocations trick-v)"
+ onmouseover="setTrick(this, true)"
+ onmouseout="setTrick(null, true)"
+ onclick="setTrick(this, false)">
+ <code class="card-header-title has-text-white">@|name|</code>
+ </button>
+ }
+ </div>
+ <script>
+ let trickList = document.getElementById("trick-list");
+ if (trickList.children.length) setTrick(trickList.children[0], false);
+ </script>
+</div>
diff --git a/interface.rkt b/interface.rkt
index 14c952a..42279fa 100644
--- a/interface.rkt
+++ b/interface.rkt
@@ -40,21 +40,34 @@
(define r16-backend<%>
(interface ()
;; evaluate a code snippet, returning either an error message or a run result
- [evaluate (#;code string? . ->m . (result/c run-result? string?))]
+ [evaluate (#;code string? . ->m .
+ (result/c run-result?
+ (error/c)))]
- ;; call a trick with arguments, returning either an error message or a run result
- [call (#;trick string? #;args string? . ->m . (result/c run-result? string?))]
+ ;; call a trick with arguments, returning either an error or a run result
+ [call (#;trick string? #;args string? . ->m .
+ (result/c run-result?
+ (error/c (no-such-trick))))]
;; delete a trick, returning an error or success message
- [delete (#;trick string? . ->m . (result/c string? string?))]
+ [delete (#;trick string? . ->m .
+ (result/c string?
+ (error/c (no-such-trick)
+ (missing-permissions))))]
;; register a trick, returning an error or success message
[register (#;trick string? #;code string?
- #;author string? #;timestamp string?
- . ->m . (result/c string? string?))]
+ #;author string? #;timestamp string? . ->m .
+ (result/c string?
+ (error/c (needs-body)
+ (missing-permissions))))]
;; update a trick, returning an error or success message
- [update (#;trick string? #;code string? . ->m . (result/c string? string?))]
+ [update (#;trick string? #;code string? . ->m .
+ (result/c string?
+ (error/c (no-such-trick)
+ (needs-body)
+ (missing-permissions))))]
;; look up a trick by name
[lookup (#;trick string? . ->m . (or/c trick? #f))]
diff --git a/result.rkt b/result.rkt
index 2f4f8d0..18bc257 100644
--- a/result.rkt
+++ b/result.rkt
@@ -2,7 +2,8 @@
(require racket/contract (for-syntax racket/base syntax/parse))
-(provide result/c ok? err? result-case)
+(provide result/c ok? err? result-case
+ error/c)
;; represents a fallible computation
;; (cons 'ok ok-value) on success
@@ -17,6 +18,19 @@
(cons/c 'err err-ctc))
'name)))]))
+;; represents an error, a message tagged with a type
+;; (cons message . case)
+(define-syntax (error/c stx)
+ (syntax-parse stx
+ [(_ (tag:id others ...) ...)
+ (with-syntax ([name stx])
+ (syntax/loc stx
+ (rename-contract
+ (cons/c string?
+ (or/c (list/c 'tag others ...)
+ ...))
+ 'name)))]))
+
(define (ok? x) (eq? 'ok (car x)))
(define (err? x) (eq? 'err (car x)))
(define (result-case if-ok if-err x)
--
2.36.1