@rip-lang/server 1.3.80 → 1.3.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/default.rip +189 -4
  2. package/package.json +1 -1
package/default.rip CHANGED
@@ -9,6 +9,7 @@
9
9
  # - Serves static files with auto-detected MIME types
10
10
  # - Directories with index.html serve the index.html
11
11
  # - Directories without index.html show a browsable file listing
12
+ # - Hover preview for images and PDFs
12
13
  # - Rip browser bundle available at /rip/rip.min.js
13
14
  # - Hot-reload via SSE for .rip, .html, and .css changes
14
15
  #
@@ -16,7 +17,7 @@
16
17
 
17
18
  import { use, start, notFound } from '@rip-lang/server'
18
19
  import { serve } from '@rip-lang/server/middleware'
19
- import { statSync, readdirSync } from 'node:fs'
20
+ import { statSync, readdirSync, readFileSync } from 'node:fs'
20
21
  import { join, resolve, basename } from 'node:path'
21
22
 
22
23
  root = resolve(process.env.APP_BASE_DIR or process.cwd())
@@ -56,11 +57,17 @@ EXT_ICONS =
56
57
  mp3: '🎵', ogg: '🎵', wav: '🎵', mp4: '🎬', webm: '🎬'
57
58
  woff: '🔤', woff2: '🔤', ttf: '🔤', otf: '🔤'
58
59
 
60
+ IMG_EXTS = new Set ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif']
61
+
59
62
  fileIcon = (name) ->
60
63
  EXT_ICONS[name.slice(name.lastIndexOf('.') + 1).toLowerCase()] or '📄'
61
64
 
62
- row = (ic, href, label, size = '', date = '') ->
63
- "<tr><td class=\"ic\">#{ic}</td><td><a href=\"#{esc href}\">#{esc label}</a></td><td class=\"sz\">#{size}</td><td class=\"dt\">#{date}</td></tr>"
65
+ previewAttr = (name, href) ->
66
+ ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase()
67
+ if IMG_EXTS.has(ext) then " data-pv=\"image\" data-src=\"#{esc href}\"" else ''
68
+
69
+ row = (ic, href, label, size = '', date = '', extra = '') ->
70
+ "<tr><td class=\"ic\">#{ic}</td><td><a href=\"#{esc href}\"#{extra}>#{esc label}</a></td><td class=\"sz\">#{size}</td><td class=\"dt\">#{date}</td></tr>"
64
71
 
65
72
  breadcrumbs = (reqPath) ->
66
73
  parts = reqPath.split('/').filter((p) -> p)
@@ -90,7 +97,7 @@ renderIndex = (reqPath, fsPath) ->
90
97
  for f in files
91
98
  try
92
99
  s = statSync(join(fsPath, f))
93
- rows.push row(fileIcon(f), f, f, fmtSize(s.size), fmtDate(s.mtimeMs))
100
+ rows.push row(fileIcon(f), f, f, fmtSize(s.size), fmtDate(s.mtimeMs), previewAttr(f, f))
94
101
  catch
95
102
  rows.push row(fileIcon(f), f, f)
96
103
 
@@ -194,11 +201,29 @@ renderIndex = (reqPath, fsPath) ->
194
201
  .sz { width: 6rem; text-align: right; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
195
202
  .dt { color: var(--muted); white-space: nowrap; }
196
203
  th.sz { text-align: right; }
204
+ .pop {
205
+ position: fixed; z-index: 9999; pointer-events: none;
206
+ width: 320px; background: var(--card);
207
+ border: 1px solid var(--border); border-radius: 12px;
208
+ box-shadow: 0 10px 30px rgba(0,0,0,.12);
209
+ overflow: hidden; opacity: 0;
210
+ transform: translateY(6px) scale(.985);
211
+ transition: opacity 150ms ease, transform 150ms ease;
212
+ }
213
+ .pop.on { opacity: 1; transform: none; }
214
+ .pop-h {
215
+ padding: 8px 14px; font-size: .6875rem; font-weight: 700;
216
+ letter-spacing: .06em; color: var(--muted); text-transform: uppercase;
217
+ border-bottom: 1px solid var(--border-row); background: var(--head-bg);
218
+ }
219
+ .pop-b { display: flex; align-items: center; justify-content: center; min-height: 100px; }
220
+ .pop-b img { display: block; max-width: 100%; max-height: 240px; }
197
221
  @media (max-width: 640px) {
198
222
  .wrap { margin: 1rem auto; }
199
223
  td, th { padding: .5rem .75rem; }
200
224
  .dt, th.dt { display: none; }
201
225
  .bar { padding: .75rem 1rem; }
226
+ .pop { display: none; }
202
227
  }
203
228
  </style>
204
229
  </head>
@@ -217,6 +242,162 @@ renderIndex = (reqPath, fsPath) ->
217
242
  </table>
218
243
  </div>
219
244
  </div>
245
+ <script>
246
+ (function(){
247
+ var pop,cur,ox=16,oy=16;
248
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
249
+ function base(s){return decodeURIComponent(s.split('?')[0].split('/').pop())}
250
+ function mk(){
251
+ if(pop)return pop;
252
+ pop=document.createElement('div');pop.className='pop';
253
+ pop.innerHTML='<div class="pop-h"></div><div class="pop-b"></div>';
254
+ document.body.appendChild(pop);return pop;
255
+ }
256
+ function show(a,e){
257
+ var t=a.dataset.pv,s=a.dataset.src;if(!t||!s)return;
258
+ var el=mk(),h=el.firstChild,b=el.lastChild;
259
+ h.textContent='PREVIEW';b.innerHTML='';
260
+ var img=new Image();img.src=s;img.alt=base(s);b.appendChild(img);
261
+ cur=a;el.classList.add('on');place(e.clientX,e.clientY);
262
+ }
263
+ function hide(){cur=null;if(pop)pop.classList.remove('on')}
264
+ function place(x,y){
265
+ if(!pop)return;var r=pop.getBoundingClientRect(),p=12,l=x+ox,t=y+oy;
266
+ if(l+r.width>innerWidth-p)l=x-r.width-ox;
267
+ if(t+r.height>innerHeight-p)t=innerHeight-r.height-p;
268
+ if(l<p)l=p;if(t<p)t=p;
269
+ pop.style.left=l+'px';pop.style.top=t+'px';
270
+ }
271
+ document.addEventListener('mouseover',function(e){var a=e.target.closest('[data-pv]');if(a)show(a,e)});
272
+ document.addEventListener('mousemove',function(e){if(cur)place(e.clientX,e.clientY)});
273
+ document.addEventListener('mouseout',function(e){var a=e.target.closest('[data-pv]');if(a&&a===cur)hide()});
274
+ document.addEventListener('scroll',hide,{passive:true});
275
+ window.addEventListener('blur',hide);
276
+ })();
277
+ </script>
278
+ </body>
279
+ </html>
280
+ """
281
+
282
+ renderMarkdown = (reqPath, fsPath) ->
283
+ md = readFileSync(fsPath, 'utf8')
284
+ body = Bun.markdown.html(md)
285
+ name = esc(basename(fsPath))
286
+ """
287
+ <!DOCTYPE html>
288
+ <html lang="en">
289
+ <head>
290
+ <meta charset="UTF-8">
291
+ <meta name="viewport" content="width=device-width, initial-scale=1">
292
+ <title>#{name}</title>
293
+ <style>
294
+ :root {
295
+ --bg: #f3f6f7; --card: #fff; --border: #d1d9e0; --border-subtle: #d1d9e0b3;
296
+ --text: #1f2328; --muted: #59636e; --link: #0969da;
297
+ --head-bg: #fafbfc; --sep: #cbd5e1;
298
+ --code-bg: #f6f8fa; --code-inline: #818b981f;
299
+ --table-alt: #f6f8fa; --hr: #d1d9e0;
300
+ --mark-bg: #fff8c5; --mark-text: #1f2328;
301
+ }
302
+ @media (prefers-color-scheme: dark) {
303
+ :root {
304
+ --bg: #0d1117; --card: #161b22; --border: #30363d; --border-subtle: #30363db3;
305
+ --text: #e6edf3; --muted: #8b949e; --link: #58a6ff;
306
+ --head-bg: #1c2129; --sep: #484f58;
307
+ --code-bg: #161b22; --code-inline: #b1bac41f;
308
+ --table-alt: #161b22; --hr: #3d444d;
309
+ --mark-bg: #bb800926; --mark-text: #e6edf3;
310
+ }
311
+ }
312
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
313
+ body {
314
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
315
+ font-size: 16px; line-height: 1.5; color: var(--text); background: var(--bg);
316
+ word-wrap: break-word;
317
+ }
318
+ .wrap { max-width: 820px; margin: 2.5rem auto; padding: 0 1rem; }
319
+ .card {
320
+ background: var(--card); border: 1px solid var(--border); border-radius: 8px;
321
+ overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
322
+ }
323
+ .bar {
324
+ display: flex; align-items: center; justify-content: space-between;
325
+ padding: .875rem 1.25rem; border-bottom: 1px solid var(--border);
326
+ }
327
+ .crumbs { font-size: .9375rem; font-weight: 500; }
328
+ .crumbs a { color: var(--link); text-decoration: none; }
329
+ .crumbs a:hover { text-decoration: underline; }
330
+ .sep { color: var(--sep); margin: 0 .3rem; font-weight: 400; }
331
+ article { padding: 2rem 2.5rem; }
332
+ article > *:first-child { margin-top: 0 !important; }
333
+ article > *:last-child { margin-bottom: 0 !important; }
334
+ article h1, article h2, article h3, article h4, article h5, article h6 {
335
+ margin-top: 1.5rem; margin-bottom: 1rem; font-weight: 600; line-height: 1.25;
336
+ }
337
+ article h1 { font-size: 2em; padding-bottom: .3em; border-bottom: 1px solid var(--border-subtle); }
338
+ article h2 { font-size: 1.5em; padding-bottom: .3em; border-bottom: 1px solid var(--border-subtle); }
339
+ article h3 { font-size: 1.25em; }
340
+ article h4 { font-size: 1em; }
341
+ article h5 { font-size: .875em; }
342
+ article h6 { font-size: .85em; color: var(--muted); }
343
+ article p, article blockquote, article ul, article ol, article dl, article table, article pre, article details {
344
+ margin-top: 0; margin-bottom: 1rem;
345
+ }
346
+ article a { color: var(--link); text-decoration: none; }
347
+ article a:hover { text-decoration: underline; }
348
+ article strong { font-weight: 600; }
349
+ article img { max-width: 100%; box-sizing: content-box; border-style: none; }
350
+ article ul, article ol { padding-left: 2em; }
351
+ article ol ol, article ul ol { list-style-type: lower-roman; }
352
+ article ul ul ol, article ul ol ol, article ol ul ol, article ol ol ol { list-style-type: lower-alpha; }
353
+ article ul ul, article ul ol, article ol ol, article ol ul { margin-top: 0; margin-bottom: 0; }
354
+ article li + li { margin-top: .25em; }
355
+ article li > p { margin-top: 1rem; }
356
+ article li > input[type="checkbox"] { margin-right: .5rem; }
357
+ article blockquote { padding: 0 1em; color: var(--muted); border-left: .25em solid var(--border); }
358
+ article blockquote > :first-child { margin-top: 0; }
359
+ article blockquote > :last-child { margin-bottom: 0; }
360
+ article mark { background-color: var(--mark-bg); color: var(--mark-text); }
361
+ article code, article tt {
362
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
363
+ font-size: 85%; padding: .2em .4em; margin: 0;
364
+ white-space: break-spaces; background-color: var(--code-inline); border-radius: 6px;
365
+ }
366
+ article pre {
367
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
368
+ font-size: 85%; line-height: 1.45; padding: 1rem;
369
+ overflow: auto; background-color: var(--code-bg); border: 1px solid var(--border); border-radius: 6px;
370
+ }
371
+ article pre code, article pre tt {
372
+ display: inline; padding: 0; margin: 0; overflow: visible;
373
+ line-height: inherit; word-wrap: normal; background-color: transparent;
374
+ border: 0; font-size: 100%; white-space: pre;
375
+ }
376
+ article table { display: block; width: max-content; max-width: 100%; overflow: auto; border-spacing: 0; border-collapse: collapse; }
377
+ article table th { font-weight: 600; }
378
+ article table th, article table td { padding: 6px 13px; border: 1px solid var(--border); }
379
+ article table tr { background-color: var(--card); border-top: 1px solid var(--border-subtle); }
380
+ article table tr:nth-child(2n) { background-color: var(--table-alt); }
381
+ article table td > :last-child { margin-bottom: 0; }
382
+ article hr {
383
+ height: .25em; padding: 0; margin: 1.5rem 0;
384
+ background-color: var(--hr); border: 0; overflow: hidden;
385
+ }
386
+ article del { color: var(--muted); }
387
+ article dl dt { padding: 0; margin-top: 1rem; font-size: 1em; font-style: italic; font-weight: 600; }
388
+ article dl dd { padding: 0 1rem; margin-bottom: 1rem; }
389
+ @media (max-width: 640px) { article { padding: 1.25rem 1rem; } .wrap { margin: 1rem auto; } }
390
+ </style>
391
+ </head>
392
+ <body>
393
+ <div class="wrap">
394
+ <div class="card">
395
+ <div class="bar">
396
+ <div class="crumbs">#{breadcrumbs reqPath}</div>
397
+ </div>
398
+ <article>#{body}</article>
399
+ </div>
400
+ </div>
220
401
  </body>
221
402
  </html>
222
403
  """
@@ -236,6 +417,10 @@ notFound ->
236
417
  return new Response 'Not Found', { status: 404 }
237
418
 
238
419
  if stat.isFile()
420
+ if path.endsWith('.md')
421
+ try
422
+ html = renderMarkdown(reqPath, path)
423
+ return new Response html, { headers: { 'Content-Type': 'text/html; charset=UTF-8' } }
239
424
  return @send path
240
425
 
241
426
  if stat.isDirectory()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/server",
3
- "version": "1.3.80",
3
+ "version": "1.3.81",
4
4
  "description": "Pure Rip web framework and application server",
5
5
  "type": "module",
6
6
  "main": "api.rip",