~williewillus/r16

4340981d9a0b5f2a64819485f543f57a55dbcda5 — eutro 1 year, 3 months ago f07f1c9
Prototype HTTP frontend

Generalize backend, improve HTTP frontend UI

Make the UI more mobile-friendly

Templated HTML responses, and changes to how the backend handles errors

Improve whitespace handling in result presentation

Update on top of rebase, make template arguments explicit, support dynamic template reloads

Styling improvements
M backend.rkt => backend.rkt +13 -11
@@ 70,7 70,7 @@
                      enrich-context
                      (current-context-id)
                      #f "" "" #f))
      (ev:run code ev-ctx response?))
      (cons 'ok (ev:run code ev-ctx response?)))

    (define/public (call name args)
      (define ctx-id (current-context-id))


@@ 89,9 89,9 @@
                         (current-context-id)
                         trick-obj name args #f))
         (define code (trick-body trick-obj))
         (ev:run code ev-ctx response?)]
         (cons 'ok (ev:run code ev-ctx response?))]
        [else
         (~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,22 99,22 @@
      (define frontend (current-frontend))
      (cond
        [(not trick-obj)
         (~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)))
         (~a "Successfully removed trick " name "!")]
         (cons 'ok (~a "Successfully removed trick " name "!"))]
        [else
         (~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))
         (~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)))
         (~a "Successfully registered trick " name "!")]
         (cons 'ok (~a "Successfully registered trick " name "!"))]
        [else (update name code)]))

    (define/public (update name code)


@@ 123,9 123,9 @@
      (define frontend (current-frontend))
      (cond
        [(not trick-obj)
         (~a "Trick " name " doesn't exist!")]
         (list 'err (~a "Trick " name " doesn't exist!") 'no-such-trick)]
        [(zero? (string-length code))
         (~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)


@@ 136,7 136,9 @@
                   (trick-invocations trick-obj)))
          (lambda (t)
            (send frontend can-modify? t)))
         (~a "Successfully updated trick " name "!")]))
         (cons 'ok (~a "Successfully updated trick " name "!"))]
        [else
         (list 'err (~a "You cannot modify trick " name "!") 'missing-permissions)]))

    (define/public (lookup name)
      (db:get-trick db (current-context-id) name))

M frontends/discord.rkt => frontends/discord.rkt +13 -13
@@ 447,15 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)))
             (if (ev:run-result? result)
                 (format-run-result 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"


@@ 463,17 464,16 @@
            (thunk
             (define result
               (send (current-backend) call name body))
             (if (ev:run-result? result)
                 (format-run-result 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_]"
          (list
           (send (current-backend) register
                 name (strip-backticks body)
                 (message-author-id (current-message))
                 (hash-ref (current-message) 'timestamp))))
          (define result
            (send (current-backend) register
                  name (strip-backticks body)
                  (message-author-id (current-message))
                  (hash-ref (current-message) 'timestamp)))
          (list (result-case cdr cadr result)))

        (define/command/trick (show-trick name _body)
          " [_name_]:  show metadata and source for the trick [_name_]"


@@ 494,11 494,11 @@

        (define/command/trick (update-trick name body)
          " [_name_] [_code_]:  change the source of the trick [_name_]; requires ownership or administrator"
          (list (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 (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"

A frontends/http/base.html => frontends/http/base.html +43 -0
@@ 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>

A frontends/http/favicon.ico => frontends/http/favicon.ico +0 -0
A frontends/http/home.html => frontends/http/home.html +5 -0
@@ 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>

A frontends/http/icon.svg => frontends/http/icon.svg +194 -0
@@ 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>

A frontends/http/main.rkt => frontends/http/main.rkt +265 -0
@@ 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]))

A frontends/http/raw.html => frontends/http/raw.html +23 -0
@@ 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>

A frontends/http/result.html => frontends/http/result.html +40 -0
@@ 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>

A frontends/http/simple.html => frontends/http/simple.html +4 -0
@@ 0,0 1,4 @@
@; Inputs: msg
<div class="p-12">
  <span class="text-2xl font-bold mb-2">@|msg|</span>
</div>

A frontends/http/style-in.css => frontends/http/style-in.css +20 -0
@@ 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;
}

A frontends/http/style.css => frontends/http/style.css +1 -0
@@ 0,0 1,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

A frontends/http/tailwind.config.js => frontends/http/tailwind.config.js +5 -0
@@ 0,0 1,5 @@
module.exports = {
  content: [
    "*.{html,rkt}"
  ]
}

A frontends/http/tricks.html => frontends/http/tricks.html +109 -0
@@ 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>

M interface.rkt => interface.rkt +23 -8
@@ 3,13 3,15 @@
(require
 racket/class
 racket/contract
 "result.rkt"
 (only-in "evaluator.rkt" definitions? run-result?)
 "common.rkt")

(provide r16-backend? r16-frontend?
         r16-backend<%> r16-frontend<%>
         current-backend current-frontend
         current-context-id)
         current-context-id
         (all-from-out "result.rkt"))

;; an r16 frontend
(define r16-frontend<%>


@@ 38,21 40,34 @@
(define r16-backend<%>
  (interface ()
    ;; evaluate a code snippet, returning either an error message or a run result
    [evaluate (#;code string? . ->m . (or/c string? run-result?))]
    [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 . (or/c string? run-result?))]
    ;; 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 . 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 . 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 . 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))]

A result.rkt => result.rkt +37 -0
@@ 0,0 1,37 @@
#lang racket/base

(require racket/contract (for-syntax racket/base syntax/parse))

(provide result/c ok? err? result-case
         error/c)

;; represents a fallible computation
;; (cons 'ok ok-value) on success
;; (cons 'err err-value) on failure
(define-syntax (result/c stx)
  (syntax-parse stx
    [(_ ok-ctc:expr err-ctc:expr)
     (with-syntax ([name stx])
       (syntax/loc stx
         (rename-contract
          (or/c (cons/c 'ok ok-ctc)
                (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)
  ((if (ok? x) if-ok if-err) (cdr x)))