| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 71ccdac commit cf11402
8 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -302,6 +302,7 @@ If a platform's native libraries aren't available at runtime, the tray silently | |||
| 302 | 302 | ||
| 303 | 303 | | Example | What it shows | | |
| 304 | 304 | |---|---| | |
| 305 | + | [`examples/showcase/`](examples/showcase/) | **Kitchen sink (start here)** — every capability in one window | | ||
| 305 | 306 | | [`examples/hello/`](examples/hello/) | Minimal app — `run-app` in 8 lines | | |
| 306 | 307 | | [`examples/counter/`](examples/counter/) | JS↔Racket bridge — `fetch` calls Racket state | | |
| 307 | 308 | | [`examples/webview-demo.rkt`](examples/webview-demo.rkt) | Webview lifecycle: load, navigate, close, verification APIs | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -294,6 +294,7 @@ Glaze 提供跨平台的系统托盘,让你的应用驻留在通知区 / 菜 | |||
| 294 | 294 | ||
| 295 | 295 | | 示例 | 展示内容 | | |
| 296 | 296 | |---|---| | |
| 297 | + | [`examples/showcase/`](examples/showcase/) | **综合演示(推荐先看)** —— 全部能力一屏尽览 | | ||
| 297 | 298 | | [`examples/hello/`](examples/hello/) | 最小应用 —— 8 行 `run-app` | | |
| 298 | 299 | | [`examples/counter/`](examples/counter/) | JS↔Racket 桥接 —— `fetch` 调用 Racket 状态 | | |
| 299 | 300 | | [`examples/webview-demo.rkt`](examples/webview-demo.rkt) | WebView 生命周期:加载、导航、关闭、验证 API | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,20 @@ | |||
| 1 | + # Glaze Examples — 功能索引 | ||
| 2 | + | ||
| 3 | + | 示例 | 一句话 | 展示的能力 | | ||
| 4 | + |---|---|---| | ||
| 5 | + | [`showcase/`](showcase/) | **一屏看尽全部能力(推荐先看)** | 宏路由全形态(类型校验/400/:path/500)、SSE 事件流 + 后端错误回流(on-error)、系统功能(剪贴板/通知/Finder/窗口控制)、Agent 验证(title/url/capture + 截图回传)、API token(401 演示)、更新检查、托盘、单实例 | | ||
| 6 | + | [`hello/`](hello/) | 8 行最小应用 | run-app 一键入口、静态页面 | | ||
| 7 | + | [`counter/`](counter/) | JS↔Racket 桥接主打 | define-api-routes、SSE 广播驱动 UI、api.js 生成客户端、模块可组合(provide api/bus) | | ||
| 8 | + | [`webview-demo.rkt`](webview-demo.rkt) | WebView 生命周期 | 加载/导航/关闭/on-close/验证 API 实时打印、看门狗 | | ||
| 9 | + | [`agent-verify.rkt`](agent-verify.rkt) | 无人值守验证 | agent 工作流:轮询断言 + 截图 + 退出码 | | ||
| 10 | + | [`tray-demo.rkt`](tray-demo.rkt) | 跨平台托盘 | make-tray/菜单/tooltip 动态更新 | | ||
| 11 | + | ||
| 12 | + ## 快速开始 | ||
| 13 | + | ||
| 14 | + ```bash | ||
| 15 | + racket examples/showcase/main.rkt # 综合演示(单实例锁定) | ||
| 16 | + racket examples/counter/main.rkt # 桥接 + 事件 | ||
| 17 | + racket examples/hello/main.rkt # 最小应用 | ||
| 18 | + ``` | ||
| 19 | + | ||
| 20 | + 所有示例均可 `raco glaze build` 打包为独立应用。 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,131 @@ | |||
| 1 | + #lang racket/base | ||
| 2 | + | ||
| 3 | + ;; Glaze Showcase — one window, every capability, each tab wired to the | ||
| 4 | + ;; real backend. Run: racket examples/showcase/main.rkt | ||
| 5 | + | ||
| 6 | + (require racket/file | ||
| 7 | + racket/runtime-path | ||
| 8 | + glaze) | ||
| 9 | + | ||
| 10 | + (define-runtime-path public "public") | ||
| 11 | + | ||
| 12 | + ;; ---- state & services ---------------------------------------------------- | ||
| 13 | + (define bus (make-event-bus)) | ||
| 14 | + (define wv-box (box #f)) | ||
| 15 | + (define wide? (box #f)) | ||
| 16 | + (define hits (box 0)) | ||
| 17 | + | ||
| 18 | + ;; 1 Hz event stream (SSE tab) | ||
| 19 | + (void (thread (lambda () | ||
| 20 | + (let loop () | ||
| 21 | + (sleep 1) | ||
| 22 | + (bus-broadcast! bus 'tick (hasheq 'now (current-inexact-milliseconds))) | ||
| 23 | + (loop))))) | ||
| 24 | + | ||
| 25 | + ;; error reporting: handler exceptions flow back to the page as events | ||
| 26 | + (define (report-error! exn uri) | ||
| 27 | + (bus-broadcast! bus 'backend-error | ||
| 28 | + (hasheq 'uri uri 'message (exn-message exn)))) | ||
| 29 | + | ||
| 30 | + ;; ---- routes: every define-api-routes shape -------------------------------- | ||
| 31 | + (define-api-routes api | ||
| 32 | + ;; typed params with defaults; bad input -> 400 naming the parameter | ||
| 33 | + [(POST "api/add") | ||
| 34 | + (add [a exact-integer?] [b exact-integer? 1]) | ||
| 35 | + (begin (set-box! hits (add1 (unbox hits))) | ||
| 36 | + (hasheq 'sum (+ a b) 'hits (unbox hits)))] | ||
| 37 | + ;; :path params arrive as plain arguments | ||
| 38 | + [(GET "api/echo/:msg") | ||
| 39 | + (echo msg) | ||
| 40 | + (hasheq 'echo msg 'len (string-length msg))] | ||
| 41 | + ;; handler exceptions -> 500 + the on-error hook (see run-app below) | ||
| 42 | + [(POST "api/boom") | ||
| 43 | + (boom) | ||
| 44 | + (raise-user-error 'showcase "boom: 数据处理故意失败 (演示 500 + 错误上报)")] | ||
| 45 | + ;; system integration | ||
| 46 | + [(POST "api/clip-write") | ||
| 47 | + (clip-write text) | ||
| 48 | + (hasheq 'ok (clipboard-set! text))] | ||
| 49 | + [(POST "api/clip-read") | ||
| 50 | + (clip-read) | ||
| 51 | + (hasheq 'text (clipboard-get))] | ||
| 52 | + [(POST "api/notify") | ||
| 53 | + (do-notify title body) | ||
| 54 | + (hasheq 'ok (notify! title body #:subtitle "showcase"))] | ||
| 55 | + [(POST "api/reveal") | ||
| 56 | + (do-reveal) | ||
| 57 | + (hasheq 'ok (reveal-path (path->string (find-system-path 'run-file))))] | ||
| 58 | + ;; window controls (handle arrives via #:on-ready) | ||
| 59 | + [(POST "api/win-title") | ||
| 60 | + (win-title n) | ||
| 61 | + (let ([t (format "Showcase ~a" n)]) | ||
| 62 | + (and (unbox wv-box) (webview-set-title! (unbox wv-box) t)) | ||
| 63 | + (hasheq 'title t))] | ||
| 64 | + [(POST "api/win-size") | ||
| 65 | + (win-size) | ||
| 66 | + (let* ([w? (not (unbox wide?))]) | ||
| 67 | + (set-box! wide? w?) | ||
| 68 | + (and (unbox wv-box) | ||
| 69 | + (webview-set-size! (unbox wv-box) (if w? 1080 860) (if w? 700 560))) | ||
| 70 | + (hasheq 'size (if w? "1080x700" "860x560")))] | ||
| 71 | + ;; verification APIs: the agent workflow, on demand | ||
| 72 | + [(POST "api/self-check") | ||
| 73 | + (self-check) | ||
| 74 | + (let* ([wv (unbox wv-box)] | ||
| 75 | + [shot (and wv (webview-capture! wv (build-path public "shots" "latest.png")))] | ||
| 76 | + [pass? (and wv | ||
| 77 | + (equal? (webview-title wv) "Glaze Showcase") | ||
| 78 | + shot | ||
| 79 | + (>= (file-size shot) 5000))]) | ||
| 80 | + (hasheq 'ok (and pass? #t) | ||
| 81 | + 'title (and wv (webview-title wv)) | ||
| 82 | + 'url (and wv (webview-url wv)) | ||
| 83 | + 'shot-bytes (and shot (file-size shot))))] | ||
| 84 | + ;; update check against a local manifest (v99 > current) | ||
| 85 | + [(POST "api/check-update") | ||
| 86 | + (check-update-now) | ||
| 87 | + (let ([info (check-update "http://127.0.0.1:18952/manifest.json" | ||
| 88 | + #:current-version "0.3.0")]) | ||
| 89 | + (hasheq 'found (and info #t) 'info info))] | ||
| 90 | + ;; token-guarded secret (guarded by #:api-token below) | ||
| 91 | + [(POST "api/secret") | ||
| 92 | + (secret) | ||
| 93 | + (hasheq 'secret "the cake is a lie")]) | ||
| 94 | + | ||
| 95 | + ;; a deliberately hostile manifest for the update tab | ||
| 96 | + (call-with-output-file (build-path public "manifest.json") | ||
| 97 | + (lambda (o) | ||
| 98 | + (write-bytes (string->bytes/utf-8 | ||
| 99 | + "{\"version\":\"99.0.0\",\"url\":\"https://example.com/99\",\"notes\":\"showcase manifest\"}") o)) | ||
| 100 | + #:exists 'replace) | ||
| 101 | + | ||
| 102 | + (module+ main | ||
| 103 | + (unless (single-instance? "glaze-showcase") | ||
| 104 | + (displayln "[showcase] another instance is running; exiting") | ||
| 105 | + (exit 1)) | ||
| 106 | + | ||
| 107 | + ;; tray: About re-focuses the window, Quit exits | ||
| 108 | + (define tray | ||
| 109 | + (make-tray #:icon #f | ||
| 110 | + #:tooltip "Glaze Showcase" | ||
| 111 | + #:menu (list | ||
| 112 | + (make-menu-item "关于 / About" | ||
| 113 | + #:action (lambda () | ||
| 114 | + (and (unbox wv-box) | ||
| 115 | + (webview-set-title! | ||
| 116 | + (unbox wv-box) | ||
| 117 | + "Glaze Showcase")))) | ||
| 118 | + (menu-separator) | ||
| 119 | + (make-menu-item "退出 / Quit" | ||
| 120 | + #:action (lambda () (exit 0)))))) | ||
| 121 | + | ||
| 122 | + (run-app | ||
| 123 | + #:public-dir public | ||
| 124 | + #:api api | ||
| 125 | + #:events bus | ||
| 126 | + #:api-token "showcase-demo-token" | ||
| 127 | + #:title "Glaze Showcase" | ||
| 128 | + #:width 860 #:height 560 | ||
| 129 | + #:port 18952 | ||
| 130 | + #:on-error report-error! | ||
| 131 | + #:on-ready (lambda (wv url) (when wv (set-box! wv-box wv))))) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,120 @@ | |||
| 1 | + <!DOCTYPE html> | ||
| 2 | + <html> | ||
| 3 | + <head> | ||
| 4 | + <meta charset="utf-8"> | ||
| 5 | + <title>Glaze Showcase</title> | ||
| 6 | + <style> | ||
| 7 | + :root{--bg:#F4F3EE;--ink:#2d2a26;--sub:#6b675f;--acc:#C15F3C;--line:#e2ded4} | ||
| 8 | + *{box-sizing:border-box} | ||
| 9 | + body{font-family:-apple-system,sans-serif;background:var(--bg);color:var(--ink);margin:0;height:100vh;display:flex;flex-direction:column} | ||
| 10 | + header{padding:10px 16px;border-bottom:1px solid var(--line);display:flex;align-items:baseline;gap:10px} | ||
| 11 | + header h1{font-size:16px;margin:0} header em{color:var(--acc);font-style:normal;font-weight:800} | ||
| 12 | + header .sp{flex:1} #tick{font-size:11px;color:var(--sub);font-family:ui-monospace,Menlo,monospace} | ||
| 13 | + nav{display:flex;border-bottom:1px solid var(--line);background:#efede7} | ||
| 14 | + nav div{padding:8px 14px;font-size:13px;cursor:pointer;color:var(--sub)} | ||
| 15 | + nav div.on{color:var(--ink);border-bottom:2px solid var(--acc);font-weight:700} | ||
| 16 | + main{flex:1;overflow:auto;padding:16px} | ||
| 17 | + .tab{display:none}.tab.on{display:block} | ||
| 18 | + .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0} | ||
| 19 | + button{background:var(--acc);color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer} | ||
| 20 | + button.alt{background:#d8d4c8;color:var(--ink)} | ||
| 21 | + input{padding:8px 10px;border:1px solid #c9c4b8;border-radius:8px;font-size:13px;width:110px} | ||
| 22 | + .log{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;font:12px ui-monospace,Menlo,monospace; | ||
| 23 | + color:#4a463f;min-height:70px;max-height:180px;overflow:auto;white-space:pre-wrap;margin-top:10px} | ||
| 24 | + .err{color:#b3402a}.ok{color:#5d7f5b} | ||
| 25 | + .note{font-size:12px;color:var(--sub);margin:6px 0} | ||
| 26 | + img.shot{max-width:100%;border:1px solid var(--line);border-radius:10px;margin-top:10px;background:#fff} | ||
| 27 | + .big{font-size:34px;font-weight:800;color:var(--acc)} | ||
| 28 | + </style> | ||
| 29 | + </head> | ||
| 30 | + <body> | ||
| 31 | + <header><h1>Glaze <em>Showcase</em></h1><span class="note">每个标签都是真实后端调用</span> | ||
| 32 | + <span class="sp"></span><span id="tick">SSE 未连接</span></header> | ||
| 33 | + <nav id="nav"> | ||
| 34 | + <div class="on" data-t="bridge">桥接 · 宏路由</div> | ||
| 35 | + <div data-t="events">事件 · SSE</div> | ||
| 36 | + <div data-t="sys">系统功能</div> | ||
| 37 | + <div data-t="verify">验证 · Agent</div> | ||
| 38 | + <div data-t="secure">安全 · 更新</div> | ||
| 39 | + </nav> | ||
| 40 | + <main> | ||
| 41 | + <!-- 桥接 --> | ||
| 42 | + <section class="tab on" id="bridge"> | ||
| 43 | + <div class="row"><input id="a" value="17"> + <input id="b" value="25"> | ||
| 44 | + <button onclick="doAdd()">求和(类型校验)</button> | ||
| 45 | + <button class="alt" onclick="doAdd(true)">传错误类型 → 400</button></div> | ||
| 46 | + <div class="row"><input id="msg" value="hello-glaze" style="width:180px"> | ||
| 47 | + <button class="alt" onclick="doEcho()">:path 参数 echo</button></div> | ||
| 48 | + <div class="row"><button class="alt" onclick="doBoom()">触发 500(看错误回流)</button></div> | ||
| 49 | + <div class="note">错误会被后端 on-error 捕获并经 SSE 推回本页(见“事件”标签与下方日志)</div> | ||
| 50 | + <div class="log" id="blog"></div> | ||
| 51 | + </section> | ||
| 52 | + <!-- 事件 --> | ||
| 53 | + <section class="tab" id="events"> | ||
| 54 | + <div class="row"><button onclick="glaze.api.add({a:1,b:1}).then(()=>log('elog','一次后端广播已触发(见右上角时钟与系统通知)'))">后端推一条事件</button> | ||
| 55 | + <button class="alt" onclick="glaze.api.notify({title:'Showcase',body:'SSE 事件驱动'})">同时发通知</button></div> | ||
| 56 | + <div class="note">右上角时间戳由后端 1Hz 广播驱动;下面是原始事件流:</div> | ||
| 57 | + <div class="log" id="elog"></div> | ||
| 58 | + </section> | ||
| 59 | + <!-- 系统 --> | ||
| 60 | + <section class="tab" id="sys"> | ||
| 61 | + <div class="row"><input id="cliptext" value="showcase 剪贴板" style="width:200px"> | ||
| 62 | + <button onclick="glaze.api.clipWrite({text:cli()}).then(r=>log('slog','剪贴板写入 '+r.ok))">写剪贴板</button> | ||
| 63 | + <button class="alt" onclick="glaze.api.clipRead().then(r=>log('slog','剪贴板内容: '+JSON.stringify(r.text)))">读剪贴板</button></div> | ||
| 64 | + <div class="row"><button onclick="glaze.api.notify({title:'Showcase',body:'系统通知来了'}).then(()=>log('slog','通知已发送(看右上角)'))">系统通知</button> | ||
| 65 | + <button class="alt" onclick="glaze.api.reveal().then(r=>log('slog','Finder 定位 '+r.ok))">Finder 定位</button></div> | ||
| 66 | + <div class="row"><button class="alt" onclick="glaze.api.winTitle({n:Date.now()%1000|0}).then(r=>log('slog','标题 → '+r.title))">改窗口标题</button> | ||
| 67 | + <button class="alt" onclick="glaze.api.winSize().then(r=>log('slog','尺寸 → '+r.size))">窗口宽度切换</button></div> | ||
| 68 | + <div class="log" id="slog"></div> | ||
| 69 | + </section> | ||
| 70 | + <!-- 验证 --> | ||
| 71 | + <section class="tab" id="verify"> | ||
| 72 | + <div class="row"><button onclick="doCheck()">运行 Agent 自检</button> | ||
| 73 | + <span class="note">title / url / capture 三项断言 + 截图回传</span></div> | ||
| 74 | + <div class="log" id="vlog"></div> | ||
| 75 | + <img class="shot" id="shot" style="display:none"> | ||
| 76 | + </section> | ||
| 77 | + <!-- 安全更新 --> | ||
| 78 | + <section class="tab" id="secure"> | ||
| 79 | + <div class="row"><button onclick="glaze.api.secret().then(r=>log('glog','token 路由(cookie 自动授权): '+JSON.stringify(r)), | ||
| 80 | + e=>log('glog','失败: '+e.message))">调受 token 保护的路由</button> | ||
| 81 | + <button class="alt" onclick="fetch('/api/secret',{method:'POST'}).then(r=>log('glog','不带 token 直调 → HTTP '+r.status+'(401 = 守卫生效)'))">不带 token 直调 → 401</button></div> | ||
| 82 | + <div class="row"><button class="alt" onclick="glaze.api.checkUpdate().then(r=>log('glog','更新检查: '+JSON.stringify(r)))">检查更新(本地 manifest v99)</button></div> | ||
| 83 | + <div class="log" id="glog"></div> | ||
| 84 | + </section> | ||
| 85 | + </main> | ||
| 86 | + <script src="/glaze/api.js"></script> | ||
| 87 | + <script> | ||
| 88 | + const logs={blog:'#2d2a26',elog:'#2d2a26',slog:'#2d2a26',vlog:'#2d2a26',glog:'#2d2a26'}; | ||
| 89 | + function log(id,msg){const el=document.getElementById(id);el.textContent+='▸ '+msg+'\n';el.scrollTop=el.scrollHeight;} | ||
| 90 | + const cli=()=>document.getElementById('cliptext').value; | ||
| 91 | + async function doAdd(bad){ | ||
| 92 | + const a=bad?'不是数字':+document.getElementById('a').value; | ||
| 93 | + const b=+document.getElementById('b').value; | ||
| 94 | + try{const r=await glaze.api.add({a,b});log('blog','a+b = '+r.sum+'(第 '+r.hits+' 次调用)');} | ||
| 95 | + catch(e){log('blog','400: '+e.message);} | ||
| 96 | + } | ||
| 97 | + async function doEcho(){const r=await glaze.api.echo(document.getElementById('msg').value); | ||
| 98 | + log('blog',`:path echo → ${JSON.stringify(r.echo)} (len=${r.len})`);} | ||
| 99 | + async function doBoom(){ | ||
| 100 | + try{await glaze.api.boom();}catch(e){log('blog','500 已返回: '+e.message.slice(0,60)+'…');} | ||
| 101 | + } | ||
| 102 | + async function doCheck(){ | ||
| 103 | + const r=await glaze.api.selfCheck(); | ||
| 104 | + log('vlog',`title=${JSON.stringify(r.title)} url=${r.url}`); | ||
| 105 | + log('vlog',`capture=${r.shotBytes} bytes → 全部通过: ${r.ok}`); | ||
| 106 | + const img=document.getElementById('shot');img.style.display='block'; | ||
| 107 | + img.src='/shots/latest.png?'+Date.now(); | ||
| 108 | + } | ||
| 109 | + // tabs | ||
| 110 | + document.getElementById('nav').addEventListener('click',e=>{ | ||
| 111 | + if(!e.target.dataset.t)return; | ||
| 112 | + document.querySelectorAll('nav div').forEach(d=>d.classList.toggle('on',d===e.target)); | ||
| 113 | + document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('on',t.id===e.target.dataset.t)); | ||
| 114 | + }); | ||
| 115 | + // SSE: clock + error stream | ||
| 116 | + glaze.on('tick',d=>{document.getElementById('tick').textContent='SSE · '+new Date(d.now).toLocaleTimeString();}); | ||
| 117 | + glaze.on('backend-error',d=>{log('blog','[on-error 回流] '+d.uri+': '+d.message.slice(0,50));log('elog','[error] '+d.uri);}); | ||
| 118 | + </script> | ||
| 119 | + </body> | ||
| 120 | + </html> | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1 @@ | |||
| 1 | + {"version":"99.0.0","url":"https://example.com/99","notes":"showcase manifest"} | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,6 +5,7 @@ | |||
| 5 | 5 | "api-macros.rkt" | |
| 6 | 6 | "events.rkt" | |
| 7 | 7 | "sys/main.rkt" | |
| 8 | + "update.rkt" | ||
| 8 | 9 | "browser.rkt" | |
| 9 | 10 | "assets.rkt" | |
| 10 | 11 | "build.rkt" | |
@@ -13,7 +14,8 @@ | |||
| 13 | 14 | "webview/main.rkt") | |
| 14 | 15 | ||
| 15 | 16 | (provide (all-from-out "server.rkt" "api.rkt" "api-macros.rkt" "events.rkt" | |
| 16 | - "browser.rkt" "assets.rkt" "build.rkt" "sys/main.rkt") | ||
| 17 | + "browser.rkt" "assets.rkt" "build.rkt" "sys/main.rkt" | ||
| 18 | + "update.rkt") | ||
| 17 | 19 | (all-from-out "app.rkt") | |
| 18 | 20 | (all-from-out "tray/main.rkt") | |
| 19 | 21 | (all-from-out "webview/main.rkt")) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -276,7 +276,17 @@ | |||
| 276 | 276 | "\n};\n")) | |
| 277 | 277 | ||
| 278 | 278 | ;; "api/counter/bump" -> counterBump ; "api/items/:id/bump" -> itemsIdBump | |
| 279 | - ;; The first segment keeps its case (api/bump -> bump, not Bump). | ||
| 279 | + ;; "api/clip-copy" -> clipCopy. Hyphenated segments camel-case (a bare | ||
| 280 | + ;; hyphen key like `clip-copy:` would be ILLEGAL JavaScript and break the | ||
| 281 | + ;; whole generated file); the first segment keeps a lowercase head. | ||
| 282 | + (define (js-camel seg first-lower?) | ||
| 283 | + (define parts (filter non-empty-string? (string-split seg "-"))) | ||
| 284 | + (apply string-append | ||
| 285 | + (for/list ([p (in-list parts)] [i (in-naturals)]) | ||
| 286 | + (if (and (zero? i) first-lower? (regexp-match? #rx"^[a-z]" p)) | ||
| 287 | + p | ||
| 288 | + (string-append (string-upcase (substring p 0 1)) (substring p 1)))))) | ||
| 289 | + | ||
| 280 | 290 | (define (route->js-name segments) | |
| 281 | 291 | (define drop-api | |
| 282 | 292 | (if (and (pair? segments) (string=? (first segments) "api")) | |
@@ -286,9 +296,8 @@ | |||
| 286 | 296 | (for/list ([seg (in-list drop-api)] [i (in-naturals)]) | |
| 287 | 297 | (cond | |
| 288 | 298 | [(param? seg) (string-titlecase (param-id seg))] | |
| 289 | - [(and (positive? i) (regexp-match? #rx"^[a-z]" seg)) | ||
| 290 | - (string-append (string-upcase (substring seg 0 1)) (substring seg 1))] | ||
| 291 | - [else (if (param? seg) (param-id seg) seg)])))) | ||
| 299 | + [(zero? i) (js-camel seg #t)] | ||
| 300 | + [else (js-camel seg #f)])))) | ||
| 292 | 301 | ||
| 293 | 302 | ;; Try each route against the request; on a match apply the handler and | |
| 294 | 303 | ;; normalize its result (jsexpr -> 200 JSON; response -> itself; exception -> | |
| Back | FazBrowse Home | New Git URL |
0 commit comments