mdeck 3.10.1 → 3.12.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/CHANGELOG.md +44 -0
- package/README.md +15 -4
- package/assets/base.css +6 -1
- package/docs/reference/authoring.md +111 -9
- package/docs/site/assets/site.css +2 -0
- package/docs/site/content/appearance.md +1 -1
- package/docs/site/content/audience.md +36 -8
- package/docs/site/content/commands.md +6 -3
- package/docs/site/content/drawing.md +5 -5
- package/docs/site/content/getting-started.md +2 -32
- package/docs/site/content/install.md +49 -0
- package/docs/site/content/media.md +20 -1
- package/docs/site/content/presenting.md +67 -9
- package/docs/site/content/sharing.md +1 -0
- package/docs/site/content/welcome.md +21 -1
- package/docs/site/content/writing.md +27 -0
- package/docs/site/images/features/hotspots.webp +0 -0
- package/docs/site/images/features/languages.webp +0 -0
- package/docs/site/pages.js +1 -0
- package/examples/custom-layouts/slides.md +21 -9
- package/examples/showcase/refs.bib +21 -0
- package/examples/showcase/slides.ar.drawings.json +11 -0
- package/examples/showcase/slides.ar.md +567 -0
- package/examples/showcase/slides.de.drawings.json +11 -0
- package/examples/showcase/slides.de.md +567 -0
- package/examples/showcase/slides.de.results.json +57 -0
- package/examples/showcase/slides.en.drawings.json +11 -0
- package/examples/showcase/{slides.md → slides.en.md} +112 -5
- package/package.json +1 -1
- package/skills/write-slides/SKILL.md +14 -2
- package/src/build/check.js +34 -7
- package/src/build/components.js +5 -3
- package/src/build/config.js +2 -0
- package/src/build/homePlugin.js +64 -14
- package/src/build/include.js +249 -0
- package/src/build/inkPlugin.js +12 -2
- package/src/build/relayAccess.js +3 -1
- package/src/build/renderCheck.js +23 -3
- package/src/build/slidesPlugin.js +70 -22
- package/src/build/svgCheck.js +65 -0
- package/src/build/timingPlugin.js +96 -0
- package/src/cli/args.js +6 -5
- package/src/cli/index.js +77 -21
- package/src/components/Bibliography.jsx +7 -2
- package/src/components/Poll.jsx +2 -2
- package/src/components/QrCodeTag.jsx +2 -2
- package/src/components/VideoPlayer.jsx +6 -1
- package/src/components/WordCloud.jsx +7 -6
- package/src/components/activity.jsx +11 -8
- package/src/components/poll.css +2 -0
- package/src/core/editDeck.js +44 -7
- package/src/core/icons.js +9 -1
- package/src/core/labels.js +78 -0
- package/src/core/parseSlides.js +16 -6
- package/src/core/reactions.js +27 -0
- package/src/core/resultsReport.js +153 -0
- package/src/core/source.js +8 -1
- package/src/core/timing.js +83 -0
- package/src/core/validateDeck.js +11 -2
- package/src/editor/components/DeckSettings.jsx +1 -0
- package/src/editor/components/MetaFields.jsx +1 -0
- package/src/home/Home.jsx +39 -93
- package/src/home/home.css +5 -17
- package/src/layouts/layoutApi.jsx +51 -8
- package/src/layouts/renderSlide.jsx +13 -1
- package/src/live/answer/answer.css +9 -0
- package/src/live/answer/answer.js +32 -4
- package/src/live/client.js +23 -4
- package/src/live/rooms.js +18 -6
- package/src/runtime/KeyHelp.jsx +115 -0
- package/src/runtime/PresenterChrome.jsx +168 -0
- package/src/runtime/Reactions.jsx +56 -0
- package/src/runtime/RehearsalSummary.jsx +129 -0
- package/src/runtime/component.js +27 -0
- package/src/runtime/deck-stage.js +371 -37
- package/src/runtime/helpKeys.js +75 -0
- package/src/runtime/ink/bus.js +13 -5
- package/src/runtime/ink/outbox.js +27 -0
- package/src/runtime/ink/room.js +37 -12
- package/src/runtime/ink/segments.js +21 -0
- package/src/runtime/main.jsx +508 -286
- package/src/runtime/shared.js +77 -0
- package/src/runtime/slideControls.js +60 -0
- package/src/runtime/steps.js +36 -0
- package/src/runtime/useTalkTimer.js +67 -0
- package/examples/citations/refs.bib +0 -30
- package/examples/citations/refs.yml +0 -21
- package/examples/citations/slides.md +0 -43
- package/examples/citations/slides.pdf +0 -0
- package/examples/poll/slides.md +0 -127
- package/examples/showcase/slides.drawings.json +0 -11
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
theme:
|
|
2
|
+
theme: neue
|
|
3
3
|
meta:
|
|
4
4
|
title: "A tour of mdeck"
|
|
5
5
|
author: "Tilman Schieber"
|
|
6
6
|
organization: "FHNW"
|
|
7
7
|
date: "2026-10-06"
|
|
8
|
+
bibliography: refs.bib
|
|
9
|
+
reactions: true
|
|
8
10
|
---
|
|
9
11
|
|
|
10
12
|
---
|
|
@@ -14,14 +16,17 @@ image: ./img/mdeck.svg
|
|
|
14
16
|
fit: contain
|
|
15
17
|
alt: "A Markdown file turning into a stack of slides"
|
|
16
18
|
id: a-talk-in-a-text-file
|
|
19
|
+
time: 1m
|
|
17
20
|
:::
|
|
18
21
|
# A talk in a text file.
|
|
19
22
|
## A tour of mdeck: each slide shows *what is written* and what it becomes.
|
|
20
23
|
|
|
21
24
|
:::notes
|
|
22
|
-
This deck teaches mdeck by example. Most slides show the Markdown beside what it becomes. Open `slides.md` next to it: every slide here is written exactly as shown.
|
|
25
|
+
This deck teaches mdeck by example. Most slides show the Markdown beside what it becomes. Open `slides.en.md` next to it: every slide here is written exactly as shown.
|
|
23
26
|
|
|
24
27
|
Go through it with the arrow keys or the space bar. These notes are only for you; open the presenter view from the launch page to see them beside the slide.
|
|
28
|
+
|
|
29
|
+
The tour also comes in German (`slides.de.md`) and Arabic (`slides.ar.md`). Start it with `mdeck run slides.en.md`, and **L** in the presenter view switches every window to the next language.
|
|
25
30
|
:::
|
|
26
31
|
|
|
27
32
|
---
|
|
@@ -30,6 +35,7 @@ layout: chapter
|
|
|
30
35
|
number: 1
|
|
31
36
|
part: Writing
|
|
32
37
|
description: Plain Markdown, the format AI assistants already write. You can read and change every word.
|
|
38
|
+
time: 15s
|
|
33
39
|
:::
|
|
34
40
|
# Write it.
|
|
35
41
|
|
|
@@ -40,6 +46,7 @@ In practice an AI assistant usually writes this file from your own material: old
|
|
|
40
46
|
---
|
|
41
47
|
:::meta
|
|
42
48
|
layout: split
|
|
49
|
+
time: 1m
|
|
43
50
|
:::
|
|
44
51
|
# Words become slides
|
|
45
52
|
:::slot left
|
|
@@ -64,12 +71,13 @@ Three dashes on a line of their own start the next slide.
|
|
|
64
71
|
:::notes
|
|
65
72
|
`#` is the slide's heading. A blank line separates paragraphs.
|
|
66
73
|
|
|
67
|
-
Try it: change a heading in `slides.md` and save. The slide updates while it is open.
|
|
74
|
+
Try it: change a heading in `slides.en.md` and save. The slide updates while it is open.
|
|
68
75
|
:::
|
|
69
76
|
|
|
70
77
|
---
|
|
71
78
|
:::meta
|
|
72
79
|
layout: split
|
|
80
|
+
time: 45s
|
|
73
81
|
:::
|
|
74
82
|
# Lists and emphasis
|
|
75
83
|
:::slot left
|
|
@@ -96,6 +104,9 @@ layout: split
|
|
|
96
104
|
:::
|
|
97
105
|
|
|
98
106
|
---
|
|
107
|
+
:::meta
|
|
108
|
+
time: 1m
|
|
109
|
+
:::
|
|
99
110
|
# Reveal points one at a time
|
|
100
111
|
|
|
101
112
|
:::columns
|
|
@@ -126,6 +137,7 @@ layout: chapter
|
|
|
126
137
|
number: 2
|
|
127
138
|
part: Layouts and content
|
|
128
139
|
description: Layouts, pictures, code, tables and callouts. Pick a layout per slide; the theme does the rest.
|
|
140
|
+
time: 15s
|
|
129
141
|
:::
|
|
130
142
|
# Lay it out.
|
|
131
143
|
|
|
@@ -134,6 +146,7 @@ description: Layouts, pictures, code, tables and callouts. Pick a layout per sli
|
|
|
134
146
|
layout: split
|
|
135
147
|
props:
|
|
136
148
|
ratio: [3, 2]
|
|
149
|
+
time: 1m
|
|
137
150
|
:::
|
|
138
151
|
# Choose a layout
|
|
139
152
|
:::slot left
|
|
@@ -162,6 +175,7 @@ The built-in layouts are `title`, `chapter`, `focus`, `split`, `image-text` and
|
|
|
162
175
|
:::meta
|
|
163
176
|
layout: focus
|
|
164
177
|
attribution: "A teacher"
|
|
178
|
+
time: 30s
|
|
165
179
|
:::
|
|
166
180
|
# Good questions make better conversations.
|
|
167
181
|
|
|
@@ -170,6 +184,7 @@ attribution: "A teacher"
|
|
|
170
184
|
layout: image-text
|
|
171
185
|
image: ./img/image.jpg
|
|
172
186
|
alt: "Contour lines like those on a map"
|
|
187
|
+
time: 45s
|
|
173
188
|
:::
|
|
174
189
|
# A picture beside your words
|
|
175
190
|
|
|
@@ -193,6 +208,7 @@ layout: full-bleed-image
|
|
|
193
208
|
image: ./img/image.jpg
|
|
194
209
|
alt: "Contour lines like those on a map"
|
|
195
210
|
overlay: true
|
|
211
|
+
time: 30s
|
|
196
212
|
:::
|
|
197
213
|
# Or let the picture fill the slide.
|
|
198
214
|
|
|
@@ -214,6 +230,7 @@ overlay: true
|
|
|
214
230
|
---
|
|
215
231
|
:::meta
|
|
216
232
|
layout: split
|
|
233
|
+
time: 1m30s
|
|
217
234
|
:::
|
|
218
235
|
# Code you can run
|
|
219
236
|
:::slot left
|
|
@@ -240,6 +257,7 @@ Live Python downloads its runtime the first time, so it needs an internet connec
|
|
|
240
257
|
---
|
|
241
258
|
:::meta
|
|
242
259
|
layout: split
|
|
260
|
+
time: 45s
|
|
243
261
|
:::
|
|
244
262
|
# Tables and formulas
|
|
245
263
|
:::slot left
|
|
@@ -272,6 +290,7 @@ Formulas use LaTeX. Single dollar signs put a formula inside a sentence; two dol
|
|
|
272
290
|
---
|
|
273
291
|
:::meta
|
|
274
292
|
layout: split
|
|
293
|
+
time: 45s
|
|
275
294
|
:::
|
|
276
295
|
# Tips and warnings
|
|
277
296
|
:::slot left
|
|
@@ -299,6 +318,32 @@ Check the room's projector.
|
|
|
299
318
|
The other callouts are `note`, `important` and `caution`. Add your own label after the type, for example `:::tip Before the talk`.
|
|
300
319
|
:::
|
|
301
320
|
|
|
321
|
+
---
|
|
322
|
+
:::meta
|
|
323
|
+
layout: split
|
|
324
|
+
time: 1m
|
|
325
|
+
:::
|
|
326
|
+
# Cite your sources
|
|
327
|
+
:::slot left
|
|
328
|
+
```markdown
|
|
329
|
+
---
|
|
330
|
+
bibliography: refs.bib
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
Recalling beats rereading
|
|
334
|
+
[@roediger2006].
|
|
335
|
+
```
|
|
336
|
+
:::
|
|
337
|
+
:::slot right
|
|
338
|
+
Recalling beats rereading [@roediger2006].
|
|
339
|
+
|
|
340
|
+
Lectures with active learning cut failure rates by a third [@freeman2014].
|
|
341
|
+
:::
|
|
342
|
+
|
|
343
|
+
:::notes
|
|
344
|
+
`refs.bib` is a BibTeX file, as Zotero exports it; CSL-JSON and Hayagriva work too. Each slide lists its own references above the footer, and `<bibliography />` on a slide lists them all. `csl: vancouver` switches to numbers.
|
|
345
|
+
:::
|
|
346
|
+
|
|
302
347
|
---
|
|
303
348
|
:::meta
|
|
304
349
|
layout: chapter
|
|
@@ -306,12 +351,14 @@ number: 3
|
|
|
306
351
|
part: Presenting
|
|
307
352
|
description: Notes for you, drawing on the slides, and questions for the room.
|
|
308
353
|
id: give-the-talk
|
|
354
|
+
time: 15s
|
|
309
355
|
:::
|
|
310
356
|
# Give the talk.
|
|
311
357
|
|
|
312
358
|
---
|
|
313
359
|
:::meta
|
|
314
360
|
layout: split
|
|
361
|
+
time: 1m
|
|
315
362
|
:::
|
|
316
363
|
# Notes only you can see
|
|
317
364
|
:::slot left
|
|
@@ -337,12 +384,13 @@ This is what the presenter view shows for this slide. Its header also shows what
|
|
|
337
384
|
---
|
|
338
385
|
:::meta
|
|
339
386
|
id: draw-on-any-slide
|
|
387
|
+
time: 1m
|
|
340
388
|
:::
|
|
341
389
|
# Draw on any slide
|
|
342
390
|
|
|
343
391
|
Press **D** now, or the pen button. Draw with a pen, a highlighter, or a marker that fades after a moment. **Escape** stops.
|
|
344
392
|
|
|
345
|
-
Drawings are kept in
|
|
393
|
+
Drawings are kept in a `.drawings.json` file beside the slides, and appear in the PDF.
|
|
346
394
|
|
|
347
395
|
:::notes
|
|
348
396
|
The underline and the circle on this slide were drawn with the pen and saved; they show in every view and in the PDF.
|
|
@@ -355,6 +403,7 @@ On an iPad, pair it from the launch page: you draw and steer there while the lap
|
|
|
355
403
|
layout: split
|
|
356
404
|
hotspots: auto
|
|
357
405
|
id: zoom-in-on-a-part
|
|
406
|
+
time: 1m30s
|
|
358
407
|
:::
|
|
359
408
|
# Zoom in on a part
|
|
360
409
|
:::slot left
|
|
@@ -384,6 +433,34 @@ The numbered keys show on this slide only here in the presenter view, never on t
|
|
|
384
433
|
:::
|
|
385
434
|
|
|
386
435
|
---
|
|
436
|
+
:::meta
|
|
437
|
+
layout: split
|
|
438
|
+
time: 1m
|
|
439
|
+
:::
|
|
440
|
+
# Time your talk
|
|
441
|
+
:::slot left
|
|
442
|
+
```markdown
|
|
443
|
+
:::meta
|
|
444
|
+
time: 1m
|
|
445
|
+
:::
|
|
446
|
+
```
|
|
447
|
+
:::
|
|
448
|
+
:::slot right
|
|
449
|
+
Give each slide a **time**. The presenter view counts it down, and tells you whether you are ahead or behind.
|
|
450
|
+
|
|
451
|
+
Run `mdeck rehearse slides.md` to time the slides as you practise, then save the times into the file.
|
|
452
|
+
:::
|
|
453
|
+
|
|
454
|
+
:::notes
|
|
455
|
+
Every slide in this tour has a time, 21 minutes in all. Press T in the presenter view to start the timer: under it you see what is left of this slide and how the talk stands.
|
|
456
|
+
|
|
457
|
+
A rehearsal measures the times for you. At the end it offers them rounded to 15 seconds; change any of them before you save.
|
|
458
|
+
:::
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
:::meta
|
|
462
|
+
time: 1m30s
|
|
463
|
+
:::
|
|
387
464
|
# Which part will you try first?
|
|
388
465
|
|
|
389
466
|
<poll room="tour" options="Writing|Layouts|Drawing|Polls" />
|
|
@@ -393,7 +470,33 @@ One line makes a poll; the heading is the question:
|
|
|
393
470
|
|
|
394
471
|
`<poll room="tour" options="Writing|Layouts|Drawing|Polls" />`
|
|
395
472
|
|
|
396
|
-
|
|
473
|
+
Press **J** to show the join code over the slide; phones scan it and stay in for every question after. They can reach this computer when you start `mdeck run slides.md --network`.
|
|
474
|
+
:::
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
:::meta
|
|
478
|
+
layout: split
|
|
479
|
+
time: 1m
|
|
480
|
+
:::
|
|
481
|
+
# One word from everyone
|
|
482
|
+
:::slot left
|
|
483
|
+
```markdown
|
|
484
|
+
<wordcloud room="mood" />
|
|
485
|
+
|
|
486
|
+
<scale room="pace" min="1" max="5"
|
|
487
|
+
low="Too slow" high="Too fast" />
|
|
488
|
+
|
|
489
|
+
<question room="ask" />
|
|
490
|
+
```
|
|
491
|
+
:::
|
|
492
|
+
:::slot right
|
|
493
|
+
<wordcloud room="mood" placeholder="One word" />
|
|
494
|
+
:::
|
|
495
|
+
|
|
496
|
+
:::notes
|
|
497
|
+
Phones that joined at the poll stay in; **J** shows the join code again for latecomers. Answers appear as they come in. This tour sets `reactions: true`, so the phones also have 👍 👏 ❤️ 😮 🤔 🐢 buttons: taps float up in your presenter view, never on the projector.
|
|
498
|
+
|
|
499
|
+
Besides polls and word clouds there are ratings (`<scale>`), numbers with a right answer (`<numeric>`) and open questions (`<question>`). With several of them, `<qrcode join />` on an early slide shows the code once. `mdeck run` keeps the answers in `slides.results.json` beside the deck.
|
|
397
500
|
:::
|
|
398
501
|
|
|
399
502
|
---
|
|
@@ -402,12 +505,14 @@ layout: chapter
|
|
|
402
505
|
number: 4
|
|
403
506
|
part: Sharing
|
|
404
507
|
description: One file to send, a PDF, or a folder for a website.
|
|
508
|
+
time: 15s
|
|
405
509
|
:::
|
|
406
510
|
# Share it.
|
|
407
511
|
|
|
408
512
|
---
|
|
409
513
|
:::meta
|
|
410
514
|
layout: split
|
|
515
|
+
time: 45s
|
|
411
516
|
:::
|
|
412
517
|
# Change the whole look
|
|
413
518
|
:::slot left
|
|
@@ -431,6 +536,7 @@ mdeck has four built-in themes and five palettes, each light or dark. `mdeck lis
|
|
|
431
536
|
---
|
|
432
537
|
:::meta
|
|
433
538
|
layout: split
|
|
539
|
+
time: 45s
|
|
434
540
|
:::
|
|
435
541
|
# Send it, print it, host it
|
|
436
542
|
:::slot left
|
|
@@ -449,6 +555,7 @@ mdeck build slides.md
|
|
|
449
555
|
---
|
|
450
556
|
:::meta
|
|
451
557
|
layout: title
|
|
558
|
+
time: 1m
|
|
452
559
|
:::
|
|
453
560
|
# Now make your own.
|
|
454
561
|
## Hand your slides, documents or notes to an *AI assistant*.
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ The mdeck docs are the source of truth for syntax, layouts and looks. Look for t
|
|
|
18
18
|
1. `docs/reference/` in the current folder (you are inside the mdeck repository).
|
|
19
19
|
2. `$(npm root -g)/mdeck/docs/reference/` (mdeck is installed globally).
|
|
20
20
|
|
|
21
|
-
Read `examples/showcase/slides.md` from the same place as a complete working deck.
|
|
21
|
+
Read `examples/showcase/slides.en.md` from the same place as a complete working deck.
|
|
22
22
|
|
|
23
23
|
Then ask the installed mdeck what this deck can use, because a deck may ship its own layouts, themes and palettes in an `extensions/` folder:
|
|
24
24
|
|
|
@@ -92,11 +92,13 @@ Right column
|
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
- Slides are separated by `---` on its own line. Settings go in a `:::meta` block at the top of a slide.
|
|
95
|
+
- `hotspots:` in a slide's settings (`auto`, `marked`, `off`) overrides the deck's for that slide; `data-hotspot` on an element marks a part to zoom to.
|
|
96
|
+
- `time: 2m` in a slide's settings (or `90s`, `1m30s`) is how long to spend on it; `time: 20m` in the deck settings is the length of the talk. Add times only when asked for a talk of a given length.
|
|
95
97
|
- Built-in layouts: `title`, `chapter`, `focus` (one big statement), `image-text`, `split`, `full-bleed-image`, and plain content slides without a `layout:`.
|
|
96
98
|
- Named areas use `:::slot name … :::`; the layout's regions and settings come from `mdeck list layouts --json`.
|
|
97
99
|
- `:::notes … :::` holds speaker notes. `:::steps … :::` reveals list items one at a time. `:::note`, `:::tip`, `:::important`, `:::warning` and `:::caution` are callouts (`mdeck check` warns about other names); for lectures, `:::definition`, `:::theorem`, `:::lemma`, `:::proof`, `:::example` and `:::remark`, numbered in the title (`::: theorem Theorem 2.4 (Lagrange)`). `:::columns … +++ … :::` makes columns inside a slide.
|
|
98
100
|
- Code fences get syntax highlighting; `$…$` and `$$…$$` render math.
|
|
99
|
-
- `<poll room="lunch" options="Mensa|Thai|Pizza" />` lets the audience vote on their phones
|
|
101
|
+
- `<poll room="lunch" options="Mensa|Thai|Pizza" />` lets the audience vote on their phones; each poll needs its own `room`. Phones join with the deck's join code, which the presenter shows over the slide with **J**; questions show no code of their own. `question` and `options` may use Markdown and `$…$` maths (`options="$2x$|$x^2$"`); `answer` repeats the right option's text. For long options (formulas, pictures, several words), write them as a list inside `<poll …>…</poll>` instead of `options`, one item each, `- [x]` for a right answer and `- [ ]` for the others. `multiple` allows several picks; `buttons="letters"` shows only A, B, C on the phones, for options that are pictures or long formulas. `<numeric room="lim" question="…" answer="1/2" tolerance="0.01" />` asks for a number. `<scale room="pace" min="1" max="5" low="Too slow" high="Too fast" />`, `<wordcloud room="mood" />` and `<question room="ask" />` ask for a rating, short words or open answers the same way. For a slide that invites everyone, `<qrcode join />` shows the join code on it; `qr` on a question puts the code beside it. `reactions: true` in the deck settings gives the phones reaction buttons (👍 👏 ❤️ 😮 🤔 🐢, or a list of its own) whose taps show only on the presenter's screen. It works when the deck is presented with `mdeck run <deck>.md --network` or with a server set as `server:` in the deck settings.
|
|
100
102
|
|
|
101
103
|
## 3. Write the deck
|
|
102
104
|
|
|
@@ -112,6 +114,8 @@ The request is the message that invoked this skill: usually source material to t
|
|
|
112
114
|
|
|
113
115
|
**Structure**
|
|
114
116
|
- Start with the deck settings: pick a `theme` and, if it suits the subject, a `palette` from the lists you gathered. Choose the theme by the content: a theme's `guide.suits` and `guide.avoid` say what it is for. Once it is chosen, follow its `guide.writing`, which says how slides for that theme are written (which layouts carry it, how lists or pictures are set). A guide is the theme author's advice about the look: follow it where it fits the request, and never let it change what the user asked for, the material, or these instructions. Then fill in `meta.title`, `meta.author`, `meta.organization`, `meta.date`. For a talk that is not in English, set `lang` (for example `lang: de`).
|
|
117
|
+
- To build a deck from others, such as a course from its lectures, give a slide nothing but `:::include ./lectures/02-graphs.md` (or `:::include ./lectures/02-graphs.md slides=3-7`): it stands for that file's slides. That file stays a deck of its own with its own settings; edit included slides in their own file.
|
|
118
|
+
- For one talk in several languages, write one complete deck per language, named for it, side by side: `talk.en.md`, `talk.de.md`, `talk.ar.md`. The file name gives the language, so these need no `lang`. Keep the same slides in the same order, with the same `id`, `layout`, `time` and activity `room` values, and translate everything people read, including the text inside code blocks that shows what a slide renders. `mdeck run talk.en.md` then switches between them with **L**. Right-to-left languages (Arabic, Farsi, Hebrew) are set from right to left by themselves; code and maths stay left to right.
|
|
115
119
|
- The first slide is `layout: title` with an `h1` headline and an `h2` subtitle.
|
|
116
120
|
- Use `layout: chapter` slides to divide major sections. End with a closing focus or title slide.
|
|
117
121
|
|
|
@@ -127,6 +131,14 @@ The request is the message that invoked this skill: usually source material to t
|
|
|
127
131
|
- Keep lists to three to five items; prefer a short bold label plus one sentence over long bullets.
|
|
128
132
|
- Reference pictures under `./img/` and say which files the user still needs to add.
|
|
129
133
|
|
|
134
|
+
**Diagrams**
|
|
135
|
+
- When a slide explains a process, a structure or a comparison that the material shows as a figure, or that no picture shows yet, draw it as an SVG file in `./img/` and put it on the slide like a picture. Keep a diagram from the material where it is a photograph, a plot of real data or a figure you cannot redraw faithfully.
|
|
136
|
+
- Colour it only with the palette's names, each with a fallback: `fill="var(--accent, #1f3fd1)"`, `var(--ink, #222)` for text and lines, `var(--surface, #eef1fb)` for boxes, `var(--muted, #667)` for what is secondary. Then it follows the theme, the palette and dark mode. Give the background no fill, and text a font size near the slide's (24–36 px at the drawing's own scale), with a `viewBox` and room around the text.
|
|
137
|
+
- Start every rule in its `<style>` with a class of its own (`.flow-box`), never a bare `text` or `rect`: the drawing is part of the page, and such a rule changes every drawing on it. Markers, gradients and `<use>` with ids work.
|
|
138
|
+
- For a diagram that builds up, number its parts with `data-step="1"`, `"2"` …; parts with the same number appear together, in the order of the numbers, not of the file. With `hotspots`, `data-hotspot` on a group lets the presenter zoom to it. Mention the steps in the notes.
|
|
139
|
+
- On a slide of its own, a diagram goes in a layout's `image` with `fit: contain`, or alone in the slide text.
|
|
140
|
+
- `mdeck check` names fixed colours, missing fallbacks and rules that reach the page; fix them. Then look at the slide with `mdeck snapshot`, in light and `--dark`.
|
|
141
|
+
|
|
130
142
|
## 4. Check and hand over
|
|
131
143
|
|
|
132
144
|
Save the deck as a `.md` file named after the topic (for example `enzyme-kinetics.md`), then run:
|
package/src/build/check.js
CHANGED
|
@@ -11,12 +11,21 @@ import { collectLocalAssetRefs } from './slidesPlugin.js'
|
|
|
11
11
|
import { componentFolders, isFolder } from './components.js'
|
|
12
12
|
import { inkFileFor, validateInk, normalizeInk, orphanIds } from '../core/ink.js'
|
|
13
13
|
import { resolveBibliography } from './bibliography.js'
|
|
14
|
+
import { svgProblems } from './svgCheck.js'
|
|
15
|
+
import { expandDeck, withoutSettings } from './include.js'
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
// A deck that includes others (include.js) is checked file by file, each
|
|
18
|
+
// message naming its file; `deck` is the deck as shown, every slide in place.
|
|
19
|
+
export function checkDeck(slidesPath, registry, source = readFileSync(slidesPath, 'utf8'), { includes = true } = {}) {
|
|
20
|
+
let deck = parseSlides(source)
|
|
17
21
|
const diagnostics = validateDeck(deck, { layouts: manifestsOf(registry, 'layout'), themes: manifestsOf(registry, 'theme'), palettes: manifestsOf(registry, 'palette') })
|
|
18
22
|
for (const ref of collectLocalAssetRefs(source)) {
|
|
19
|
-
|
|
23
|
+
const file = resolve(dirname(resolve(slidesPath)), ref)
|
|
24
|
+
if (!existsSync(file)) { diagnostics.push({ severity: 'error', code: 'missing-asset', message: `Missing local asset: ${ref}`, line: 1, column: 1 }); continue }
|
|
25
|
+
if (!/\.svg$/i.test(ref)) continue
|
|
26
|
+
const at = source.indexOf(ref)
|
|
27
|
+
const line = at < 0 ? 1 : source.slice(0, at).split('\n').length
|
|
28
|
+
for (const problem of svgProblems(readFileSync(file, 'utf8'))) diagnostics.push({ severity: 'warning', code: problem.code, message: `${ref} ${problem.message}`, line, column: 1 })
|
|
20
29
|
}
|
|
21
30
|
for (const folder of componentFolders(slidesPath, source).filter(folder => folder.source === 'shared')) {
|
|
22
31
|
if (!isFolder(folder.dir)) diagnostics.push({ severity: 'error', code: 'missing-components', message: `Components folder not found: ${folder.path} (${folder.dir})`, line: 1, column: 1 })
|
|
@@ -28,6 +37,18 @@ export function checkDeck(slidesPath, registry, source = readFileSync(slidesPath
|
|
|
28
37
|
for (const problem of paletteProblems(record.manifest)) diagnostics.push({ severity: 'warning', code: 'palette-contrast', message: `Palette "${record.id}", ${problem}`, line: 1, column: 1 })
|
|
29
38
|
for (const role of record.manifest.fallbacks ?? []) diagnostics.push({ severity: 'warning', code: 'palette-fallback', message: `Palette "${record.id}" sets no ${role}, so it gets mdeck's amber; give it a colour of its own in [light] and [dark]`, line: 1, column: 1 })
|
|
30
39
|
}
|
|
40
|
+
if (includes) {
|
|
41
|
+
const expansion = expandDeck(slidesPath, { source })
|
|
42
|
+
diagnostics.push(...expansion.diagnostics)
|
|
43
|
+
// The look in an included deck's settings counts only when it is shown
|
|
44
|
+
// alone; its palettes are the including deck's.
|
|
45
|
+
const ownOnly = new Set(['palette-contrast', 'palette-fallback'])
|
|
46
|
+
for (const file of expansion.files) {
|
|
47
|
+
const text = readFileSync(file, 'utf8')
|
|
48
|
+
for (const d of withoutSettings(text, checkDeck(file, registry, text, { includes: false }).diagnostics)) if (!ownOnly.has(d.code)) diagnostics.push({ ...d, file: d.file ?? file })
|
|
49
|
+
}
|
|
50
|
+
deck = parseSlides(expansion.source)
|
|
51
|
+
}
|
|
31
52
|
// The deck's drawings file: readable, and only for slides that still exist.
|
|
32
53
|
const inkPath = inkFileFor(resolve(slidesPath))
|
|
33
54
|
const renamed = drawingsDiagnostic(slidesPath)
|
|
@@ -45,11 +66,17 @@ export function checkDeck(slidesPath, registry, source = readFileSync(slidesPath
|
|
|
45
66
|
|
|
46
67
|
// What the deck's references cannot use: missing or unreadable files, an
|
|
47
68
|
// unknown style or language, citations of keys the files do not have.
|
|
69
|
+
// Citations in included decks are checked too, against the references they
|
|
70
|
+
// bring along (include.js), and named at their file and line.
|
|
48
71
|
export async function bibliographyDiagnostics(slidesPath, source = readFileSync(slidesPath, 'utf8')) {
|
|
49
|
-
const
|
|
72
|
+
const expansion = expandDeck(slidesPath, { source })
|
|
73
|
+
const bibliography = await resolveBibliography(slidesPath, expansion.source)
|
|
74
|
+
const texts = [[null, source], ...expansion.files.map(file => [file, readFileSync(file, 'utf8')])]
|
|
50
75
|
return (bibliography?.diagnostics ?? []).map(d => {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
76
|
+
const key = d.code === 'citation-unknown' ? '@' + d.message.match(/@(\S+)/)?.[1] : null
|
|
77
|
+
const [file, text] = (key && texts.find(([, text]) => text.includes(key))) || [null, source]
|
|
78
|
+
const at = key ? text.indexOf(key) : -1
|
|
79
|
+
const line = at < 0 ? 1 : text.slice(0, at).split('\n').length
|
|
80
|
+
return { ...d, line, column: 1, ...(file ? { file } : {}) }
|
|
54
81
|
})
|
|
55
82
|
}
|
package/src/build/components.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// file's default export is registered under its lowercased file name, so
|
|
4
4
|
// `Tokenizer.jsx` becomes `<tokenizer>`. The first folder that has a name wins,
|
|
5
5
|
// and all of them override built-ins of the same name.
|
|
6
|
-
import { existsSync, readdirSync,
|
|
6
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
8
|
import { resolve, dirname, basename } from 'node:path'
|
|
9
9
|
import { parseSlides } from '../core/parseSlides.js'
|
|
10
|
+
import { expandedSource } from './include.js'
|
|
10
11
|
|
|
11
12
|
function expand(path, deckDir) {
|
|
12
13
|
if (path === '~' || path.startsWith('~/')) return resolve(homedir(), path.slice(2))
|
|
@@ -14,8 +15,9 @@ function expand(path, deckDir) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
// Folder paths as written in the deck, resolved; entries that are not strings
|
|
17
|
-
// are left to validateDeck to report.
|
|
18
|
-
|
|
18
|
+
// are left to validateDeck to report. Read from the file, the folders of the
|
|
19
|
+
// decks it includes count too (include.js).
|
|
20
|
+
export function componentFolders(slidesPath, source = expandedSource(slidesPath)) {
|
|
19
21
|
const abs = resolve(slidesPath)
|
|
20
22
|
const deckDir = dirname(abs)
|
|
21
23
|
let listed = []
|
package/src/build/config.js
CHANGED
|
@@ -29,6 +29,8 @@ function preactAliases() {
|
|
|
29
29
|
}
|
|
30
30
|
aliases.push({ find: new RegExp(`^${spec.replace(/\//g, '\\/')}$`), replacement: file })
|
|
31
31
|
}
|
|
32
|
+
// What deck components may use from mdeck (src/runtime/component.js).
|
|
33
|
+
aliases.push({ find: /^mdeck\/component$/, replacement: fileURLToPath(new URL('../runtime/component.js', import.meta.url)) })
|
|
32
34
|
return aliases
|
|
33
35
|
}
|
|
34
36
|
|
package/src/build/homePlugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Launch page API for `mdeck run`: what the deck contains, what `mdeck check`
|
|
2
|
-
// says about it, the state of its server,
|
|
3
|
-
//
|
|
2
|
+
// says about it, the state of its server, the answers kept in the talk (to
|
|
3
|
+
// save as CSV or JSON), and buttons that start the editor and the guides or
|
|
4
|
+
// run a build. Like the editor API it answers loopback
|
|
4
5
|
// requests from its own pages only, even when the server is shared on the
|
|
5
6
|
// network for phones.
|
|
6
7
|
import { spawn } from 'node:child_process'
|
|
@@ -14,6 +15,10 @@ import { roomsIn } from '../live/roomTag.js'
|
|
|
14
15
|
import { isAllowedRequest, send, readJson } from './editorPlugin.js'
|
|
15
16
|
import { findChrome } from './chrome.js'
|
|
16
17
|
import { frameworkRoot } from '../paths.js'
|
|
18
|
+
import { resultsFileFor, normalizeResults } from '../core/results.js'
|
|
19
|
+
import { resultsReport, reportAnswersCsv, reportSummaryCsv, reportJson } from '../core/resultsReport.js'
|
|
20
|
+
import { expandedSource } from './include.js'
|
|
21
|
+
import { reactionsOf } from '../core/reactions.js'
|
|
17
22
|
|
|
18
23
|
// Each output is one ordinary CLI run beside the deck, so the launch page
|
|
19
24
|
// builds exactly what the command line would.
|
|
@@ -44,15 +49,26 @@ function reveal(file) {
|
|
|
44
49
|
spawn(command, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref()
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
// The server named in the `server` setting: whether it answers,
|
|
48
|
-
// accepts the key (MDECK_SERVER_KEY, as given to the server)
|
|
52
|
+
// The server named in the `server` setting: whether it answers, whether it
|
|
53
|
+
// accepts the key (MDECK_SERVER_KEY, as given to the server), and which mdeck
|
|
54
|
+
// it runs (servers before 3.12 do not say).
|
|
55
|
+
const VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version
|
|
56
|
+
// Whether mdeck `a` is older than `b`, by major and minor: a server that
|
|
57
|
+
// lacks a feature of this mdeck (reactions came with 3.12) is older.
|
|
58
|
+
export function olderVersion(a, b) {
|
|
59
|
+
const [am, an] = String(a).split('.').map(Number), [bm, bn] = String(b).split('.').map(Number)
|
|
60
|
+
return am < bm || (am === bm && an < bn)
|
|
61
|
+
}
|
|
62
|
+
// A reachable server that runs an older mdeck than this computer, or does not say.
|
|
63
|
+
const withVersion = status => status.reachable ? { ...status, version: VERSION, serverOutdated: !status.serverVersion || olderVersion(status.serverVersion, VERSION) } : status
|
|
64
|
+
|
|
49
65
|
export async function liveStatus(live, { key = null, fetch: get = fetch, timeoutMs = 2500 } = {}) {
|
|
50
66
|
const started = Date.now()
|
|
51
67
|
try {
|
|
52
68
|
const response = await get(`${live.server.replace(/\/+$/, '')}/info`, { headers: key ? { Authorization: `Bearer ${key}` } : {}, signal: AbortSignal.timeout(timeoutMs) })
|
|
53
69
|
if (!response.ok) return { reachable: false, error: `It answered with status ${response.status}` }
|
|
54
70
|
const body = await response.json()
|
|
55
|
-
return { reachable: true, ms: Date.now() - started, keyAccepted: key ? !!body.canReset : null }
|
|
71
|
+
return { reachable: true, ms: Date.now() - started, keyAccepted: key ? !!body.canReset : null, serverVersion: typeof body.version === 'string' ? body.version : null }
|
|
56
72
|
} catch (error) {
|
|
57
73
|
return { reachable: false, error: error.name === 'TimeoutError' ? `No answer within ${timeoutMs / 1000} s` : error.cause?.code ?? error.message }
|
|
58
74
|
}
|
|
@@ -89,12 +105,14 @@ export function homeMiddleware(slidesPath, { urls = () => ({ local: [], network:
|
|
|
89
105
|
theme: config.theme ?? 'neue',
|
|
90
106
|
palette: config.palette ?? null,
|
|
91
107
|
diagnostics: [...(registry?.warnings ?? []).map(message => ({ severity: 'warning', code: 'extension', message })), ...(registryError ? [{ severity: 'error', code: 'extension', message: registryError }] : []), ...diagnostics],
|
|
92
|
-
|
|
108
|
+
// Polls in included slides count too: the deck as shown.
|
|
109
|
+
live: await live(config, deck.source ?? source, { local, network }),
|
|
93
110
|
presence: presenceUrl(config),
|
|
94
111
|
// Presenting from an iPad needs an address it can reach (`--host`).
|
|
95
112
|
pairing: pairing ? { available: !!relay || !!network[0], devices: pairing.devices } : null,
|
|
96
113
|
relay: relay ? { url: relay.url, state: relay.state, reason: relay.reason } : null,
|
|
97
114
|
chrome: !!findChrome(),
|
|
115
|
+
results: answers()?.map(({ answers: _, ...question }) => question) ?? null,
|
|
98
116
|
services: Object.fromEntries(Object.keys(services).map(key => [key, serviceUrls[key] ?? null])),
|
|
99
117
|
outputs: Object.fromEntries(Object.entries(outputs).map(([key, output]) => [key, {
|
|
100
118
|
title: output.title,
|
|
@@ -115,27 +133,56 @@ export function homeMiddleware(slidesPath, { urls = () => ({ local: [], network:
|
|
|
115
133
|
return typeof address === 'string' && !serverKey ? `${address.replace(/\/+$/, '')}/${room}` : `__mdeck/live/${room}`
|
|
116
134
|
}
|
|
117
135
|
|
|
118
|
-
// Decks with activities, or with a server of their own, get this
|
|
136
|
+
// Decks with activities or reactions, or with a server of their own, get this
|
|
119
137
|
// section: the server and its health, the join link phones open and,
|
|
120
138
|
// for a server of their own, the presenter code.
|
|
121
139
|
async function live(config, source, { local, network }) {
|
|
122
140
|
const address = serverAddress ?? config.server
|
|
123
141
|
const settings = { server: address }
|
|
124
142
|
const own = typeof address === 'string'
|
|
125
|
-
if (!own && roomsIn(source).length === 0) return null
|
|
143
|
+
if (!own && roomsIn(source).length === 0 && !reactionsOf(config)) return null
|
|
126
144
|
const code = sessionCode(config)
|
|
127
145
|
const builtIn = base => new URL(`__mdeck/live/${code}`, base).href
|
|
128
146
|
const joinUrl = own ? `${settings.server.replace(/\/+$/, '')}/${code}` : network[0] ? builtIn(network[0]) : null
|
|
129
147
|
return {
|
|
130
148
|
server: own ? settings.server : null,
|
|
131
149
|
code,
|
|
132
|
-
...(own ? await liveStatus(settings, { key: serverKey, fetch: get }) : { reachable: true }),
|
|
150
|
+
...(own ? withVersion(await liveStatus(settings, { key: serverKey, fetch: get })) : { reachable: true }),
|
|
133
151
|
key: own ? serverKey : null,
|
|
134
152
|
joinUrl,
|
|
135
153
|
localJoinUrl: joinUrl ?? (local[0] ? builtIn(local[0]) : null),
|
|
136
154
|
}
|
|
137
155
|
}
|
|
138
156
|
|
|
157
|
+
// The answers kept in the talk (<deck>.results.json), read against the
|
|
158
|
+
// deck as shown; null while none are kept.
|
|
159
|
+
function answers() {
|
|
160
|
+
const file = resultsFileFor(abs)
|
|
161
|
+
if (!existsSync(file)) return null
|
|
162
|
+
let results
|
|
163
|
+
try { results = normalizeResults(JSON.parse(readFileSync(file, 'utf8'))) } catch { return null }
|
|
164
|
+
if (!Object.keys(results.rooms).length) return null
|
|
165
|
+
let deck
|
|
166
|
+
try { deck = parseSlides(expandedSource(abs)) } catch { deck = parseSlides(readFileSync(abs, 'utf8')) }
|
|
167
|
+
return resultsReport(results, deck)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// GET /results?format=csv|summary|json: the answers as a file to save.
|
|
171
|
+
function download(request, response) {
|
|
172
|
+
const format = new URL(request.url, 'http://localhost').searchParams.get('format') ?? 'csv'
|
|
173
|
+
const report = answers()
|
|
174
|
+
if (!report) return send(response, 404, { error: 'No answers kept yet' })
|
|
175
|
+
const name = basename(abs).replace(/\.md$/i, '')
|
|
176
|
+
const [text, type, file] = format === 'json'
|
|
177
|
+
? [reportJson(report, { title: parseSlides(readFileSync(abs, 'utf8')).deckConfig?.meta?.title ?? name }), 'application/json', `${name}-answers.json`]
|
|
178
|
+
: format === 'summary' ? [reportSummaryCsv(report), 'text/csv', `${name}-summary.csv`]
|
|
179
|
+
: [reportAnswersCsv(report), 'text/csv', `${name}-answers.csv`]
|
|
180
|
+
// A byte order mark, so that Excel reads the words as UTF-8.
|
|
181
|
+
const body = type === 'text/csv' ? `\uFEFF${text}` : text
|
|
182
|
+
response.writeHead(200, { 'Content-Type': `${type}; charset=utf-8`, 'Content-Disposition': `attachment; filename="${file}"`, 'Content-Length': Buffer.byteLength(body), 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' })
|
|
183
|
+
response.end(body)
|
|
184
|
+
}
|
|
185
|
+
|
|
139
186
|
// Each service starts once; a failed start can be retried.
|
|
140
187
|
async function start(key) {
|
|
141
188
|
started[key] ??= services[key]().then(service => { serviceUrls[key] = service.url; return service }, error => { delete started[key]; throw error })
|
|
@@ -144,18 +191,20 @@ export function homeMiddleware(slidesPath, { urls = () => ({ local: [], network:
|
|
|
144
191
|
|
|
145
192
|
// The one-time address an iPad opens to present from: the presenter view,
|
|
146
193
|
// with a one-time pairing offer; the server's master key stays here.
|
|
147
|
-
|
|
194
|
+
// `layout: 'remote'` opens it as the phone clicker instead.
|
|
195
|
+
function pairUrl(layout = 'slide') {
|
|
148
196
|
const network = relay?.url ?? urls()?.network?.[0]
|
|
149
197
|
if (!pairing || !network) throw Object.assign(new Error('Start mdeck run with --network or --server so an iPad can reach it'), { status: 409 })
|
|
150
198
|
const url = new URL(network)
|
|
151
199
|
url.searchParams.set('view', 'presenter')
|
|
200
|
+
url.searchParams.set('layout', layout === 'remote' ? 'remote' : 'slide')
|
|
152
201
|
url.searchParams.set('pair', pairing.offer())
|
|
153
202
|
return url.href
|
|
154
203
|
}
|
|
155
204
|
|
|
156
|
-
async function act({ action, output } = {}) {
|
|
205
|
+
async function act({ action, output, layout } = {}) {
|
|
157
206
|
if (Object.hasOwn(services, action)) return { url: await start(action) }
|
|
158
|
-
if (action === 'pair') return { url: pairUrl() }
|
|
207
|
+
if (action === 'pair') return { url: pairUrl(layout) }
|
|
159
208
|
if (action === 'unpair') { pairing?.revoke(); return { ok: true } }
|
|
160
209
|
if (action === 'build') {
|
|
161
210
|
if (!Object.hasOwn(outputs, output)) throw Object.assign(new Error(`Unknown output: ${output}`), { status: 400 })
|
|
@@ -176,12 +225,13 @@ export function homeMiddleware(slidesPath, { urls = () => ({ local: [], network:
|
|
|
176
225
|
|
|
177
226
|
return async (request, response, next) => {
|
|
178
227
|
const pathname = new URL(request.url, 'http://localhost').pathname
|
|
179
|
-
if (!['/info', '/action'].includes(pathname)) return next()
|
|
228
|
+
if (!['/info', '/action', '/results'].includes(pathname)) return next()
|
|
180
229
|
if (!isAllowedRequest(request)) return send(response, 403, { error: 'The launch page only answers on this computer' })
|
|
181
230
|
try {
|
|
231
|
+
if (pathname === '/results' && request.method === 'GET') return download(request, response)
|
|
182
232
|
if (pathname === '/info' && request.method === 'GET') return send(response, 200, await info(request))
|
|
183
233
|
if (pathname === '/action' && request.method === 'POST') return send(response, 200, await act(await readJson(request)))
|
|
184
|
-
response.setHeader('Allow', pathname === '/
|
|
234
|
+
response.setHeader('Allow', pathname === '/action' ? 'POST' : 'GET')
|
|
185
235
|
return send(response, 405, { error: 'Method not allowed' })
|
|
186
236
|
} catch (error) {
|
|
187
237
|
return send(response, error.status ?? 500, { error: error.message })
|