mdeck 3.10.1 → 3.11.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 +23 -0
- package/README.md +13 -2
- package/assets/base.css +4 -1
- package/docs/reference/authoring.md +65 -4
- package/docs/site/assets/site.css +2 -0
- package/docs/site/content/commands.md +3 -1
- package/docs/site/content/drawing.md +1 -1
- package/docs/site/content/install.md +47 -0
- package/docs/site/content/media.md +20 -1
- package/docs/site/content/presenting.md +39 -2
- package/docs/site/content/sharing.md +1 -0
- package/docs/site/content/welcome.md +21 -1
- 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.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 +10 -1
- package/src/build/check.js +7 -1
- package/src/build/config.js +2 -0
- package/src/build/slidesPlugin.js +6 -6
- package/src/build/svgCheck.js +65 -0
- package/src/build/timingPlugin.js +80 -0
- package/src/cli/args.js +4 -3
- package/src/cli/index.js +34 -11
- package/src/components/VideoPlayer.jsx +6 -1
- package/src/core/editDeck.js +44 -7
- package/src/core/icons.js +3 -0
- package/src/core/labels.js +75 -0
- package/src/core/parseSlides.js +3 -3
- package/src/core/timing.js +83 -0
- package/src/core/validateDeck.js +5 -0
- package/src/editor/components/DeckSettings.jsx +1 -0
- package/src/editor/components/MetaFields.jsx +1 -0
- package/src/home/Home.jsx +1 -0
- package/src/layouts/layoutApi.jsx +51 -8
- package/src/runtime/RehearsalSummary.jsx +129 -0
- package/src/runtime/component.js +27 -0
- package/src/runtime/deck-stage.js +49 -20
- package/src/runtime/ink/bus.js +13 -5
- package/src/runtime/ink/outbox.js +27 -0
- package/src/runtime/ink/room.js +31 -6
- package/src/runtime/ink/segments.js +21 -0
- package/src/runtime/main.jsx +157 -28
- package/src/runtime/shared.js +77 -0
- package/src/runtime/steps.js +36 -0
- package/src/runtime/useTalkTimer.js +65 -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,11 @@
|
|
|
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
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
---
|
|
@@ -14,14 +15,17 @@ image: ./img/mdeck.svg
|
|
|
14
15
|
fit: contain
|
|
15
16
|
alt: "A Markdown file turning into a stack of slides"
|
|
16
17
|
id: a-talk-in-a-text-file
|
|
18
|
+
time: 1m
|
|
17
19
|
:::
|
|
18
20
|
# A talk in a text file.
|
|
19
21
|
## A tour of mdeck: each slide shows *what is written* and what it becomes.
|
|
20
22
|
|
|
21
23
|
:::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.
|
|
24
|
+
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
25
|
|
|
24
26
|
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.
|
|
27
|
+
|
|
28
|
+
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
29
|
:::
|
|
26
30
|
|
|
27
31
|
---
|
|
@@ -30,6 +34,7 @@ layout: chapter
|
|
|
30
34
|
number: 1
|
|
31
35
|
part: Writing
|
|
32
36
|
description: Plain Markdown, the format AI assistants already write. You can read and change every word.
|
|
37
|
+
time: 15s
|
|
33
38
|
:::
|
|
34
39
|
# Write it.
|
|
35
40
|
|
|
@@ -40,6 +45,7 @@ In practice an AI assistant usually writes this file from your own material: old
|
|
|
40
45
|
---
|
|
41
46
|
:::meta
|
|
42
47
|
layout: split
|
|
48
|
+
time: 1m
|
|
43
49
|
:::
|
|
44
50
|
# Words become slides
|
|
45
51
|
:::slot left
|
|
@@ -64,12 +70,13 @@ Three dashes on a line of their own start the next slide.
|
|
|
64
70
|
:::notes
|
|
65
71
|
`#` is the slide's heading. A blank line separates paragraphs.
|
|
66
72
|
|
|
67
|
-
Try it: change a heading in `slides.md` and save. The slide updates while it is open.
|
|
73
|
+
Try it: change a heading in `slides.en.md` and save. The slide updates while it is open.
|
|
68
74
|
:::
|
|
69
75
|
|
|
70
76
|
---
|
|
71
77
|
:::meta
|
|
72
78
|
layout: split
|
|
79
|
+
time: 45s
|
|
73
80
|
:::
|
|
74
81
|
# Lists and emphasis
|
|
75
82
|
:::slot left
|
|
@@ -96,6 +103,9 @@ layout: split
|
|
|
96
103
|
:::
|
|
97
104
|
|
|
98
105
|
---
|
|
106
|
+
:::meta
|
|
107
|
+
time: 1m
|
|
108
|
+
:::
|
|
99
109
|
# Reveal points one at a time
|
|
100
110
|
|
|
101
111
|
:::columns
|
|
@@ -126,6 +136,7 @@ layout: chapter
|
|
|
126
136
|
number: 2
|
|
127
137
|
part: Layouts and content
|
|
128
138
|
description: Layouts, pictures, code, tables and callouts. Pick a layout per slide; the theme does the rest.
|
|
139
|
+
time: 15s
|
|
129
140
|
:::
|
|
130
141
|
# Lay it out.
|
|
131
142
|
|
|
@@ -134,6 +145,7 @@ description: Layouts, pictures, code, tables and callouts. Pick a layout per sli
|
|
|
134
145
|
layout: split
|
|
135
146
|
props:
|
|
136
147
|
ratio: [3, 2]
|
|
148
|
+
time: 1m
|
|
137
149
|
:::
|
|
138
150
|
# Choose a layout
|
|
139
151
|
:::slot left
|
|
@@ -162,6 +174,7 @@ The built-in layouts are `title`, `chapter`, `focus`, `split`, `image-text` and
|
|
|
162
174
|
:::meta
|
|
163
175
|
layout: focus
|
|
164
176
|
attribution: "A teacher"
|
|
177
|
+
time: 30s
|
|
165
178
|
:::
|
|
166
179
|
# Good questions make better conversations.
|
|
167
180
|
|
|
@@ -170,6 +183,7 @@ attribution: "A teacher"
|
|
|
170
183
|
layout: image-text
|
|
171
184
|
image: ./img/image.jpg
|
|
172
185
|
alt: "Contour lines like those on a map"
|
|
186
|
+
time: 45s
|
|
173
187
|
:::
|
|
174
188
|
# A picture beside your words
|
|
175
189
|
|
|
@@ -193,6 +207,7 @@ layout: full-bleed-image
|
|
|
193
207
|
image: ./img/image.jpg
|
|
194
208
|
alt: "Contour lines like those on a map"
|
|
195
209
|
overlay: true
|
|
210
|
+
time: 30s
|
|
196
211
|
:::
|
|
197
212
|
# Or let the picture fill the slide.
|
|
198
213
|
|
|
@@ -214,6 +229,7 @@ overlay: true
|
|
|
214
229
|
---
|
|
215
230
|
:::meta
|
|
216
231
|
layout: split
|
|
232
|
+
time: 1m30s
|
|
217
233
|
:::
|
|
218
234
|
# Code you can run
|
|
219
235
|
:::slot left
|
|
@@ -240,6 +256,7 @@ Live Python downloads its runtime the first time, so it needs an internet connec
|
|
|
240
256
|
---
|
|
241
257
|
:::meta
|
|
242
258
|
layout: split
|
|
259
|
+
time: 45s
|
|
243
260
|
:::
|
|
244
261
|
# Tables and formulas
|
|
245
262
|
:::slot left
|
|
@@ -272,6 +289,7 @@ Formulas use LaTeX. Single dollar signs put a formula inside a sentence; two dol
|
|
|
272
289
|
---
|
|
273
290
|
:::meta
|
|
274
291
|
layout: split
|
|
292
|
+
time: 45s
|
|
275
293
|
:::
|
|
276
294
|
# Tips and warnings
|
|
277
295
|
:::slot left
|
|
@@ -299,6 +317,32 @@ Check the room's projector.
|
|
|
299
317
|
The other callouts are `note`, `important` and `caution`. Add your own label after the type, for example `:::tip Before the talk`.
|
|
300
318
|
:::
|
|
301
319
|
|
|
320
|
+
---
|
|
321
|
+
:::meta
|
|
322
|
+
layout: split
|
|
323
|
+
time: 1m
|
|
324
|
+
:::
|
|
325
|
+
# Cite your sources
|
|
326
|
+
:::slot left
|
|
327
|
+
```markdown
|
|
328
|
+
---
|
|
329
|
+
bibliography: refs.bib
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
Recalling beats rereading
|
|
333
|
+
[@roediger2006].
|
|
334
|
+
```
|
|
335
|
+
:::
|
|
336
|
+
:::slot right
|
|
337
|
+
Recalling beats rereading [@roediger2006].
|
|
338
|
+
|
|
339
|
+
Lectures with active learning cut failure rates by a third [@freeman2014].
|
|
340
|
+
:::
|
|
341
|
+
|
|
342
|
+
:::notes
|
|
343
|
+
`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.
|
|
344
|
+
:::
|
|
345
|
+
|
|
302
346
|
---
|
|
303
347
|
:::meta
|
|
304
348
|
layout: chapter
|
|
@@ -306,12 +350,14 @@ number: 3
|
|
|
306
350
|
part: Presenting
|
|
307
351
|
description: Notes for you, drawing on the slides, and questions for the room.
|
|
308
352
|
id: give-the-talk
|
|
353
|
+
time: 15s
|
|
309
354
|
:::
|
|
310
355
|
# Give the talk.
|
|
311
356
|
|
|
312
357
|
---
|
|
313
358
|
:::meta
|
|
314
359
|
layout: split
|
|
360
|
+
time: 1m
|
|
315
361
|
:::
|
|
316
362
|
# Notes only you can see
|
|
317
363
|
:::slot left
|
|
@@ -337,12 +383,13 @@ This is what the presenter view shows for this slide. Its header also shows what
|
|
|
337
383
|
---
|
|
338
384
|
:::meta
|
|
339
385
|
id: draw-on-any-slide
|
|
386
|
+
time: 1m
|
|
340
387
|
:::
|
|
341
388
|
# Draw on any slide
|
|
342
389
|
|
|
343
390
|
Press **D** now, or the pen button. Draw with a pen, a highlighter, or a marker that fades after a moment. **Escape** stops.
|
|
344
391
|
|
|
345
|
-
Drawings are kept in
|
|
392
|
+
Drawings are kept in a `.drawings.json` file beside the slides, and appear in the PDF.
|
|
346
393
|
|
|
347
394
|
:::notes
|
|
348
395
|
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 +402,7 @@ On an iPad, pair it from the launch page: you draw and steer there while the lap
|
|
|
355
402
|
layout: split
|
|
356
403
|
hotspots: auto
|
|
357
404
|
id: zoom-in-on-a-part
|
|
405
|
+
time: 1m30s
|
|
358
406
|
:::
|
|
359
407
|
# Zoom in on a part
|
|
360
408
|
:::slot left
|
|
@@ -384,6 +432,34 @@ The numbered keys show on this slide only here in the presenter view, never on t
|
|
|
384
432
|
:::
|
|
385
433
|
|
|
386
434
|
---
|
|
435
|
+
:::meta
|
|
436
|
+
layout: split
|
|
437
|
+
time: 1m
|
|
438
|
+
:::
|
|
439
|
+
# Time your talk
|
|
440
|
+
:::slot left
|
|
441
|
+
```markdown
|
|
442
|
+
:::meta
|
|
443
|
+
time: 1m
|
|
444
|
+
:::
|
|
445
|
+
```
|
|
446
|
+
:::
|
|
447
|
+
:::slot right
|
|
448
|
+
Give each slide a **time**. The presenter view counts it down, and tells you whether you are ahead or behind.
|
|
449
|
+
|
|
450
|
+
Run `mdeck rehearse slides.md` to time the slides as you practise, then save the times into the file.
|
|
451
|
+
:::
|
|
452
|
+
|
|
453
|
+
:::notes
|
|
454
|
+
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.
|
|
455
|
+
|
|
456
|
+
A rehearsal measures the times for you. At the end it offers them rounded to 15 seconds; change any of them before you save.
|
|
457
|
+
:::
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
:::meta
|
|
461
|
+
time: 1m30s
|
|
462
|
+
:::
|
|
387
463
|
# Which part will you try first?
|
|
388
464
|
|
|
389
465
|
<poll room="tour" options="Writing|Layouts|Drawing|Polls" />
|
|
@@ -393,7 +469,34 @@ One line makes a poll; the heading is the question:
|
|
|
393
469
|
|
|
394
470
|
`<poll room="tour" options="Writing|Layouts|Drawing|Polls" />`
|
|
395
471
|
|
|
396
|
-
Phones join with the QR code. They can reach this computer when you start `mdeck run slides.md --network`.
|
|
472
|
+
Phones join with the QR code. They can reach this computer when you start `mdeck run slides.md --network`.
|
|
473
|
+
:::
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
:::meta
|
|
477
|
+
layout: split
|
|
478
|
+
time: 1m
|
|
479
|
+
:::
|
|
480
|
+
# One word from everyone
|
|
481
|
+
:::slot left
|
|
482
|
+
```markdown
|
|
483
|
+
<wordcloud room="mood"
|
|
484
|
+
qr="false" />
|
|
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" qr="false" />
|
|
494
|
+
:::
|
|
495
|
+
|
|
496
|
+
:::notes
|
|
497
|
+
Phones that joined with the poll's code stay in: `qr="false"` leaves the code out here. Answers appear as they come in.
|
|
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,6 +92,7 @@ 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
|
+
- `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
96
|
- Built-in layouts: `title`, `chapter`, `focus` (one big statement), `image-text`, `split`, `full-bleed-image`, and plain content slides without a `layout:`.
|
|
96
97
|
- Named areas use `:::slot name … :::`; the layout's regions and settings come from `mdeck list layouts --json`.
|
|
97
98
|
- `:::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.
|
|
@@ -127,6 +128,14 @@ The request is the message that invoked this skill: usually source material to t
|
|
|
127
128
|
- Keep lists to three to five items; prefer a short bold label plus one sentence over long bullets.
|
|
128
129
|
- Reference pictures under `./img/` and say which files the user still needs to add.
|
|
129
130
|
|
|
131
|
+
**Diagrams**
|
|
132
|
+
- 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.
|
|
133
|
+
- 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.
|
|
134
|
+
- 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.
|
|
135
|
+
- 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.
|
|
136
|
+
- On a slide of its own, a diagram goes in a layout's `image` with `fit: contain`, or alone in the slide text.
|
|
137
|
+
- `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`.
|
|
138
|
+
|
|
130
139
|
## 4. Check and hand over
|
|
131
140
|
|
|
132
141
|
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,18 @@ 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'
|
|
14
15
|
|
|
15
16
|
export function checkDeck(slidesPath, registry, source = readFileSync(slidesPath, 'utf8')) {
|
|
16
17
|
const deck = parseSlides(source)
|
|
17
18
|
const diagnostics = validateDeck(deck, { layouts: manifestsOf(registry, 'layout'), themes: manifestsOf(registry, 'theme'), palettes: manifestsOf(registry, 'palette') })
|
|
18
19
|
for (const ref of collectLocalAssetRefs(source)) {
|
|
19
|
-
|
|
20
|
+
const file = resolve(dirname(resolve(slidesPath)), ref)
|
|
21
|
+
if (!existsSync(file)) { diagnostics.push({ severity: 'error', code: 'missing-asset', message: `Missing local asset: ${ref}`, line: 1, column: 1 }); continue }
|
|
22
|
+
if (!/\.svg$/i.test(ref)) continue
|
|
23
|
+
const at = source.indexOf(ref)
|
|
24
|
+
const line = at < 0 ? 1 : source.slice(0, at).split('\n').length
|
|
25
|
+
for (const problem of svgProblems(readFileSync(file, 'utf8'))) diagnostics.push({ severity: 'warning', code: problem.code, message: `${ref} ${problem.message}`, line, column: 1 })
|
|
20
26
|
}
|
|
21
27
|
for (const folder of componentFolders(slidesPath, source).filter(folder => folder.source === 'shared')) {
|
|
22
28
|
if (!isFolder(folder.dir)) diagnostics.push({ severity: 'error', code: 'missing-components', message: `Components folder not found: ${folder.path} (${folder.dir})`, line: 1, column: 1 })
|
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
|
|
|
@@ -5,7 +5,7 @@ import { validateDeck, formatDiagnostics } from '../core/validateDeck.js'
|
|
|
5
5
|
import { loadRegistry, extensionRoots, manifestsOf } from '../extensions/discover.js'
|
|
6
6
|
import { resolvePalette } from '../extensions/appearance.js'
|
|
7
7
|
import { embedFonts } from './fonts.js'
|
|
8
|
-
import { stripNotes as stripNotesFrom,
|
|
8
|
+
import { stripNotes as stripNotesFrom, shareSlides } from '../core/editDeck.js'
|
|
9
9
|
import { componentFolders, componentFiles } from './components.js'
|
|
10
10
|
import { inkFileFor, normalizeInk, emptyInk } from '../core/ink.js'
|
|
11
11
|
import { resultsFileFor, normalizeResults } from '../core/results.js'
|
|
@@ -230,13 +230,13 @@ export function deckLanguages(abs) {
|
|
|
230
230
|
// the deck's text where there is no file (`mdeck design`, on the sample deck);
|
|
231
231
|
// `slidesPath` then only says where its folder is. `languages` also brings
|
|
232
232
|
// the deck's other languages along, for `mdeck run`; a built file has one.
|
|
233
|
-
export function slidesPlugin(slidesPath, { languages = false, inlineImages = false, inlineMedia = false, editor = false, stripNotes = false, stripActivities = false, ink = true, results = true, embedFonts: embedThemeFonts = false, deckLook = false, source: given = null } = {}) {
|
|
233
|
+
export function slidesPlugin(slidesPath, { languages = false, inlineImages = false, inlineMedia = false, editor = false, stripNotes = false, stripActivities = false, slides = null, ink = true, results = true, embedFonts: embedThemeFonts = false, deckLook = false, source: given = null } = {}) {
|
|
234
234
|
const abs = resolve(slidesPath)
|
|
235
235
|
const readDeck = () => given ?? readFileSync(abs, 'utf-8')
|
|
236
236
|
const watchDeck = context => { if (given === null) context.addWatchFile(abs) }
|
|
237
237
|
const sourceFor = raw => {
|
|
238
238
|
const shared = stripNotes ? stripNotesFrom(raw) : raw
|
|
239
|
-
return stripActivities ?
|
|
239
|
+
return stripActivities || slides ? shareSlides(shared, { activities: stripActivities, slides }).source : shared
|
|
240
240
|
}
|
|
241
241
|
const inkPath = inkFileFor(abs)
|
|
242
242
|
// The language the file name gives, for a deck that sets no `lang`.
|
|
@@ -346,10 +346,10 @@ export function slidesPlugin(slidesPath, { languages = false, inlineImages = fal
|
|
|
346
346
|
if (ink && existsSync(inkPath)) {
|
|
347
347
|
try { data = normalizeInk(JSON.parse(readFileSync(inkPath, 'utf-8')), size) } catch (error) { this.warn(`${inkPath}: ${error.message}; showing no ink`) }
|
|
348
348
|
}
|
|
349
|
-
// Without the activity slides, drawings follow their slides to their new ids.
|
|
350
|
-
if (stripActivities) {
|
|
349
|
+
// Without the activity slides or those not chosen, drawings follow their slides to their new ids.
|
|
350
|
+
if (stripActivities || slides) {
|
|
351
351
|
try {
|
|
352
|
-
const { ids, removed } =
|
|
352
|
+
const { ids, removed } = shareSlides(readDeck(), { activities: stripActivities, slides })
|
|
353
353
|
const gone = new Set(removed)
|
|
354
354
|
data = { ...data, slides: Object.fromEntries(Object.entries(data.slides).filter(([id]) => !gone.has(id)).map(([id, strokes]) => [ids[id] ?? id, strokes])) }
|
|
355
355
|
} catch {}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// What `mdeck check` says about the deck's SVG drawings. A drawing that uses
|
|
2
|
+
// the palette (var(--…)) is drawn into the page, where it should follow the
|
|
3
|
+
// palette everywhere, work without it, and leave the rest of the page alone.
|
|
4
|
+
// One that does not is shown as a picture and cannot build up or be zoomed to.
|
|
5
|
+
|
|
6
|
+
const COLOUR_ATTRIBUTE = /\s(fill|stroke|stop-color|flood-color|lighting-color|color)\s*=\s*(["'])([^"']*)\2/gi
|
|
7
|
+
const COLOUR_PROPERTY = /(?<![\w-])(fill|stroke|stop-color|flood-color|lighting-color|color)\s*:\s*([^;}"']+)/gi
|
|
8
|
+
const FOLLOWS = /^(none|transparent|currentcolor|inherit|initial|unset|context-fill|context-stroke)$|^(var|url)\(/i
|
|
9
|
+
|
|
10
|
+
// [{ code, message }] for one drawing's markup.
|
|
11
|
+
export function svgProblems(markup) {
|
|
12
|
+
markup = markup.replace(/<!--[\s\S]*?-->/g, '')
|
|
13
|
+
const problems = []
|
|
14
|
+
const themed = /var\(--/.test(markup)
|
|
15
|
+
const steps = [...markup.matchAll(/\sdata-step(?:\s*=\s*(["'])([^"']*)\1)?/g)].map(match => (match[2] ?? '').trim())
|
|
16
|
+
if (!themed) {
|
|
17
|
+
if (steps.length || /\sdata-hotspot\b/.test(markup)) problems.push({ code: 'svg-not-inline', message: 'has data-step or data-hotspot, but uses no palette colour (var(--…)), so it is shown as a picture and they do nothing; give one colour as var(--ink, #222)' })
|
|
18
|
+
return problems
|
|
19
|
+
}
|
|
20
|
+
const bare = [...markup.matchAll(/var\(\s*(--[\w-]+)\s*\)/g)].map(match => match[1])
|
|
21
|
+
if (bare.length) problems.push({ code: 'svg-fallback', message: `uses ${unique(bare).map(name => `var(${name})`).join(', ')} without a fallback, which shows nothing where the drawing is opened on its own; write var(${bare[0]}, #…)` })
|
|
22
|
+
const fixed = [
|
|
23
|
+
...[...markup.matchAll(COLOUR_ATTRIBUTE)].map(match => [match[1], match[3].trim()]),
|
|
24
|
+
...[...styleText(markup).matchAll(COLOUR_PROPERTY), ...inlineStyles(markup).flatMap(style => [...style.matchAll(COLOUR_PROPERTY)])].map(match => [match[1], match[2].trim()]),
|
|
25
|
+
].filter(([, value]) => value && !FOLLOWS.test(value))
|
|
26
|
+
if (fixed.length) {
|
|
27
|
+
const examples = unique(fixed.map(([name, value]) => `${name}="${value}"`)).slice(0, 3).join(', ')
|
|
28
|
+
problems.push({ code: 'svg-fixed-colour', message: `has ${fixed.length === 1 ? 'a fixed colour' : `${fixed.length} fixed colours`} (${examples}) that stay${fixed.length === 1 ? 's' : ''} the same in every palette and in dark mode; use var(--ink, …) or another palette colour where it should follow` })
|
|
29
|
+
}
|
|
30
|
+
const unscoped = unique(selectors(styleText(markup)).filter(selector => !selector.startsWith('.')))
|
|
31
|
+
if (unscoped.length) problems.push({ code: 'svg-style', message: `has style rules that reach the whole page (${unscoped.slice(0, 3).join(', ')}), its icons, QR codes and other drawings included; start each with a class of this drawing, such as .flow-` })
|
|
32
|
+
if (steps.some(Boolean) && steps.some(step => !step)) problems.push({ code: 'svg-steps', message: 'has numbered and bare data-step parts: the numbered ones show together in their order, the bare ones in document order. Number all of them' })
|
|
33
|
+
return problems
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const unique = items => [...new Set(items)]
|
|
37
|
+
|
|
38
|
+
const styleText = markup => [...markup.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map(match => match[1].replace(/<!\[CDATA\[|\]\]>/g, '')).join('\n')
|
|
39
|
+
const inlineStyles = markup => [...markup.matchAll(/\sstyle\s*=\s*(["'])([^"']*)\1/gi)].map(match => match[2])
|
|
40
|
+
|
|
41
|
+
// The selectors of a stylesheet's rules, also inside @media; not the steps
|
|
42
|
+
// of @keyframes.
|
|
43
|
+
function selectors(css) {
|
|
44
|
+
css = css.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
45
|
+
const found = []
|
|
46
|
+
let depth = 0
|
|
47
|
+
let skipUntil = -1
|
|
48
|
+
let start = 0
|
|
49
|
+
for (let i = 0; i < css.length; i++) {
|
|
50
|
+
if (css[i] === '{') {
|
|
51
|
+
const prelude = css.slice(start, i).trim()
|
|
52
|
+
if (skipUntil < 0) {
|
|
53
|
+
if (/^@keyframes\b|^@font-face\b/i.test(prelude)) skipUntil = depth
|
|
54
|
+
else if (prelude && !prelude.startsWith('@')) found.push(...prelude.split(',').map(s => s.trim()).filter(Boolean))
|
|
55
|
+
}
|
|
56
|
+
depth++
|
|
57
|
+
start = i + 1
|
|
58
|
+
} else if (css[i] === '}') {
|
|
59
|
+
depth--
|
|
60
|
+
if (depth === skipUntil) skipUntil = -1
|
|
61
|
+
start = i + 1
|
|
62
|
+
} else if (css[i] === ';' && depth === 0) start = i + 1
|
|
63
|
+
}
|
|
64
|
+
return found
|
|
65
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Saving a rehearsal's times while `mdeck run` runs: the presenter view
|
|
2
|
+
// sends each slide's new time, and it goes into the slide's settings as
|
|
3
|
+
// `time: 1m30s` (core/timing.js). null takes a slide's time out.
|
|
4
|
+
//
|
|
5
|
+
// GET /__mdeck/timing → { ids }
|
|
6
|
+
// POST /__mdeck/timing { ids, times: { [slideId]: '1m30s' | null } } → { ok, changed }
|
|
7
|
+
//
|
|
8
|
+
// Slides without an `id:` are named by position (slide-3), so the times are
|
|
9
|
+
// only written when the deck still has the slides the page saw (`ids`);
|
|
10
|
+
// otherwise the answer is 409. The deck is copied to .mdeck-backups/ before
|
|
11
|
+
// the first write, and the write does not reload the open windows
|
|
12
|
+
// (ownWrites.js).
|
|
13
|
+
import { readFileSync, writeFileSync, renameSync } from 'node:fs'
|
|
14
|
+
import { resolve } from 'node:path'
|
|
15
|
+
import { parseSlides } from '../core/parseSlides.js'
|
|
16
|
+
import { setSlideMeta } from '../core/editDeck.js'
|
|
17
|
+
import { parseTime, formatTime } from '../core/timing.js'
|
|
18
|
+
import { isAllowedRequest, send, readJson, backupDeck } from './editorPlugin.js'
|
|
19
|
+
import { markOwnWrite } from './ownWrites.js'
|
|
20
|
+
|
|
21
|
+
export function createTimingFile(deckPath) {
|
|
22
|
+
const abs = resolve(deckPath)
|
|
23
|
+
let backedUp = false
|
|
24
|
+
const read = () => { const source = readFileSync(abs, 'utf8'); return { source, deck: parseSlides(source) } }
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
path: abs,
|
|
28
|
+
ids: () => read().deck.slides.map(slide => slide.id),
|
|
29
|
+
save(ids, times) {
|
|
30
|
+
let { source, deck } = read()
|
|
31
|
+
const current = deck.slides.map(slide => slide.id)
|
|
32
|
+
if (!Array.isArray(ids) || ids.join('\n') !== current.join('\n')) return { status: 409, error: 'The deck changed since the rehearsal began; rehearse again', ids: current }
|
|
33
|
+
const before = source
|
|
34
|
+
let changed = 0
|
|
35
|
+
for (const [id, value] of Object.entries(times ?? {})) {
|
|
36
|
+
if (!current.includes(id)) return { status: 400, error: `No slide "${id}" in the deck` }
|
|
37
|
+
const seconds = value == null ? null : parseTime(value)
|
|
38
|
+
if (value != null && seconds == null) return { status: 400, error: `"${value}" is not a time` }
|
|
39
|
+
const time = seconds == null ? undefined : formatTime(seconds)
|
|
40
|
+
if (deck.slides.find(slide => slide.id === id).meta.time === time) continue
|
|
41
|
+
source = setSlideMeta(deck, id, { time })
|
|
42
|
+
deck = parseSlides(source)
|
|
43
|
+
changed++
|
|
44
|
+
}
|
|
45
|
+
if (source === before) return { status: 200, ok: true, changed: 0 }
|
|
46
|
+
if (!backedUp) { backupDeck(abs, before); backedUp = true }
|
|
47
|
+
markOwnWrite(abs, source)
|
|
48
|
+
const temp = `${abs}.${process.pid}.tmp`
|
|
49
|
+
writeFileSync(temp, source, 'utf8')
|
|
50
|
+
renameSync(temp, abs)
|
|
51
|
+
return { status: 200, ok: true, changed }
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function timingMiddleware(file, { authorize = isAllowedRequest } = {}) {
|
|
57
|
+
return async (request, response, next) => {
|
|
58
|
+
const { pathname } = new URL(request.url, 'http://localhost')
|
|
59
|
+
if (pathname !== '/') return next()
|
|
60
|
+
if (!authorize(request)) return send(response, 403, { error: 'Only the presenter can save times' })
|
|
61
|
+
try {
|
|
62
|
+
if (request.method === 'GET') return send(response, 200, { ids: file.ids() })
|
|
63
|
+
if (request.method !== 'POST') { response.setHeader('Allow', 'GET, POST'); return send(response, 405, { error: 'Method not allowed' }) }
|
|
64
|
+
const { ids, times } = await readJson(request)
|
|
65
|
+
const { status, ...result } = file.save(ids, times)
|
|
66
|
+
return send(response, status, result)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return send(response, error.status ?? 500, { error: error.message })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function timingPlugin(slidesPath, { authorize } = {}) {
|
|
74
|
+
return {
|
|
75
|
+
name: 'vite-plugin-mdeck-timing',
|
|
76
|
+
configureServer(server) {
|
|
77
|
+
server.middlewares.use('/__mdeck/timing', timingMiddleware(createTimingFile(slidesPath), { authorize }))
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/cli/args.js
CHANGED
|
@@ -5,13 +5,14 @@ const output = { '--output': value, '-o': '--output', '--no-drawings': flag, '--
|
|
|
5
5
|
const commands = {
|
|
6
6
|
new: { max: 0 },
|
|
7
7
|
run: { ...common, '--network': flag, '--server': optional, max: 1 },
|
|
8
|
+
rehearse: { ...common, '--network': flag, '--server': optional, max: 1 },
|
|
8
9
|
edit: { ...common, max: 1 },
|
|
9
10
|
design: { ...common, max: 1 },
|
|
10
11
|
themes: { '--local': flag, '--force': flag, '--check': flag, '--output': value, '-o': '--output', max: 4 },
|
|
11
12
|
palettes: { '--local': flag, '--force': flag, max: 4 },
|
|
12
|
-
build: { ...output, '--single-file': flag, '--launchers': flag, '--reader': flag, '--notes': flag, '--no-polls': flag, max: 1 },
|
|
13
|
-
send: { ...output, '--notes': flag, '--no-polls': flag, '--no-pdf': flag, max: 1 },
|
|
14
|
-
pdf: { ...output, '--no-polls': flag, max: 1 },
|
|
13
|
+
build: { ...output, '--single-file': flag, '--launchers': flag, '--reader': flag, '--notes': flag, '--no-polls': flag, '--slide': value, max: 1 },
|
|
14
|
+
send: { ...output, '--notes': flag, '--no-polls': flag, '--no-pdf': flag, '--slide': value, max: 1 },
|
|
15
|
+
pdf: { ...output, '--no-polls': flag, '--slide': value, max: 1 },
|
|
15
16
|
preview: { ...common, max: 1 },
|
|
16
17
|
check: { '--strict': flag, '--render': flag, max: 1 },
|
|
17
18
|
snapshot: { '--output': value, '-o': '--output', '--slide': value, '--dark': flag, '--light': flag, '--sheet': flag, max: 1 },
|