deckrun 1.3.0 → 1.4.0
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.
- package/README.md +16 -7
- package/dist/editor-content.js +1 -1
- package/dist/editor.js +130 -196
- package/dist/generate.js +8 -8
- package/dist/index.js +175 -41
- package/dist/preview.js +81 -1
- package/dist/themes.js +6 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,15 @@ Write slides in Markdown or bring a self-contained HTML document, run a local se
|
|
|
9
9
|
- **Export** - Markdown, HTML, headless-rendered PDF, or a standalone presenter-ready HTML page
|
|
10
10
|
- **Local-first** - binds only to `127.0.0.1`; nothing is uploaded, and your work stays in browser local storage until export
|
|
11
11
|
|
|
12
|
-

|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
Check out the sample Markdown source in [`examples/example-2.md`](https://raw.githubusercontent.com/arpitbbhayani/deckrun/refs/heads/master/examples/example-2.md) and load it directly to see how `deckrun` presents it (no installation required):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx deckrun
|
|
20
|
+
```
|
|
13
21
|
|
|
14
22
|
## Installation
|
|
15
23
|
|
|
@@ -23,7 +31,8 @@ Or run it without installing:
|
|
|
23
31
|
|
|
24
32
|
```bash
|
|
25
33
|
npx deckrun # open the editor
|
|
26
|
-
npx deckrun slides.md # present a file
|
|
34
|
+
npx deckrun slides.md # present a local file
|
|
35
|
+
npx deckrun <url> # present a public Markdown or HTML URL
|
|
27
36
|
```
|
|
28
37
|
|
|
29
38
|
## Usage
|
|
@@ -66,7 +75,7 @@ On start, the CLI prints the slide count and the local URL:
|
|
|
66
75
|
present → http://127.0.0.1:7890 (Ctrl+C to stop)
|
|
67
76
|
```
|
|
68
77
|
|
|
69
|
-
With no file, it starts the editor instead:
|
|
78
|
+
With no file or URL, it starts the editor instead:
|
|
70
79
|
|
|
71
80
|
```text
|
|
72
81
|
editor → http://127.0.0.1:7890 (Ctrl+C to stop)
|
|
@@ -80,11 +89,11 @@ The server binds to `127.0.0.1` only, so the deck is never exposed on the networ
|
|
|
80
89
|
|
|
81
90
|
| Option | Default | Description |
|
|
82
91
|
| --------------------- | ------- | ------------------------------------------------------ |
|
|
83
|
-
| `[file]` | | Markdown
|
|
92
|
+
| `[file]` | | Markdown file, HTML file, or public URL to present. Omit it to open the editor. |
|
|
84
93
|
| `-p, --port <number>` | `7890` | Port to serve the presentation on |
|
|
85
94
|
| `--no-open` | `false` | Start the HTTP server without opening the browser |
|
|
86
95
|
| `--fullscreen` | `false` | Prompt to enter fullscreen on the first key or click |
|
|
87
|
-
| `--theme <name>` | `
|
|
96
|
+
| `--theme <name>` | `nord` | Any of the fourteen themes, by id |
|
|
88
97
|
| `--size <name>` | `m` | Type size: `s`, `m`, `l`, or `xl` |
|
|
89
98
|
| `--head-font <name>` | | Override the theme's heading and title face |
|
|
90
99
|
| `--body-font <name>` | | Override the theme's body face |
|
|
@@ -138,7 +147,7 @@ the one you have open.
|
|
|
138
147
|
- Markdown is syntax-highlighted in place: headings by level, bold, italic, inline code, links, image directives, fences, tables, notes, and raw HTML each get their own color.
|
|
139
148
|
- Enter continues the list you are in, and continues numbering. Enter on an empty item ends the list.
|
|
140
149
|
- Tab inserts two spaces.
|
|
141
|
-
- `Alt Up`
|
|
150
|
+
- `Alt Left` / `Alt Right` (or `Alt Up` / `Alt Down`) hop the caret between slides.
|
|
142
151
|
|
|
143
152
|
### Discovering what a slide can hold
|
|
144
153
|
|
|
@@ -237,7 +246,7 @@ An HTML doc's PDF is not paginated to 16:9 slides — it prints the doc's own `@
|
|
|
237
246
|
| `Cmd E` | Inline code |
|
|
238
247
|
| `Cmd G` | Toggle grid preview |
|
|
239
248
|
| `Cmd Shift L` | Theme and type size picker |
|
|
240
|
-
| `Alt Up`, `Alt Down`| Previous and next slide
|
|
249
|
+
| `Alt Left`, `Alt Right`, `Alt Up`, `Alt Down`| Previous and next slide |
|
|
241
250
|
| `Esc` | Close a menu, the palette, or the guide |
|
|
242
251
|
|
|
243
252
|
`Cmd K` (palette), `Cmd /` (guide), `Cmd D/B/I/E` (snippets), and `Cmd G` (grid) are Markdown-only — an HTML doc's source is plain text with no snippet catalog or grid view. Library, present, and both exports stay wired the same for either kind.
|
package/dist/editor-content.js
CHANGED
|
@@ -317,7 +317,7 @@ export const TIPS = [
|
|
|
317
317
|
"Use *** for a rule inside a slide. --- would start a new one.",
|
|
318
318
|
"<kbd>Cmd</kbd> renders as a key cap, <mark>42ms</mark> as a highlight.",
|
|
319
319
|
"Cmd Enter presents the deck in a new tab, exactly as your audience sees it.",
|
|
320
|
-
"Alt
|
|
320
|
+
"Alt Left/Right or Alt Up/Down hops between slides.",
|
|
321
321
|
"Export as PDF from the top bar: a real file, 16:9 pages, no print dialog to wrestle.",
|
|
322
322
|
"Seven bullets is about the ceiling before a slide stops reading from the back row.",
|
|
323
323
|
"Drag the divider to resize. Double-click it to snap back to an even split.",
|
package/dist/editor.js
CHANGED
|
@@ -29,7 +29,7 @@ export function generateEditorHtml(theme = DEFAULT_THEME, sizeInput = DEFAULT_SI
|
|
|
29
29
|
<head>
|
|
30
30
|
<meta charset="UTF-8">
|
|
31
31
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
32
|
-
<title>deckrun editor</title>
|
|
32
|
+
<title>deckrun · editor</title>
|
|
33
33
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
34
34
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
35
35
|
<link href="${googleFontsHref()}" rel="stylesheet">
|
|
@@ -305,21 +305,30 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
305
305
|
position: absolute;
|
|
306
306
|
inset: 0;
|
|
307
307
|
margin: 0;
|
|
308
|
-
padding: 18px 20px
|
|
308
|
+
padding: 18px 20px;
|
|
309
309
|
font-family: var(--font-mono);
|
|
310
310
|
font-size: 13.5px;
|
|
311
311
|
font-weight: 400;
|
|
312
|
+
font-style: normal;
|
|
312
313
|
line-height: 1.75;
|
|
313
314
|
letter-spacing: 0;
|
|
315
|
+
word-spacing: 0;
|
|
314
316
|
white-space: pre-wrap;
|
|
315
317
|
overflow-wrap: break-word;
|
|
316
318
|
word-break: break-word;
|
|
317
319
|
tab-size: 2;
|
|
318
320
|
border: 0;
|
|
321
|
+
box-sizing: border-box;
|
|
319
322
|
background: transparent;
|
|
323
|
+
scrollbar-width: none;
|
|
324
|
+
-ms-overflow-style: none;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#hl::-webkit-scrollbar, #src::-webkit-scrollbar {
|
|
328
|
+
display: none;
|
|
320
329
|
}
|
|
321
330
|
|
|
322
|
-
#hl { z-index: 1; overflow: hidden; pointer-events: none; color: var(--subtext0);
|
|
331
|
+
#hl { z-index: 1; overflow: hidden; pointer-events: none; color: var(--subtext0); }
|
|
323
332
|
#measure { z-index: 0; visibility: hidden; overflow: hidden; }
|
|
324
333
|
|
|
325
334
|
#src {
|
|
@@ -332,38 +341,28 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
332
341
|
outline: none;
|
|
333
342
|
}
|
|
334
343
|
|
|
335
|
-
#src::selection { background: var(--
|
|
344
|
+
#src::selection { background: var(--surface2); color: transparent; }
|
|
336
345
|
|
|
337
|
-
/* Markdown tokens */
|
|
338
|
-
.t-h1 { color: var(--accent);
|
|
339
|
-
.t-h2 { color: var(--accent-2);
|
|
340
|
-
.t-h3 { color: var(--accent-3);
|
|
346
|
+
/* Markdown tokens — color only to guarantee pixel-identical alignment */
|
|
347
|
+
.t-h1 { color: var(--accent); }
|
|
348
|
+
.t-h2 { color: var(--accent-2); }
|
|
349
|
+
.t-h3 { color: var(--accent-3); }
|
|
341
350
|
.t-h4 { color: var(--teal); }
|
|
342
|
-
.t-strong { color: var(--peach);
|
|
343
|
-
.t-em { color: var(--subtext1);
|
|
351
|
+
.t-strong { color: var(--peach); }
|
|
352
|
+
.t-em { color: var(--subtext1); }
|
|
344
353
|
.t-code { color: var(--green); }
|
|
345
354
|
.t-fence { color: var(--overlay1); }
|
|
346
355
|
.t-codeline { color: var(--subtext1); }
|
|
347
|
-
.t-quote { color: var(--subtext0);
|
|
356
|
+
.t-quote { color: var(--subtext0); }
|
|
348
357
|
.t-marker { color: var(--accent); }
|
|
349
358
|
.t-link { color: var(--blue); }
|
|
350
359
|
.t-url { color: var(--overlay0); }
|
|
351
360
|
.t-img { color: var(--sapphire); }
|
|
352
|
-
.t-dir { color: var(--yellow);
|
|
353
|
-
.t-note { color: var(--overlay0);
|
|
361
|
+
.t-dir { color: var(--yellow); }
|
|
362
|
+
.t-note { color: var(--overlay0); }
|
|
354
363
|
.t-html { color: var(--pink); }
|
|
355
364
|
.t-table { color: var(--lavender); }
|
|
356
|
-
|
|
357
|
-
.t-sep { position: relative; color: var(--accent); counter-increment: sld; }
|
|
358
|
-
.t-sep::after {
|
|
359
|
-
content: 'slide ' counter(sld);
|
|
360
|
-
position: absolute;
|
|
361
|
-
left: calc(100% + 14px);
|
|
362
|
-
top: 0;
|
|
363
|
-
white-space: nowrap;
|
|
364
|
-
color: var(--overlay0);
|
|
365
|
-
font-size: 11px;
|
|
366
|
-
}
|
|
365
|
+
.t-sep { color: var(--accent); }
|
|
367
366
|
|
|
368
367
|
/* ── Drop target ──────────────────────────────────────────────────────── */
|
|
369
368
|
#pane-edit.is-dropping::after {
|
|
@@ -382,56 +381,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
382
381
|
pointer-events: none;
|
|
383
382
|
}
|
|
384
383
|
|
|
385
|
-
/* ── Contextual nudge ─────────────────────────────────────────────────── */
|
|
386
|
-
#nudge {
|
|
387
|
-
position: absolute;
|
|
388
|
-
left: 14px;
|
|
389
|
-
right: 14px;
|
|
390
|
-
bottom: 14px;
|
|
391
|
-
z-index: 7;
|
|
392
|
-
display: flex;
|
|
393
|
-
align-items: flex-start;
|
|
394
|
-
gap: 12px;
|
|
395
|
-
padding: 11px 13px;
|
|
396
|
-
background: var(--base);
|
|
397
|
-
border: 1px solid var(--surface1);
|
|
398
|
-
border-left: 2px solid var(--accent);
|
|
399
|
-
border-radius: 9px;
|
|
400
|
-
box-shadow: var(--shadow-md);
|
|
401
|
-
opacity: 0;
|
|
402
|
-
transform: translateY(10px);
|
|
403
|
-
pointer-events: none;
|
|
404
|
-
transition: opacity 0.22s ease, transform 0.22s ease;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
#nudge.is-on { opacity: 1; transform: translateY(0); pointer-events: all; }
|
|
408
|
-
#nudge__body { flex: 1 1 auto; }
|
|
409
|
-
#nudge__label {
|
|
410
|
-
font-size: 10px;
|
|
411
|
-
letter-spacing: 0.14em;
|
|
412
|
-
text-transform: uppercase;
|
|
413
|
-
color: var(--accent);
|
|
414
|
-
margin-bottom: 4px;
|
|
415
|
-
}
|
|
416
|
-
#nudge__text { font-size: 12.5px; color: var(--subtext1); line-height: 1.6; }
|
|
417
|
-
#nudge__text code {
|
|
418
|
-
font: inherit;
|
|
419
|
-
color: var(--green);
|
|
420
|
-
background: var(--surface-soft);
|
|
421
|
-
border-radius: 4px;
|
|
422
|
-
padding: 0.05em 0.3em;
|
|
423
|
-
}
|
|
424
|
-
#nudge__acts { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
|
|
425
|
-
.nudge-btn {
|
|
426
|
-
font-size: 11px;
|
|
427
|
-
color: var(--subtext0);
|
|
428
|
-
border: 1px solid var(--surface1);
|
|
429
|
-
border-radius: 6px;
|
|
430
|
-
padding: 4px 8px;
|
|
431
|
-
}
|
|
432
|
-
.nudge-btn:hover { border-color: var(--accent); color: var(--text); }
|
|
433
|
-
.nudge-btn--x { padding: 4px 7px; color: var(--overlay0); }
|
|
434
|
-
|
|
435
384
|
/* ── Preview ──────────────────────────────────────────────────────────── */
|
|
436
385
|
#prev-head {
|
|
437
386
|
display: flex;
|
|
@@ -1208,16 +1157,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
1208
1157
|
<pre id="measure" aria-hidden="true"></pre>
|
|
1209
1158
|
<textarea id="src" spellcheck="false" autocomplete="off" autocapitalize="off" wrap="soft" aria-label="Markdown source"></textarea>
|
|
1210
1159
|
</div>
|
|
1211
|
-
<div id="nudge">
|
|
1212
|
-
<div id="nudge__body">
|
|
1213
|
-
<div id="nudge__label">try this</div>
|
|
1214
|
-
<div id="nudge__text"></div>
|
|
1215
|
-
</div>
|
|
1216
|
-
<div id="nudge__acts">
|
|
1217
|
-
<button class="nudge-btn" id="nudge-do">insert</button>
|
|
1218
|
-
<button class="nudge-btn nudge-btn--x" id="nudge-x" title="Dismiss">×</button>
|
|
1219
|
-
</div>
|
|
1220
|
-
</div>
|
|
1221
1160
|
<textarea id="src-html" spellcheck="false" autocomplete="off" autocapitalize="off" wrap="off" aria-label="HTML source"></textarea>
|
|
1222
1161
|
</section>
|
|
1223
1162
|
|
|
@@ -1488,6 +1427,19 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
1488
1427
|
return days + 'd ago';
|
|
1489
1428
|
}
|
|
1490
1429
|
|
|
1430
|
+
function updateDocTitle() {
|
|
1431
|
+
if ($('screen-start') && $('screen-start').classList.contains('is-on')) {
|
|
1432
|
+
document.title = 'deckrun \u00b7 start';
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if ($('library') && $('library').classList.contains('is-on')) {
|
|
1436
|
+
document.title = 'deckrun \u00b7 library';
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
var name = ($('docname').value || '').trim();
|
|
1440
|
+
document.title = name ? name + ' \u00b7 deckrun' : 'deckrun \u00b7 editor';
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1491
1443
|
/** One deck's metadata is refreshed from the editor on every save. */
|
|
1492
1444
|
function touchCurrent() {
|
|
1493
1445
|
var list = loadIndex();
|
|
@@ -1506,6 +1458,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
1506
1458
|
break;
|
|
1507
1459
|
}
|
|
1508
1460
|
}
|
|
1461
|
+
updateDocTitle();
|
|
1509
1462
|
return saveIndex(list);
|
|
1510
1463
|
}
|
|
1511
1464
|
|
|
@@ -1577,7 +1530,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
1577
1530
|
var elChipSlides = $('chip-slides'), elSave = $('save-state'), elPos = $('pos'), elWords = $('words');
|
|
1578
1531
|
var elCount = $('prev-count'), elScale = $('prev-scale');
|
|
1579
1532
|
var elNotes = $('notes'), elNotesText = $('notes__text');
|
|
1580
|
-
var elNudge = $('nudge'), elNudgeText = $('nudge__text'), elNudgeDo = $('nudge-do'), elNudgeX = $('nudge-x');
|
|
1581
1533
|
var palette = $('palette'), palInput = $('pal-input'), palList = $('pal-list');
|
|
1582
1534
|
var guide = $('guide'), guideBody = $('guide-body');
|
|
1583
1535
|
var srcHtml = $('src-html'), frameHtml = $('frame-html');
|
|
@@ -1701,7 +1653,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
1701
1653
|
out.push('<span class="t-sep">' + esc(line) + '</span>');
|
|
1702
1654
|
continue;
|
|
1703
1655
|
}
|
|
1704
|
-
if (/^\\s
|
|
1656
|
+
if (/^\\s*<!--/.test(line)) {
|
|
1705
1657
|
out.push('<span class="t-note">' + esc(line) + '</span>');
|
|
1706
1658
|
continue;
|
|
1707
1659
|
}
|
|
@@ -1712,7 +1664,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
1712
1664
|
out.push('<span class="t-h' + lvl + '">' + inlineTokens(line) + '</span>');
|
|
1713
1665
|
continue;
|
|
1714
1666
|
}
|
|
1715
|
-
if (/^\\s
|
|
1667
|
+
if (/^\\s*>/.test(line)) {
|
|
1716
1668
|
out.push('<span class="t-quote">' + inlineTokens(line) + '</span>');
|
|
1717
1669
|
continue;
|
|
1718
1670
|
}
|
|
@@ -1850,12 +1802,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
1850
1802
|
var before = src.value.slice(0, src.selectionStart);
|
|
1851
1803
|
var lines = before.split('\\n');
|
|
1852
1804
|
elPos.textContent = 'Ln ' + lines.length + ', Col ' + (lines[lines.length - 1].length + 1);
|
|
1853
|
-
var i = caretSlide();
|
|
1854
|
-
if (i !== state.index) {
|
|
1855
|
-
state.index = i;
|
|
1856
|
-
post({ type: 'index', index: i });
|
|
1857
|
-
renderCounts();
|
|
1858
|
-
}
|
|
1859
1805
|
}
|
|
1860
1806
|
|
|
1861
1807
|
// ── Parse round-trip: the server owns the Markdown, so what you see here
|
|
@@ -2081,58 +2027,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
2081
2027
|
var bySnippetId = {};
|
|
2082
2028
|
D.snippets.forEach(function (s) { bySnippetId[s.id] = s; });
|
|
2083
2029
|
|
|
2084
|
-
function evalNudges() {
|
|
2085
|
-
|
|
2086
|
-
var ctx = docContext();
|
|
2087
|
-
for (var i = 0; i < NUDGE_RULES.length; i++) {
|
|
2088
|
-
var rule = NUDGE_RULES[i];
|
|
2089
|
-
if (state.dismissed[rule.id]) continue;
|
|
2090
|
-
var message = rule.test(ctx);
|
|
2091
|
-
if (!message) continue;
|
|
2092
|
-
showNudge(rule, message);
|
|
2093
|
-
return;
|
|
2094
|
-
}
|
|
2095
|
-
hideNudge();
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
function showNudge(rule, message) {
|
|
2099
|
-
if (state.activeNudge && state.activeNudge.rule.id === rule.id &&
|
|
2100
|
-
state.activeNudge.message === message) return;
|
|
2101
|
-
if (state.activeNudge && Date.now() - state.nudgeShownAt < 2500) return;
|
|
2102
|
-
state.activeNudge = { rule: rule, message: message };
|
|
2103
|
-
state.nudgeShownAt = Date.now();
|
|
2104
|
-
elNudgeText.innerHTML = message;
|
|
2105
|
-
elNudgeDo.textContent = rule.action === 'download' ? 'download' : 'insert';
|
|
2106
|
-
elNudge.classList.add('is-on');
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
function hideNudge() {
|
|
2110
|
-
state.activeNudge = null;
|
|
2111
|
-
elNudge.classList.remove('is-on');
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
elNudgeDo.addEventListener('click', function () {
|
|
2115
|
-
var active = state.activeNudge;
|
|
2116
|
-
if (!active) return;
|
|
2117
|
-
if (active.rule.action) runAction(active.rule.action);
|
|
2118
|
-
else if (bySnippetId[active.rule.snippet]) insertSnippet(bySnippetId[active.rule.snippet]);
|
|
2119
|
-
hideNudge();
|
|
2120
|
-
});
|
|
2121
|
-
|
|
2122
|
-
elNudgeX.addEventListener('click', function () {
|
|
2123
|
-
var active = state.activeNudge;
|
|
2124
|
-
if (active) {
|
|
2125
|
-
state.dismissed[active.rule.id] = true;
|
|
2126
|
-
// A tip stays dismissed for good; a real problem may nag again next session.
|
|
2127
|
-
if (!active.rule.sticky) {
|
|
2128
|
-
var keep = Object.keys(state.dismissed).filter(function (id) {
|
|
2129
|
-
return !NUDGE_RULES.some(function (r) { return r.id === id && r.sticky; });
|
|
2130
|
-
});
|
|
2131
|
-
lsSet(K.nudge, JSON.stringify(keep));
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
hideNudge();
|
|
2135
|
-
});
|
|
2030
|
+
function evalNudges() {}
|
|
2031
|
+
function hideNudge() {}
|
|
2136
2032
|
|
|
2137
2033
|
// ── Tips carousel ──────────────────────────────────────────────────────
|
|
2138
2034
|
var tipTimer = null;
|
|
@@ -2330,6 +2226,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
2330
2226
|
guide.classList.remove('is-on');
|
|
2331
2227
|
$('library').classList.remove('is-on');
|
|
2332
2228
|
if ($('themes').classList.contains('is-on')) closeThemes(true);
|
|
2229
|
+
updateDocTitle();
|
|
2333
2230
|
if (!keepFocus) src.focus();
|
|
2334
2231
|
}
|
|
2335
2232
|
|
|
@@ -2801,7 +2698,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
2801
2698
|
// Hovering previews too, so the mouse gets the same instant feedback
|
|
2802
2699
|
// as the arrow keys.
|
|
2803
2700
|
card.addEventListener('mouseenter', function () { selectTheme(themeCards.indexOf(card)); });
|
|
2804
|
-
card.addEventListener('click', function () { commitTheme(); });
|
|
2701
|
+
card.addEventListener('click', function () { selectTheme(themeCards.indexOf(card)); commitTheme(); });
|
|
2805
2702
|
grid.appendChild(card);
|
|
2806
2703
|
themeCards.push(card);
|
|
2807
2704
|
});
|
|
@@ -2876,6 +2773,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
2876
2773
|
lsSet(K.current, id);
|
|
2877
2774
|
setDocKind(meta.kind || 'markdown');
|
|
2878
2775
|
$('docname').value = meta.name;
|
|
2776
|
+
updateDocTitle();
|
|
2879
2777
|
updateDeckCount();
|
|
2880
2778
|
elSave.className = '';
|
|
2881
2779
|
elSave.textContent = '';
|
|
@@ -3029,6 +2927,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3029
2927
|
for (var i = 0; i < list.length; i++) if (list[i].id === state.deckId) libSel = i;
|
|
3030
2928
|
renderLibrary();
|
|
3031
2929
|
$('library').classList.add('is-on');
|
|
2930
|
+
updateDocTitle();
|
|
3032
2931
|
}
|
|
3033
2932
|
|
|
3034
2933
|
$('lib-new').addEventListener('click', function () { closeOverlays(); showStartScreen(); });
|
|
@@ -3041,10 +2940,12 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3041
2940
|
function showStartScreen() {
|
|
3042
2941
|
$('start-lib').classList.toggle('is-disabled', !loadIndex().length);
|
|
3043
2942
|
$('screen-start').classList.add('is-on');
|
|
2943
|
+
updateDocTitle();
|
|
3044
2944
|
}
|
|
3045
2945
|
|
|
3046
2946
|
function hideStartScreenSilently() {
|
|
3047
2947
|
$('screen-start').classList.remove('is-on');
|
|
2948
|
+
updateDocTitle();
|
|
3048
2949
|
}
|
|
3049
2950
|
|
|
3050
2951
|
/** Escape/backdrop close. With nothing open yet, that would be a dead end,
|
|
@@ -3091,7 +2992,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3091
2992
|
openLibrary();
|
|
3092
2993
|
}
|
|
3093
2994
|
|
|
3094
|
-
/** Fetches a public
|
|
2995
|
+
/** Fetches a public URL server-side (sidesteps CORS) and opens it as a new Markdown deck or HTML doc. */
|
|
3095
2996
|
function loadHtmlUrl() {
|
|
3096
2997
|
var input = $('start-html-url');
|
|
3097
2998
|
var raw = input.value.trim();
|
|
@@ -3118,12 +3019,14 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3118
3019
|
.then(function (body) {
|
|
3119
3020
|
saveNow();
|
|
3120
3021
|
var name = uniqueName(body.title || url.hostname);
|
|
3121
|
-
var
|
|
3022
|
+
var kind = body.kind || (body.markdown ? 'markdown' : 'html');
|
|
3023
|
+
var content = kind === 'markdown' ? (body.markdown || body.content || '') : (body.html || body.content || '');
|
|
3024
|
+
var id = createDeck(name, content, kind);
|
|
3122
3025
|
if (!id) { noRoom(); return; }
|
|
3123
3026
|
input.value = '';
|
|
3124
3027
|
hideStartScreenSilently();
|
|
3125
3028
|
openDeck(id);
|
|
3126
|
-
toast('Loaded ' +
|
|
3029
|
+
toast('Loaded ' + (kind === 'markdown' ? 'Markdown deck' : 'HTML doc') + ' from ' + url.hostname);
|
|
3127
3030
|
})
|
|
3128
3031
|
.catch(function (err) { toast('Could not load URL: ' + err.message, 'err'); })
|
|
3129
3032
|
.then(function () { btn.disabled = false; });
|
|
@@ -3148,6 +3051,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3148
3051
|
else if (name === 'grid') setMode(state.mode === 'grid' ? 'single' : 'grid');
|
|
3149
3052
|
else if (name === 'theme') openThemes();
|
|
3150
3053
|
else if (name === 'guide') { guide.classList.add('is-on'); }
|
|
3054
|
+
else if (name === 'palette') openPalette();
|
|
3151
3055
|
else if (name === 'decks') openLibrary();
|
|
3152
3056
|
else if (name === 'new') newDeck();
|
|
3153
3057
|
else if (name === 'duplicate') duplicateDeck();
|
|
@@ -3179,26 +3083,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3179
3083
|
e.preventDefault();
|
|
3180
3084
|
typeText(' ');
|
|
3181
3085
|
onInput();
|
|
3182
|
-
return;
|
|
3183
|
-
}
|
|
3184
|
-
// Continue the list you are in.
|
|
3185
|
-
if (e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
|
3186
|
-
var upto = src.value.slice(0, src.selectionStart);
|
|
3187
|
-
var line = upto.slice(upto.lastIndexOf('\\n') + 1);
|
|
3188
|
-
var m = line.match(/^(\\s*)([-*+]|(\\d+)[.)])(\\s+)(.*)$/);
|
|
3189
|
-
if (m && src.selectionStart === src.selectionEnd) {
|
|
3190
|
-
e.preventDefault();
|
|
3191
|
-
if (!m[5]) {
|
|
3192
|
-
// Empty item: end the list instead of nesting further.
|
|
3193
|
-
var start = src.selectionStart - line.length;
|
|
3194
|
-
src.setSelectionRange(start, src.selectionStart);
|
|
3195
|
-
typeText('\\n');
|
|
3196
|
-
} else {
|
|
3197
|
-
var marker = m[3] ? (parseInt(m[3], 10) + 1) + m[2].slice(String(m[3]).length) : m[2];
|
|
3198
|
-
typeText('\\n' + m[1] + marker + m[4]);
|
|
3199
|
-
}
|
|
3200
|
-
onInput();
|
|
3201
|
-
}
|
|
3202
3086
|
}
|
|
3203
3087
|
});
|
|
3204
3088
|
|
|
@@ -3304,7 +3188,10 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3304
3188
|
e.target.value = '';
|
|
3305
3189
|
});
|
|
3306
3190
|
|
|
3307
|
-
$('docname').addEventListener('input',
|
|
3191
|
+
$('docname').addEventListener('input', function () {
|
|
3192
|
+
updateDocTitle();
|
|
3193
|
+
scheduleSave();
|
|
3194
|
+
});
|
|
3308
3195
|
|
|
3309
3196
|
$('btn-guide').addEventListener('click', function () { runAction('guide'); });
|
|
3310
3197
|
$('btn-palette').addEventListener('click', openPalette);
|
|
@@ -3415,12 +3302,25 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3415
3302
|
themeCards[next].scrollIntoView({ block: 'nearest' });
|
|
3416
3303
|
}, true);
|
|
3417
3304
|
$('btn-present').addEventListener('click', present);
|
|
3418
|
-
$('btn-prev').addEventListener('click', function () { setIndex(state.index - 1,
|
|
3419
|
-
$('btn-next').addEventListener('click', function () { setIndex(state.index + 1,
|
|
3305
|
+
$('btn-prev').addEventListener('click', function () { setIndex(state.index - 1, false); });
|
|
3306
|
+
$('btn-next').addEventListener('click', function () { setIndex(state.index + 1, false); });
|
|
3420
3307
|
$('seg-single').addEventListener('click', function () { setMode('single'); });
|
|
3421
3308
|
$('seg-grid').addEventListener('click', function () { setMode('grid'); });
|
|
3422
3309
|
|
|
3310
|
+
$('pane-prev').addEventListener('mousedown', function () {
|
|
3311
|
+
if (document.activeElement && isEditingText()) {
|
|
3312
|
+
document.activeElement.blur();
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3423
3316
|
// ── Global keys ────────────────────────────────────────────────────────
|
|
3317
|
+
function isEditingText() {
|
|
3318
|
+
var el = document.activeElement;
|
|
3319
|
+
if (!el) return false;
|
|
3320
|
+
var tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
3321
|
+
return tag === 'textarea' || tag === 'input' || el.isContentEditable;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3424
3324
|
document.addEventListener('keydown', function (e) {
|
|
3425
3325
|
var mod = e.metaKey || e.ctrlKey;
|
|
3426
3326
|
|
|
@@ -3458,31 +3358,59 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3458
3358
|
}
|
|
3459
3359
|
return;
|
|
3460
3360
|
}
|
|
3461
|
-
if (!mod) return;
|
|
3462
|
-
|
|
3463
|
-
var key = e.key.toLowerCase();
|
|
3464
|
-
|
|
3465
|
-
var md = state.kind === 'markdown';
|
|
3466
|
-
|
|
3467
|
-
if (key === 'k' && !e.shiftKey && md) { e.preventDefault(); openPalette(); }
|
|
3468
|
-
else if (key === 'o') { e.preventDefault(); openLibrary(); }
|
|
3469
|
-
else if (key === '/' && md) { e.preventDefault(); runAction('guide'); }
|
|
3470
|
-
else if (key === 's' && e.shiftKey) { e.preventDefault(); exportPdf(); }
|
|
3471
|
-
else if (key === 's') { e.preventDefault(); saveNow(); download(); }
|
|
3472
|
-
else if (key === 'enter') { e.preventDefault(); present(); }
|
|
3473
|
-
else if (key === 'd' && md) { e.preventDefault(); insertSnippet(bySnippetId['slide-break']); }
|
|
3474
|
-
else if (key === 'b' && md) { e.preventDefault(); insertSnippet(bySnippetId.bold); }
|
|
3475
|
-
else if (key === 'i' && md) { e.preventDefault(); insertSnippet(bySnippetId.italic); }
|
|
3476
|
-
else if (key === 'e' && md) { e.preventDefault(); insertSnippet(bySnippetId['inline-code']); }
|
|
3477
|
-
else if (key === 'g' && md) { e.preventDefault(); runAction('grid'); }
|
|
3478
|
-
else if (key === 'l' && e.shiftKey) { e.preventDefault(); runAction('theme'); }
|
|
3479
|
-
});
|
|
3480
3361
|
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3362
|
+
if (mod) {
|
|
3363
|
+
var key = e.key.toLowerCase();
|
|
3364
|
+
var md = state.kind === 'markdown';
|
|
3365
|
+
|
|
3366
|
+
if (key === 'k' && !e.shiftKey && md) { e.preventDefault(); openPalette(); }
|
|
3367
|
+
else if (key === 'o') { e.preventDefault(); openLibrary(); }
|
|
3368
|
+
else if (key === '/' && md) { e.preventDefault(); runAction('guide'); }
|
|
3369
|
+
else if (key === 's' && e.shiftKey) { e.preventDefault(); exportPdf(); }
|
|
3370
|
+
else if (key === 's') { e.preventDefault(); saveNow(); download(); }
|
|
3371
|
+
else if (key === 'enter') { e.preventDefault(); present(); }
|
|
3372
|
+
else if (key === 'd' && md) { e.preventDefault(); insertSnippet(bySnippetId['slide-break']); }
|
|
3373
|
+
else if (key === 'b' && md) { e.preventDefault(); insertSnippet(bySnippetId.bold); }
|
|
3374
|
+
else if (key === 'i' && md) { e.preventDefault(); insertSnippet(bySnippetId.italic); }
|
|
3375
|
+
else if (key === 'e' && md) { e.preventDefault(); insertSnippet(bySnippetId['inline-code']); }
|
|
3376
|
+
else if (key === 'g' && md) { e.preventDefault(); runAction('grid'); }
|
|
3377
|
+
else if (key === 'l' && e.shiftKey) { e.preventDefault(); runAction('theme'); }
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
if (palette.classList.contains('is-on') || guide.classList.contains('is-on') ||
|
|
3382
|
+
$('library').classList.contains('is-on') || menuIsOpen()) {
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
// Alt + arrows/page keys hops between slides (works anywhere, including inside the editor).
|
|
3387
|
+
if (e.altKey) {
|
|
3388
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight' || e.key === 'PageDown') {
|
|
3389
|
+
e.preventDefault();
|
|
3390
|
+
setIndex(state.index + 1, false);
|
|
3391
|
+
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft' || e.key === 'PageUp') {
|
|
3392
|
+
e.preventDefault();
|
|
3393
|
+
setIndex(state.index - 1, false);
|
|
3394
|
+
}
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
// Direct arrow keys / page keys when not typing into a text field.
|
|
3399
|
+
if (!isEditingText()) {
|
|
3400
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown') {
|
|
3401
|
+
e.preventDefault();
|
|
3402
|
+
setIndex(state.index + 1, false);
|
|
3403
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
|
|
3404
|
+
e.preventDefault();
|
|
3405
|
+
setIndex(state.index - 1, false);
|
|
3406
|
+
} else if (e.key === 'Home') {
|
|
3407
|
+
e.preventDefault();
|
|
3408
|
+
setIndex(0, false);
|
|
3409
|
+
} else if (e.key === 'End') {
|
|
3410
|
+
e.preventDefault();
|
|
3411
|
+
setIndex(state.slides.length - 1, false);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3486
3414
|
});
|
|
3487
3415
|
|
|
3488
3416
|
// ── Split pane ─────────────────────────────────────────────────────────
|
|
@@ -3539,6 +3467,12 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
|
|
|
3539
3467
|
} else if (m.type === 'goto') {
|
|
3540
3468
|
setMode('single');
|
|
3541
3469
|
setIndex(m.index, true);
|
|
3470
|
+
} else if (m.type === 'nav') {
|
|
3471
|
+
setIndex(state.index + m.delta, false);
|
|
3472
|
+
} else if (m.type === 'index-select') {
|
|
3473
|
+
setIndex(m.index, false);
|
|
3474
|
+
} else if (m.type === 'action') {
|
|
3475
|
+
runAction(m.action);
|
|
3542
3476
|
} else if (m.type === 'overflow') {
|
|
3543
3477
|
var had = !!state.overflow[m.index];
|
|
3544
3478
|
state.overflow[m.index] = m.overflow;
|
package/dist/generate.js
CHANGED
|
@@ -72,7 +72,8 @@ export const SLIDE_CSS = `html, body {
|
|
|
72
72
|
position: absolute;
|
|
73
73
|
inset: 0;
|
|
74
74
|
display: flex;
|
|
75
|
-
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
align-items: stretch;
|
|
76
77
|
justify-content: flex-start;
|
|
77
78
|
padding: var(--slide-pad-y) var(--slide-pad-x);
|
|
78
79
|
opacity: 0;
|
|
@@ -116,7 +117,7 @@ export const SLIDE_CSS = `html, body {
|
|
|
116
117
|
position: relative;
|
|
117
118
|
z-index: 1;
|
|
118
119
|
width: 100%;
|
|
119
|
-
max-width:
|
|
120
|
+
max-width: 100%;
|
|
120
121
|
max-height: calc(100vh - var(--slide-pad-y) * 2);
|
|
121
122
|
overflow: hidden;
|
|
122
123
|
}
|
|
@@ -151,7 +152,7 @@ export const SLIDE_CSS = `html, body {
|
|
|
151
152
|
z-index: 1;
|
|
152
153
|
display: flex;
|
|
153
154
|
width: 100%;
|
|
154
|
-
max-width:
|
|
155
|
+
max-width: 100%;
|
|
155
156
|
height: calc(100vh - var(--slide-pad-y) * 2);
|
|
156
157
|
align-items: center;
|
|
157
158
|
gap: 3.2rem;
|
|
@@ -243,7 +244,6 @@ export const SLIDE_CSS = `html, body {
|
|
|
243
244
|
line-height: calc(1.72 * var(--type-lead));
|
|
244
245
|
margin-bottom: 1rem;
|
|
245
246
|
color: var(--subtext1);
|
|
246
|
-
max-width: 62ch;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
.slide__content strong {
|
|
@@ -266,7 +266,6 @@ export const SLIDE_CSS = `html, body {
|
|
|
266
266
|
line-height: calc(1.72 * var(--type-lead));
|
|
267
267
|
margin-bottom: 1rem;
|
|
268
268
|
color: var(--subtext1);
|
|
269
|
-
max-width: 64ch;
|
|
270
269
|
}
|
|
271
270
|
|
|
272
271
|
.slide__content ul { list-style: none; padding-left: 1.35em; }
|
|
@@ -379,7 +378,6 @@ export const SLIDE_CSS = `html, body {
|
|
|
379
378
|
border-radius: 0 10px 10px 0;
|
|
380
379
|
color: var(--subtext1);
|
|
381
380
|
font-size: calc(clamp(1rem, 1.5vw, 1.3rem) * var(--type-body));
|
|
382
|
-
max-width: 60ch;
|
|
383
381
|
}
|
|
384
382
|
|
|
385
383
|
.slide__content blockquote::before {
|
|
@@ -1066,12 +1064,13 @@ export function generateHtml(slides, title, autoFullscreen = false, themeInput =
|
|
|
1066
1064
|
const fontAttrs = (head ? ` data-head="${head}"` : "") + (body ? ` data-body="${body}"` : "");
|
|
1067
1065
|
const slideHtml = slides.map((s, i) => renderSlide(s, i)).join("\n");
|
|
1068
1066
|
const total = slides.length;
|
|
1067
|
+
const pageTitle = title ? (title.toLowerCase().includes("deckrun") ? title : `${title} · deckrun`) : "deckrun";
|
|
1069
1068
|
return `<!DOCTYPE html>
|
|
1070
1069
|
<html lang="en" data-theme="${theme}" data-decor="${decorOf(theme)}" data-size="${size}"${fontAttrs}>
|
|
1071
1070
|
<head>
|
|
1072
1071
|
<meta charset="UTF-8">
|
|
1073
1072
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1074
|
-
<title>${escAttr(
|
|
1073
|
+
<title>${escAttr(pageTitle)}</title>
|
|
1075
1074
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1076
1075
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1077
1076
|
<link href="${googleFontsHref([theme], [head, body])}" rel="stylesheet">
|
|
@@ -1861,12 +1860,13 @@ const DOC_CSS = `html, body {
|
|
|
1861
1860
|
*/
|
|
1862
1861
|
export function generateDocHtml(docUrl, title, autoFullscreen = false, themeInput = DEFAULT_THEME) {
|
|
1863
1862
|
const theme = resolveThemeName(themeInput);
|
|
1863
|
+
const pageTitle = title ? (title.toLowerCase().includes("deckrun") ? title : `${title} · deckrun`) : "deckrun";
|
|
1864
1864
|
return `<!DOCTYPE html>
|
|
1865
1865
|
<html lang="en" data-theme="${theme}">
|
|
1866
1866
|
<head>
|
|
1867
1867
|
<meta charset="UTF-8">
|
|
1868
1868
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1869
|
-
<title>${escAttr(
|
|
1869
|
+
<title>${escAttr(pageTitle)}</title>
|
|
1870
1870
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1871
1871
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1872
1872
|
<link href="${googleFontsHref([theme])}" rel="stylesheet">
|
package/dist/index.js
CHANGED
|
@@ -281,8 +281,8 @@ async function handleEditorRoute(mode, pathname, req, res) {
|
|
|
281
281
|
res.end(JSON.stringify({ error: "fetch failed", detail: `upstream responded ${upstream.status}` }));
|
|
282
282
|
return true;
|
|
283
283
|
}
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
284
|
+
const rawContent = await upstream.text();
|
|
285
|
+
if (rawContent.length > MAX_BODY) {
|
|
286
286
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
287
287
|
res.end(JSON.stringify({
|
|
288
288
|
error: "too large",
|
|
@@ -290,12 +290,48 @@ async function handleEditorRoute(mode, pathname, req, res) {
|
|
|
290
290
|
}));
|
|
291
291
|
return true;
|
|
292
292
|
}
|
|
293
|
-
if (!
|
|
293
|
+
if (!rawContent.trim()) {
|
|
294
294
|
res.writeHead(422, { "Content-Type": "application/json" });
|
|
295
295
|
res.end(JSON.stringify({ error: "empty document" }));
|
|
296
296
|
return true;
|
|
297
297
|
}
|
|
298
|
-
|
|
298
|
+
const contentType = (upstream.headers.get("content-type") ?? "").toLowerCase();
|
|
299
|
+
const pathname = target.pathname.toLowerCase();
|
|
300
|
+
let isHtml = false;
|
|
301
|
+
if (pathname.endsWith(".html") || pathname.endsWith(".htm")) {
|
|
302
|
+
isHtml = true;
|
|
303
|
+
}
|
|
304
|
+
else if (pathname.endsWith(".md") || pathname.endsWith(".markdown")) {
|
|
305
|
+
isHtml = false;
|
|
306
|
+
}
|
|
307
|
+
else if (contentType.includes("text/html") ||
|
|
308
|
+
contentType.includes("application/xhtml+xml")) {
|
|
309
|
+
isHtml = true;
|
|
310
|
+
}
|
|
311
|
+
else if (contentType.includes("text/markdown") ||
|
|
312
|
+
contentType.includes("text/x-markdown") ||
|
|
313
|
+
contentType.includes("text/plain")) {
|
|
314
|
+
isHtml = false;
|
|
315
|
+
}
|
|
316
|
+
else if (/<!doctype\s+html/i.test(rawContent) || /<html[\s>]/i.test(rawContent)) {
|
|
317
|
+
isHtml = true;
|
|
318
|
+
}
|
|
319
|
+
const defaultName = target.pathname.split("/").filter(Boolean).pop() || target.hostname;
|
|
320
|
+
let title;
|
|
321
|
+
if (isHtml) {
|
|
322
|
+
title = docTitle(rawContent, defaultName);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const slides = parseSlides(rawContent);
|
|
326
|
+
title = deckTitle(slides, defaultName);
|
|
327
|
+
}
|
|
328
|
+
sendJson(res, {
|
|
329
|
+
kind: isHtml ? "html" : "markdown",
|
|
330
|
+
content: rawContent,
|
|
331
|
+
html: isHtml ? rawContent : undefined,
|
|
332
|
+
markdown: !isHtml ? rawContent : undefined,
|
|
333
|
+
title,
|
|
334
|
+
});
|
|
299
335
|
return true;
|
|
300
336
|
}
|
|
301
337
|
if (pathname === "/__present-doc" && req.method === "POST") {
|
|
@@ -373,6 +409,10 @@ async function serve(mode, baseDir, port) {
|
|
|
373
409
|
: generateEditorHtml(mode.theme, mode.size, mode.fonts));
|
|
374
410
|
return;
|
|
375
411
|
}
|
|
412
|
+
if (mode.kind === "deck" && mode.remoteDoc && pathname === "/__remote-doc") {
|
|
413
|
+
sendHtml(res, mode.remoteDoc);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
376
416
|
if (mode.kind === "editor" && (await handleEditorRoute(mode, pathname, req, res))) {
|
|
377
417
|
return;
|
|
378
418
|
}
|
|
@@ -407,9 +447,9 @@ async function serve(mode, baseDir, port) {
|
|
|
407
447
|
const program = new Command();
|
|
408
448
|
program
|
|
409
449
|
.name("deckrun")
|
|
410
|
-
.description("Present a Markdown file in the browser. Run without a file to write
|
|
450
|
+
.description("Present a Markdown file, HTML file, or public URL in the browser. Run without a file or URL to write in the built-in editor.")
|
|
411
451
|
.version(packageVersion(), "-v, --version", "Print the version number")
|
|
412
|
-
.argument("[file]", "Markdown file to present. Omit it to open the editor.")
|
|
452
|
+
.argument("[file]", "Markdown file, HTML file, or public URL to present. Omit it to open the editor.")
|
|
413
453
|
.option("-p, --port <number>", "Port to serve on", "7890")
|
|
414
454
|
.option("--no-open", "Do not automatically open the browser")
|
|
415
455
|
.option("--fullscreen", "Auto-enter fullscreen on first interaction")
|
|
@@ -468,52 +508,146 @@ program
|
|
|
468
508
|
let mode;
|
|
469
509
|
let baseDir;
|
|
470
510
|
if (file) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const ext = extname(absPath).toLowerCase();
|
|
474
|
-
if (ext === ".html" || ext === ".htm") {
|
|
475
|
-
let rawHtml;
|
|
511
|
+
if (/^https?:\/\//i.test(file)) {
|
|
512
|
+
let target;
|
|
476
513
|
try {
|
|
477
|
-
|
|
514
|
+
target = new URL(file);
|
|
478
515
|
}
|
|
479
516
|
catch {
|
|
480
|
-
console.error(`deckrun:
|
|
517
|
+
console.error(`deckrun: invalid URL '${file}'`);
|
|
481
518
|
process.exit(1);
|
|
482
519
|
}
|
|
483
|
-
|
|
484
|
-
console.error(`${c.dim}deckrun: --size, --head-font, and --body-font only apply to Markdown decks; ignored for an HTML doc.${c.reset}`);
|
|
485
|
-
}
|
|
486
|
-
const title = docTitle(rawHtml, basename(absPath, extname(absPath)));
|
|
487
|
-
mode = {
|
|
488
|
-
kind: "deck",
|
|
489
|
-
html: generateDocHtml(`/${basename(absPath)}`, title, fullscreen, theme),
|
|
490
|
-
};
|
|
491
|
-
console.log(`${c.dim}presenting ${basename(absPath)} · ${THEMES[theme].label}${c.reset}`);
|
|
492
|
-
}
|
|
493
|
-
else {
|
|
494
|
-
let markdown;
|
|
520
|
+
let upstream;
|
|
495
521
|
try {
|
|
496
|
-
|
|
522
|
+
upstream = await fetch(target, {
|
|
523
|
+
redirect: "follow",
|
|
524
|
+
signal: AbortSignal.timeout(15_000),
|
|
525
|
+
headers: { "User-Agent": "deckrun" },
|
|
526
|
+
});
|
|
497
527
|
}
|
|
498
|
-
catch {
|
|
499
|
-
console.error(`deckrun: cannot
|
|
528
|
+
catch (err) {
|
|
529
|
+
console.error(`deckrun: cannot fetch '${file}': ${err instanceof Error ? err.message : "network error"}`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
if (!upstream.ok) {
|
|
533
|
+
console.error(`deckrun: fetch failed for '${file}' (HTTP ${upstream.status})`);
|
|
500
534
|
process.exit(1);
|
|
501
535
|
}
|
|
502
|
-
const
|
|
503
|
-
if (
|
|
504
|
-
console.error(
|
|
536
|
+
const rawContent = await upstream.text();
|
|
537
|
+
if (!rawContent.trim()) {
|
|
538
|
+
console.error(`deckrun: empty document fetched from '${file}'`);
|
|
505
539
|
process.exit(1);
|
|
506
540
|
}
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
541
|
+
const contentType = (upstream.headers.get("content-type") ?? "").toLowerCase();
|
|
542
|
+
const pathname = target.pathname.toLowerCase();
|
|
543
|
+
let isHtml = false;
|
|
544
|
+
if (pathname.endsWith(".html") || pathname.endsWith(".htm")) {
|
|
545
|
+
isHtml = true;
|
|
546
|
+
}
|
|
547
|
+
else if (pathname.endsWith(".md") || pathname.endsWith(".markdown")) {
|
|
548
|
+
isHtml = false;
|
|
549
|
+
}
|
|
550
|
+
else if (contentType.includes("text/html") ||
|
|
551
|
+
contentType.includes("application/xhtml+xml")) {
|
|
552
|
+
isHtml = true;
|
|
553
|
+
}
|
|
554
|
+
else if (contentType.includes("text/markdown") ||
|
|
555
|
+
contentType.includes("text/x-markdown") ||
|
|
556
|
+
contentType.includes("text/plain")) {
|
|
557
|
+
isHtml = false;
|
|
558
|
+
}
|
|
559
|
+
else if (/<!doctype\s+html/i.test(rawContent) || /<html[\s>]/i.test(rawContent)) {
|
|
560
|
+
isHtml = true;
|
|
561
|
+
}
|
|
562
|
+
baseDir = process.cwd();
|
|
563
|
+
const defaultName = target.pathname.split("/").filter(Boolean).pop() || target.hostname;
|
|
564
|
+
if (isHtml) {
|
|
565
|
+
if (opts.size !== DEFAULT_SIZE || opts.headFont || opts.bodyFont) {
|
|
566
|
+
console.error(`${c.dim}deckrun: --size, --head-font, and --body-font only apply to Markdown decks; ignored for an HTML doc.${c.reset}`);
|
|
567
|
+
}
|
|
568
|
+
const title = docTitle(rawContent, defaultName);
|
|
569
|
+
let docHtml = rawContent;
|
|
570
|
+
if (!/<base\s/i.test(docHtml)) {
|
|
571
|
+
if (/<head[^>]*>/i.test(docHtml)) {
|
|
572
|
+
docHtml = docHtml.replace(/<head[^>]*>/i, (m) => `${m}\n <base href="${target.href}">`);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
docHtml = `<base href="${target.href}">\n` + docHtml;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
mode = {
|
|
579
|
+
kind: "deck",
|
|
580
|
+
html: generateDocHtml("/__remote-doc", title, fullscreen, theme),
|
|
581
|
+
remoteDoc: docHtml,
|
|
582
|
+
};
|
|
583
|
+
console.log(`${c.dim}presenting ${file} · ${THEMES[theme].label}${c.reset}`);
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
const slides = parseSlides(rawContent);
|
|
587
|
+
if (slides.length === 0) {
|
|
588
|
+
console.error("deckrun: no slides found in the fetched content.");
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
const title = deckTitle(slides, defaultName);
|
|
592
|
+
mode = {
|
|
593
|
+
kind: "deck",
|
|
594
|
+
html: generateHtml(slides, title, fullscreen, theme, size, fonts),
|
|
595
|
+
};
|
|
596
|
+
const faces = [
|
|
597
|
+
fonts.head ? `head ${fontName(fonts.head)}` : "",
|
|
598
|
+
fonts.body ? `body ${fontName(fonts.body)}` : "",
|
|
599
|
+
].filter(Boolean).join(" · ");
|
|
600
|
+
console.log(`${c.dim}${slides.length} slide${slides.length !== 1 ? "s" : ""} from ${file} · ${THEMES[theme].label} · type ${size}${faces ? " · " + faces : ""}${c.reset}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
const absPath = resolve(process.cwd(), file);
|
|
605
|
+
baseDir = dirname(absPath);
|
|
606
|
+
const ext = extname(absPath).toLowerCase();
|
|
607
|
+
if (ext === ".html" || ext === ".htm") {
|
|
608
|
+
let rawHtml;
|
|
609
|
+
try {
|
|
610
|
+
rawHtml = readFileSync(absPath, "utf-8");
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
console.error(`deckrun: cannot read file '${file}'`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
if (opts.size !== DEFAULT_SIZE || opts.headFont || opts.bodyFont) {
|
|
617
|
+
console.error(`${c.dim}deckrun: --size, --head-font, and --body-font only apply to Markdown decks; ignored for an HTML doc.${c.reset}`);
|
|
618
|
+
}
|
|
619
|
+
const title = docTitle(rawHtml, basename(absPath, extname(absPath)));
|
|
620
|
+
mode = {
|
|
621
|
+
kind: "deck",
|
|
622
|
+
html: generateDocHtml(`/${basename(absPath)}`, title, fullscreen, theme),
|
|
623
|
+
};
|
|
624
|
+
console.log(`${c.dim}presenting ${basename(absPath)} · ${THEMES[theme].label}${c.reset}`);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
let markdown;
|
|
628
|
+
try {
|
|
629
|
+
markdown = readFileSync(absPath, "utf-8");
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
console.error(`deckrun: cannot read file '${file}'`);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
const slides = parseSlides(markdown);
|
|
636
|
+
if (slides.length === 0) {
|
|
637
|
+
console.error("deckrun: no slides found in the file.");
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
const title = deckTitle(slides, basename(absPath, extname(absPath)));
|
|
641
|
+
mode = {
|
|
642
|
+
kind: "deck",
|
|
643
|
+
html: generateHtml(slides, title, fullscreen, theme, size, fonts),
|
|
644
|
+
};
|
|
645
|
+
const faces = [
|
|
646
|
+
fonts.head ? `head ${fontName(fonts.head)}` : "",
|
|
647
|
+
fonts.body ? `body ${fontName(fonts.body)}` : "",
|
|
648
|
+
].filter(Boolean).join(" · ");
|
|
649
|
+
console.log(`${c.dim}${slides.length} slide${slides.length !== 1 ? "s" : ""} from ${basename(absPath)} · ${THEMES[theme].label} · type ${size}${faces ? " · " + faces : ""}${c.reset}`);
|
|
650
|
+
}
|
|
517
651
|
}
|
|
518
652
|
}
|
|
519
653
|
else {
|
package/dist/preview.js
CHANGED
|
@@ -17,7 +17,7 @@ export function generatePreviewHtml(initialTheme = DEFAULT_THEME, initialSize =
|
|
|
17
17
|
<html lang="en" data-theme="${initialTheme}" data-decor="${decorOf(initialTheme)}" data-size="${size}"${fontAttrs}>
|
|
18
18
|
<head>
|
|
19
19
|
<meta charset="UTF-8">
|
|
20
|
-
<title>preview</title>
|
|
20
|
+
<title>preview · deckrun</title>
|
|
21
21
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
22
22
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
23
23
|
<link href="${googleFontsHref()}" rel="stylesheet">
|
|
@@ -220,6 +220,86 @@ body.is-empty #pv-empty { display: flex; }
|
|
|
220
220
|
if (thumb) send({ type: 'goto', index: parseInt(thumb.dataset.index, 10) });
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
window.addEventListener('keydown', function (e) {
|
|
224
|
+
var mod = e.metaKey || e.ctrlKey;
|
|
225
|
+
if (mod) {
|
|
226
|
+
var k = e.key.toLowerCase();
|
|
227
|
+
if (k === 'enter') { e.preventDefault(); send({ type: 'action', action: 'present' }); }
|
|
228
|
+
else if (k === 'k' && !e.shiftKey) { e.preventDefault(); send({ type: 'action', action: 'palette' }); }
|
|
229
|
+
else if (k === 'g') { e.preventDefault(); send({ type: 'action', action: 'grid' }); }
|
|
230
|
+
else if (k === 'o') { e.preventDefault(); send({ type: 'action', action: 'decks' }); }
|
|
231
|
+
else if (k === '/') { e.preventDefault(); send({ type: 'action', action: 'guide' }); }
|
|
232
|
+
else if (k === 's' && e.shiftKey) { e.preventDefault(); send({ type: 'action', action: 'pdf' }); }
|
|
233
|
+
else if (k === 's') { e.preventDefault(); send({ type: 'action', action: 'download' }); }
|
|
234
|
+
else if (k === 'l' && e.shiftKey) { e.preventDefault(); send({ type: 'action', action: 'theme' }); }
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (mode === 'grid') {
|
|
239
|
+
var thumbs = stage.querySelectorAll('.pv-thumb');
|
|
240
|
+
if (!thumbs.length) return;
|
|
241
|
+
var cols = 1;
|
|
242
|
+
if (thumbs.length > 1) {
|
|
243
|
+
var firstTop = thumbs[0].offsetTop;
|
|
244
|
+
for (var c = 1; c < thumbs.length; c++) {
|
|
245
|
+
if (thumbs[c].offsetTop !== firstTop) { cols = c; break; }
|
|
246
|
+
}
|
|
247
|
+
if (cols === 1 && thumbs.length > 1 && thumbs[1].offsetTop === firstTop) {
|
|
248
|
+
cols = thumbs.length;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
var step = 0;
|
|
252
|
+
if (e.key === 'ArrowRight') step = 1;
|
|
253
|
+
else if (e.key === 'ArrowLeft') step = -1;
|
|
254
|
+
else if (e.key === 'ArrowDown') step = cols;
|
|
255
|
+
else if (e.key === 'ArrowUp') step = -cols;
|
|
256
|
+
else if (e.key === 'Home') { e.preventDefault(); send({ type: 'goto', index: 0 }); return; }
|
|
257
|
+
else if (e.key === 'End') { e.preventDefault(); send({ type: 'goto', index: slides.length - 1 }); return; }
|
|
258
|
+
else if (e.key === 'Enter') { e.preventDefault(); send({ type: 'goto', index: index }); return; }
|
|
259
|
+
else if (e.key === 'Escape') { e.preventDefault(); send({ type: 'goto', index: index }); return; }
|
|
260
|
+
else return;
|
|
261
|
+
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
var next = Math.max(0, Math.min(slides.length - 1, index + step));
|
|
264
|
+
index = next;
|
|
265
|
+
var cur = stage.querySelector('.pv-thumb.is-current');
|
|
266
|
+
if (cur) cur.classList.remove('is-current');
|
|
267
|
+
var nextThumb = stage.querySelector('.pv-thumb[data-index="' + index + '"]');
|
|
268
|
+
if (nextThumb) {
|
|
269
|
+
nextThumb.classList.add('is-current');
|
|
270
|
+
nextThumb.scrollIntoView({ block: 'nearest' });
|
|
271
|
+
}
|
|
272
|
+
send({ type: 'index-select', index: index });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Single mode: Alt navigation or standard keys
|
|
277
|
+
if (e.altKey) {
|
|
278
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight' || e.key === 'PageDown') {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
send({ type: 'nav', delta: 1 });
|
|
281
|
+
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft' || e.key === 'PageUp') {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
send({ type: 'nav', delta: -1 });
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown') {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
send({ type: 'nav', delta: 1 });
|
|
291
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
send({ type: 'nav', delta: -1 });
|
|
294
|
+
} else if (e.key === 'Home') {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
send({ type: 'goto', index: 0 });
|
|
297
|
+
} else if (e.key === 'End') {
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
send({ type: 'goto', index: slides.length - 1 });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
223
303
|
window.addEventListener('resize', function () {
|
|
224
304
|
if (mode === 'grid') scaleThumbs();
|
|
225
305
|
});
|
package/dist/themes.js
CHANGED
|
@@ -53,7 +53,7 @@ export function fontName(key) {
|
|
|
53
53
|
}
|
|
54
54
|
const SANS = "system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif";
|
|
55
55
|
const SERIF = "Georgia, 'Times New Roman', serif";
|
|
56
|
-
const MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
|
|
56
|
+
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
|
|
57
57
|
export const FONTS = {
|
|
58
58
|
inter: { param: "Inter:wght@300;400;500;600;700;800", stack: `'Inter', ${SANS}`, kind: "sans" },
|
|
59
59
|
spaceGrotesk: { param: "Space+Grotesk:wght@400;500;600;700", stack: `'Space Grotesk', ${SANS}`, kind: "sans" },
|
|
@@ -71,7 +71,7 @@ export const FONTS = {
|
|
|
71
71
|
newsreader: { param: "Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400", stack: `'Newsreader', ${SERIF}`, kind: "serif" },
|
|
72
72
|
lora: { param: "Lora:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'Lora', ${SERIF}`, kind: "serif" },
|
|
73
73
|
plexMono: { param: "IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400", stack: `'IBM Plex Mono', ${MONO}`, kind: "mono" },
|
|
74
|
-
jetbrains: { param: "JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'JetBrains Mono', ${MONO}`, kind: "mono" },
|
|
74
|
+
jetbrains: { param: "JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400", stack: `'JetBrains Mono', ${MONO}`, kind: "mono" },
|
|
75
75
|
firaCode: { param: "Fira+Code:wght@400;500;600;700", stack: `'Fira Code', ${MONO}`, kind: "mono" },
|
|
76
76
|
spaceMono: { param: "Space+Mono:ital,wght@0,400;0,700;1,400", stack: `'Space Mono', ${MONO}`, kind: "mono" },
|
|
77
77
|
sourceCodePro: { param: "Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'Source Code Pro', ${MONO}`, kind: "mono" },
|
|
@@ -85,6 +85,7 @@ export const FONTS = {
|
|
|
85
85
|
*/
|
|
86
86
|
export function googleFontsHref(themes = THEME_IDS.slice(), extra = []) {
|
|
87
87
|
const wanted = new Set();
|
|
88
|
+
wanted.add("jetbrains");
|
|
88
89
|
for (const input of themes) {
|
|
89
90
|
const t = THEMES[resolveThemeName(input)];
|
|
90
91
|
wanted.add(t.type.display);
|
|
@@ -162,7 +163,7 @@ const SPECS = {
|
|
|
162
163
|
red: "#bf616a", mauve: "#b48ead", pink: "#d3a3c6",
|
|
163
164
|
},
|
|
164
165
|
roles: { accent: "sapphire", accent2: "blue", accent3: "teal" },
|
|
165
|
-
type: { display: "
|
|
166
|
+
type: { display: "jetbrains", body: "jetbrains", mono: "jetbrains", weight: 700, tracking: "-0.025em" },
|
|
166
167
|
decor: "waves",
|
|
167
168
|
hljs: `${HL}nord.min.css`,
|
|
168
169
|
},
|
|
@@ -399,7 +400,7 @@ export const THEME_IDS = [
|
|
|
399
400
|
export const THEMES = Object.fromEntries(THEME_IDS.map((id) => [id, { id, ...SPECS[id] }]));
|
|
400
401
|
/** `dark` and `light` predate the registry and still name the two originals. */
|
|
401
402
|
const ALIASES = {
|
|
402
|
-
dark: "
|
|
403
|
+
dark: "nord",
|
|
403
404
|
light: "daylight",
|
|
404
405
|
mocha: "midnight",
|
|
405
406
|
latte: "daylight",
|
|
@@ -407,7 +408,7 @@ const ALIASES = {
|
|
|
407
408
|
"rose-pine": "rosepine",
|
|
408
409
|
"rose-quartz": "rosequartz",
|
|
409
410
|
};
|
|
410
|
-
export const DEFAULT_THEME = "
|
|
411
|
+
export const DEFAULT_THEME = "nord";
|
|
411
412
|
/** A theme id from untrusted input, or `null` if it names nothing. */
|
|
412
413
|
export function findTheme(input) {
|
|
413
414
|
if (!input)
|
package/package.json
CHANGED