mdeck 3.3.0 → 3.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/CHANGELOG.md +17 -0
- package/assets/base.css +23 -2
- package/assets/extensions/themes/academic/styles.css +18 -5
- package/assets/extensions/themes/aurora/styles.css +32 -6
- package/assets/extensions/themes/minimal/styles.css +16 -4
- package/assets/extensions/themes/neue/styles.css +17 -4
- package/docs/reference/authoring.md +27 -4
- package/docs/site/content/audience.md +35 -4
- package/docs/site/content/commands.md +2 -0
- package/package.json +1 -1
- package/skills/write-slides/SKILL.md +1 -1
- package/src/build/bibliography.js +29 -11
- package/src/build/resultsPlugin.js +77 -0
- package/src/build/slidesPlugin.js +28 -5
- package/src/build/themeCheck.js +2 -1
- package/src/cli/args.js +2 -1
- package/src/cli/index.js +17 -4
- package/src/components/Numeric.jsx +59 -0
- package/src/components/Poll.jsx +58 -55
- package/src/components/Question.jsx +10 -6
- package/src/components/Scale.jsx +13 -8
- package/src/components/WordCloud.jsx +10 -6
- package/src/components/activity.jsx +88 -7
- package/src/components/inlineText.js +40 -0
- package/src/components/numbers.js +39 -0
- package/src/components/poll.css +23 -13
- package/src/core/editDeck.js +2 -2
- package/src/core/icons.js +2 -0
- package/src/core/labels.js +32 -0
- package/src/core/results.js +64 -0
- package/src/editor/sampleDeck.js +34 -8
- package/src/layouts/layoutApi.jsx +4 -3
- package/src/live/answer/answer.css +19 -0
- package/src/live/answer/answer.js +147 -24
- package/src/live/client.js +51 -6
- package/src/live/roomTag.js +6 -2
- package/src/live/rooms.js +6 -1
- package/src/runtime/citations.js +15 -5
- package/src/runtime/ink/bus.js +7 -13
- package/src/runtime/main.jsx +3 -1
- package/src/runtime/registry.jsx +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.0 — 2026-10-08
|
|
4
|
+
|
|
5
|
+
- What follows a list has room above it in every theme, so a sentence after a list no longer sits right against its last item.
|
|
6
|
+
- Fixed: a bullet list inside a numbered list took numbers too, and in some themes the number sat on top of the bullet; it also pushed the numbering of the items after it. Every built-in theme now styles only a list's own items.
|
|
7
|
+
- A list inside a list item has room above it, as much as between its own items and less than between the outer ones, instead of sitting right against its item. Themes with their own spacing for nested lists keep it.
|
|
8
|
+
- Links and quotations look the part in every theme: links take the palette's accent colour instead of the browser's blue, and a `> quotation` is set in each theme's own way instead of as a bare indent: academic as an indented serif quotation, aurora with a quotation mark in its gradient, minimal and neue with a bar at its side (grey and red). A quotation's paragraphs now take the quotation's type, which themes that set every paragraph's font used to undo.
|
|
9
|
+
- The sample deck, on which `mdeck design`, the theme gallery and the theme checks show every theme, now also has a link, a quotation, a bullet list inside a numbered list, citations with their references at the foot of the slide, and a reference list.
|
|
10
|
+
- A poll's question and options may use Markdown and `$…$` maths, as on a slide: `options="$2x$|$x^2$|$\frac{x^3}{3}$"`. The slide draws them with KaTeX, the phones' answer page as MathML, which needs no fonts or scripts there; it keeps only text, emphasis and MathML from what the presenter's screen sends. A `|` inside a formula belongs to it (`$|x|$`), and `\|` is a bar in the text. Votes and `answer` still use the text as written. The same goes for the `question` of every activity (`<scale>`, `<question>`, `<wordcloud>`) and the scale's `low` and `high` labels. An answer page from an older `mdeck server` shows the source text.
|
|
11
|
+
- Fixed: the phones' answer page showed the word “null” below the question when there was no link for following the slides.
|
|
12
|
+
- Every activity has a lock button that closes it: answers after that do not count, and the phones say it is closed until it opens again. Together with the eye (`results="hidden"`) and the tick this makes a peer-instruction round: vote, close, show the bars, discuss, show the answer.
|
|
13
|
+
- Showing a poll's right answer also tells each phone whether its vote was right, or what the right answer is. The lock, the eye and the tick are kept in the activity room's state on the server, so every screen of the talk and the phones follow them, also after a reload.
|
|
14
|
+
- Polls, scales and numbers count “12 of 31 answered” while phones are connected.
|
|
15
|
+
- `multiple` lets each phone pick several options of a poll; the slide and the phones say so. `buttons="letters"` (or `"numbers"`) puts A, B, C before the options on the slide and shows only those on the phones, for options that are pictures or long formulas.
|
|
16
|
+
- New activity `<numeric>`: phones type a number (`0.5`, `0,5`, `1/2`), the slide shows the most frequent answers as bars. `answer` and `tolerance` mark the right ones and tell each phone whether it was right.
|
|
17
|
+
- The answers of a talk are kept beside the deck in `<slides>.results.json` while `mdeck run` runs: anonymous, phones numbered per question. Builds, `mdeck send` and `mdeck pdf` show them as the record of the talk (`--no-results` leaves them out), and `mdeck results` prints them as CSV.
|
|
18
|
+
- Fixed: a `>` or `<` in an activity's attributes (`question="Is $x > 0$?"`) cut its tag short, and the phones lost the options. Pictures in a tag's attributes, such as a poll's options, are now copied into folder builds.
|
|
19
|
+
|
|
3
20
|
## 3.3.0 — 2026-10-08
|
|
4
21
|
|
|
5
22
|
- Citations and references: `bibliography: refs.bib` in the deck settings, then cite in Pandoc's syntax, `[@smith2020, p. 12]`, `[see @a; @b]`, `[-@a]` or `@smith2020` in a sentence. Each citation shows in the style's short form, each slide lists the full references of the works it cites above its footer (`show.citations: false` leaves them out), and `<bibliography />` lists them all on a slide (`part="1/2"` splits a long list). Reference files may be BibTeX (`.bib`), CSL-JSON (`.json`) or Hayagriva (`.yml`, Typst's format), several at once. `csl:` picks the style, `apa` by default, `vancouver`, `harvard1` or any `.csl` file; the style's words follow `lang`. `nocite:` lists works without citing them. Formatting happens at build time, so a deck carries only the finished text. Unknown keys are marked on the slide and reported by `mdeck check`.
|
package/assets/base.css
CHANGED
|
@@ -105,7 +105,10 @@ deck-stage[data-ink-hidden] .slide-ink { display: none; }
|
|
|
105
105
|
.split-right { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: safe center; gap: var(--gap-item); }
|
|
106
106
|
|
|
107
107
|
/* ── List containers ─────────────────────────────────────────────────────── */
|
|
108
|
+
/* Lists have no margins of their own, and paragraphs only one below them, so
|
|
109
|
+
whatever follows a list needs room above it. */
|
|
108
110
|
.slide-body :is(ul, ol) + :is(ul, ol) { margin-top: var(--gap-item); }
|
|
111
|
+
.slide-body :is(ul, ol) + :not(ul, ol) { margin-top: var(--gap-item); }
|
|
109
112
|
/* Markers (::before), padding, and font styling are visual — defined per theme */
|
|
110
113
|
.slide-body ul {
|
|
111
114
|
list-style: none;
|
|
@@ -115,7 +118,7 @@ deck-stage[data-ink-hidden] .slide-ink { display: none; }
|
|
|
115
118
|
flex-direction: column;
|
|
116
119
|
gap: var(--gap-item);
|
|
117
120
|
}
|
|
118
|
-
.slide-body ul li { position: relative; }
|
|
121
|
+
.slide-body ul > li { position: relative; }
|
|
119
122
|
.slide-body ol {
|
|
120
123
|
list-style: none;
|
|
121
124
|
padding: 0;
|
|
@@ -125,7 +128,10 @@ deck-stage[data-ink-hidden] .slide-ink { display: none; }
|
|
|
125
128
|
flex-direction: column;
|
|
126
129
|
gap: var(--gap-item);
|
|
127
130
|
}
|
|
128
|
-
.slide-body ol li { counter-increment: slide-ol; position: relative; }
|
|
131
|
+
.slide-body ol > li { counter-increment: slide-ol; position: relative; }
|
|
132
|
+
/* A list inside an item: closer to its item than items are to each other,
|
|
133
|
+
and as close inside. Themes with their own (same selector) win. */
|
|
134
|
+
.slide-body li :is(ul, ol) { margin-top: calc(var(--gap-item) * 0.5); gap: calc(var(--gap-item) * 0.5); }
|
|
129
135
|
|
|
130
136
|
/* ── Full-bleed image ────────────────────────────────────────────────────── */
|
|
131
137
|
.slide--full-bleed-image { padding: 0; }
|
|
@@ -159,6 +165,21 @@ deck-stage[data-ink-hidden] .slide-ink { display: none; }
|
|
|
159
165
|
.slide-footnotes p { margin: 0; }
|
|
160
166
|
.slide-footnotes .footnote-backref { display: none; }
|
|
161
167
|
|
|
168
|
+
/* ── Links and quotations ───────────────────────────────────────────────── */
|
|
169
|
+
/* Defaults in the palette's colours instead of the browser's blue and a bare
|
|
170
|
+
indent. :where keeps them weaker than any rule of the theme's own. */
|
|
171
|
+
:where(.slide) a { color: var(--accent); text-underline-offset: 0.15em; text-decoration-thickness: 0.06em; }
|
|
172
|
+
:where(.slide) blockquote {
|
|
173
|
+
margin: 0 0 var(--gap-item);
|
|
174
|
+
padding-left: 0.9em;
|
|
175
|
+
border-left: 4px solid var(--accent);
|
|
176
|
+
color: var(--ink-soft);
|
|
177
|
+
}
|
|
178
|
+
:where(.slide) blockquote > p:last-child { margin-bottom: 0; }
|
|
179
|
+
/* The quote's paragraphs take its type: themes set the face and size of
|
|
180
|
+
every paragraph, which would undo a quote set in its own. */
|
|
181
|
+
.slide-body blockquote > p { font: inherit; color: inherit; letter-spacing: inherit; }
|
|
182
|
+
|
|
162
183
|
/* ── Code blocks ─────────────────────────────────────────────────────────── */
|
|
163
184
|
.code-block { white-space: pre-wrap; overflow-x: hidden; }
|
|
164
185
|
|
|
@@ -180,12 +180,12 @@ strong { color: var(--ink); font-weight: 700; }
|
|
|
180
180
|
the text size, so long lists stay compact
|
|
181
181
|
───────────────────────────────────────────── */
|
|
182
182
|
.slide-body ul, .slide-body ol { max-width: 1560px; }
|
|
183
|
-
.slide-body ul li,
|
|
184
|
-
.slide-body ol li {
|
|
183
|
+
.slide-body ul > li,
|
|
184
|
+
.slide-body ol > li {
|
|
185
185
|
line-height: 1.45;
|
|
186
186
|
padding-left: 44px;
|
|
187
187
|
}
|
|
188
|
-
.slide-body ul li::before {
|
|
188
|
+
.slide-body ul > li::before {
|
|
189
189
|
content: '';
|
|
190
190
|
position: absolute;
|
|
191
191
|
left: 4px;
|
|
@@ -195,7 +195,7 @@ strong { color: var(--ink); font-weight: 700; }
|
|
|
195
195
|
background: var(--accent);
|
|
196
196
|
clip-path: polygon(0 0, 100% 50%, 0 100%);
|
|
197
197
|
}
|
|
198
|
-
.slide-body ol li::before {
|
|
198
|
+
.slide-body ol > li::before {
|
|
199
199
|
content: counter(slide-ol) '.';
|
|
200
200
|
position: absolute;
|
|
201
201
|
left: 0;
|
|
@@ -206,7 +206,7 @@ strong { color: var(--ink); font-weight: 700; }
|
|
|
206
206
|
}
|
|
207
207
|
/* Second level: an en dash, a little smaller */
|
|
208
208
|
.slide-body li :is(ul, ol) { margin-top: 8px; gap: 6px; font-size: 0.88em; }
|
|
209
|
-
.slide-body li ul li::before {
|
|
209
|
+
.slide-body li ul > li::before {
|
|
210
210
|
width: 0.5em;
|
|
211
211
|
height: 2px;
|
|
212
212
|
top: 0.72em;
|
|
@@ -644,3 +644,16 @@ strong { color: var(--ink); font-weight: 700; }
|
|
|
644
644
|
overflow-y: hidden;
|
|
645
645
|
}
|
|
646
646
|
.slide .katex-display > .katex { font-size: 1.2em; }
|
|
647
|
+
|
|
648
|
+
/* ─────────────────────────────────────────────
|
|
649
|
+
QUOTATIONS — the quote environment of a printed text: set in from
|
|
650
|
+
both sides, in the serif, without ornament
|
|
651
|
+
───────────────────────────────────────────── */
|
|
652
|
+
.slide-body blockquote {
|
|
653
|
+
margin: 0 0 var(--gap-item);
|
|
654
|
+
padding: 0 64px;
|
|
655
|
+
border: 0;
|
|
656
|
+
font-family: var(--font-serif);
|
|
657
|
+
font-style: italic;
|
|
658
|
+
color: var(--ink-soft);
|
|
659
|
+
}
|
|
@@ -66,13 +66,13 @@ deck-stage[data-deck-static] .slide::before { animation: none; }
|
|
|
66
66
|
/* Print and PDF: gradient text prints with a hairline box around it, and
|
|
67
67
|
glows print as dark halos; solid colours instead. */
|
|
68
68
|
@media print {
|
|
69
|
-
em, .slide-footer > span:last-child:not(:first-child), .chapter-num, .slide-body ol li::before {
|
|
69
|
+
em, .slide-footer > span:last-child:not(:first-child), .chapter-num, .slide-body ol > li::before {
|
|
70
70
|
background: none !important;
|
|
71
71
|
-webkit-text-fill-color: currentColor !important;
|
|
72
72
|
color: var(--aurora-a) !important;
|
|
73
73
|
filter: none !important;
|
|
74
74
|
}
|
|
75
|
-
.slide-body ul li::before, .focus-eyebrow::before, .slide-header > span:last-child:not(:empty)::after { box-shadow: none !important; }
|
|
75
|
+
.slide-body ul > li::before, .focus-eyebrow::before, .slide-header > span:last-child:not(:empty)::after { box-shadow: none !important; }
|
|
76
76
|
/* Frosted glass blurs what lies behind it, which prints as an image. */
|
|
77
77
|
.slide *, .slide *::before { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
|
|
78
78
|
}
|
|
@@ -457,13 +457,13 @@ strong { color: var(--ink); font-weight: 700; }
|
|
|
457
457
|
/* ─────────────────────────────────────────────
|
|
458
458
|
LISTS — glowing dots, gradient numbers
|
|
459
459
|
───────────────────────────────────────────── */
|
|
460
|
-
.slide-body ul li {
|
|
460
|
+
.slide-body ul > li {
|
|
461
461
|
font-weight: 400;
|
|
462
462
|
line-height: 1.35;
|
|
463
463
|
padding-left: 46px;
|
|
464
464
|
position: relative;
|
|
465
465
|
}
|
|
466
|
-
.slide-body ul li::before {
|
|
466
|
+
.slide-body ul > li::before {
|
|
467
467
|
content: '';
|
|
468
468
|
position: absolute;
|
|
469
469
|
left: 0;
|
|
@@ -474,11 +474,11 @@ strong { color: var(--ink); font-weight: 700; }
|
|
|
474
474
|
background: var(--aurora-gradient);
|
|
475
475
|
box-shadow: 0 0 14px color-mix(in oklab, var(--aurora-a) 55%, transparent);
|
|
476
476
|
}
|
|
477
|
-
.slide-body ol li {
|
|
477
|
+
.slide-body ol > li {
|
|
478
478
|
padding-left: 60px;
|
|
479
479
|
position: relative;
|
|
480
480
|
}
|
|
481
|
-
.slide-body ol li::before {
|
|
481
|
+
.slide-body ol > li::before {
|
|
482
482
|
content: counter(slide-ol);
|
|
483
483
|
position: absolute;
|
|
484
484
|
left: 0;
|
|
@@ -642,3 +642,29 @@ strong { color: var(--ink); font-weight: 700; }
|
|
|
642
642
|
overflow-x: auto;
|
|
643
643
|
overflow-y: hidden;
|
|
644
644
|
}
|
|
645
|
+
|
|
646
|
+
/* ─────────────────────────────────────────────
|
|
647
|
+
QUOTATIONS — no pane, only a large quotation mark in the colours of
|
|
648
|
+
the lights beside the quote
|
|
649
|
+
───────────────────────────────────────────── */
|
|
650
|
+
.slide-body blockquote {
|
|
651
|
+
position: relative;
|
|
652
|
+
margin: 0 0 var(--gap-item);
|
|
653
|
+
padding: 0 0 0 76px;
|
|
654
|
+
border: 0;
|
|
655
|
+
max-width: 1300px;
|
|
656
|
+
color: var(--ink);
|
|
657
|
+
}
|
|
658
|
+
.slide-body blockquote::before {
|
|
659
|
+
content: '\201C';
|
|
660
|
+
position: absolute;
|
|
661
|
+
left: 0;
|
|
662
|
+
top: -18px;
|
|
663
|
+
font-size: 120px;
|
|
664
|
+
font-weight: 800;
|
|
665
|
+
line-height: 1;
|
|
666
|
+
background: var(--aurora-gradient);
|
|
667
|
+
-webkit-background-clip: text;
|
|
668
|
+
background-clip: text;
|
|
669
|
+
color: transparent;
|
|
670
|
+
}
|
|
@@ -215,8 +215,8 @@ strong { color: var(--ink); font-weight: 600; }
|
|
|
215
215
|
/* ─────────────────────────────────────────────
|
|
216
216
|
LISTS — a short dash, plain numbers
|
|
217
217
|
───────────────────────────────────────────── */
|
|
218
|
-
.slide-body ul li { line-height: 1.4; padding-left: 40px; position: relative; }
|
|
219
|
-
.slide-body ul li::before {
|
|
218
|
+
.slide-body ul > li { line-height: 1.4; padding-left: 40px; position: relative; }
|
|
219
|
+
.slide-body ul > li::before {
|
|
220
220
|
content: '';
|
|
221
221
|
position: absolute;
|
|
222
222
|
left: 0;
|
|
@@ -225,8 +225,8 @@ strong { color: var(--ink); font-weight: 600; }
|
|
|
225
225
|
height: 2px;
|
|
226
226
|
background: var(--muted);
|
|
227
227
|
}
|
|
228
|
-
.slide-body ol li { padding-left: 52px; position: relative; }
|
|
229
|
-
.slide-body ol li::before {
|
|
228
|
+
.slide-body ol > li { padding-left: 52px; position: relative; }
|
|
229
|
+
.slide-body ol > li::before {
|
|
230
230
|
content: counter(slide-ol) '.';
|
|
231
231
|
position: absolute;
|
|
232
232
|
left: 0;
|
|
@@ -340,3 +340,15 @@ strong { color: var(--ink); font-weight: 600; }
|
|
|
340
340
|
overflow-x: auto;
|
|
341
341
|
overflow-y: hidden;
|
|
342
342
|
}
|
|
343
|
+
|
|
344
|
+
/* ─────────────────────────────────────────────
|
|
345
|
+
QUOTATIONS — a thin bar in the quiet grey, the quote set in a little,
|
|
346
|
+
in the softer ink
|
|
347
|
+
───────────────────────────────────────────── */
|
|
348
|
+
.slide-body blockquote {
|
|
349
|
+
margin: 0 0 var(--gap-item);
|
|
350
|
+
padding: 2px 0 2px 28px;
|
|
351
|
+
border: 0;
|
|
352
|
+
border-left: 3px solid var(--muted);
|
|
353
|
+
color: var(--ink-soft);
|
|
354
|
+
}
|
|
@@ -336,13 +336,13 @@ strong { color: var(--ink); font-weight: 600; }
|
|
|
336
336
|
/* ─────────────────────────────────────────────
|
|
337
337
|
LISTS — square markers in the accent, numbers in the display face
|
|
338
338
|
───────────────────────────────────────────── */
|
|
339
|
-
.slide-body ul li {
|
|
339
|
+
.slide-body ul > li {
|
|
340
340
|
font-weight: 400;
|
|
341
341
|
line-height: 1.32;
|
|
342
342
|
padding-left: 44px;
|
|
343
343
|
position: relative;
|
|
344
344
|
}
|
|
345
|
-
.slide-body ul li::before {
|
|
345
|
+
.slide-body ul > li::before {
|
|
346
346
|
content: '';
|
|
347
347
|
position: absolute;
|
|
348
348
|
left: 0;
|
|
@@ -351,12 +351,12 @@ strong { color: var(--ink); font-weight: 600; }
|
|
|
351
351
|
height: 14px;
|
|
352
352
|
background: var(--accent);
|
|
353
353
|
}
|
|
354
|
-
.slide-body ol li {
|
|
354
|
+
.slide-body ol > li {
|
|
355
355
|
counter-increment: slide-ol;
|
|
356
356
|
padding-left: 64px;
|
|
357
357
|
position: relative;
|
|
358
358
|
}
|
|
359
|
-
.slide-body ol li::before {
|
|
359
|
+
.slide-body ol > li::before {
|
|
360
360
|
content: counter(slide-ol, decimal-leading-zero);
|
|
361
361
|
position: absolute;
|
|
362
362
|
left: 0;
|
|
@@ -487,3 +487,16 @@ strong { color: var(--ink); font-weight: 600; }
|
|
|
487
487
|
overflow-x: auto;
|
|
488
488
|
overflow-y: hidden;
|
|
489
489
|
}
|
|
490
|
+
|
|
491
|
+
/* ─────────────────────────────────────────────
|
|
492
|
+
QUOTATIONS — a rule, not a box: a red bar to the left, the quote set
|
|
493
|
+
in a little, in the text's own size
|
|
494
|
+
───────────────────────────────────────────── */
|
|
495
|
+
.slide-body blockquote {
|
|
496
|
+
margin: 0 0 var(--gap-item);
|
|
497
|
+
padding: 2px 0 2px 32px;
|
|
498
|
+
border: 0;
|
|
499
|
+
border-left: 6px solid var(--accent);
|
|
500
|
+
max-width: 1200px;
|
|
501
|
+
color: var(--ink);
|
|
502
|
+
}
|
|
@@ -592,6 +592,20 @@ labels:
|
|
|
592
592
|
| `follow.tryHere` | Try following here | Mitverfolgen hier ausprobieren |
|
|
593
593
|
| `question.empty` | Answers appear here. | Hier erscheinen die Antworten. |
|
|
594
594
|
| `scale.average` | Average {n} | Durchschnitt {n} |
|
|
595
|
+
| `poll.answeredOf` | {n} of {of} answered | {n} von {of} haben geantwortet |
|
|
596
|
+
| `poll.closed` | Closed | Geschlossen |
|
|
597
|
+
| `poll.several` | Several answers possible. | Mehrere Antworten möglich. |
|
|
598
|
+
| `poll.pickSeveral` | Tap all that apply. | Alle zutreffenden antippen. |
|
|
599
|
+
| `poll.thanksSeveral` | Thanks! You chose {choice}. Tap to change. | Danke! Gewählt: {choice}. Zum Ändern antippen. |
|
|
600
|
+
| `respond.closed` | Closed: no more answers. | Geschlossen: keine weiteren Antworten. |
|
|
601
|
+
| `respond.right` | Right! | Richtig! |
|
|
602
|
+
| `respond.wrong` | Not quite. The right answer: {answer} | Leider nicht. Richtig ist: {answer} |
|
|
603
|
+
| `respond.solution` | The right answer: {answer} | Richtig ist: {answer} |
|
|
604
|
+
| `numeric.placeholder` | A number | Eine Zahl |
|
|
605
|
+
| `numeric.invalid` | Enter a number, such as 0.5 or 1/2. | Eine Zahl eingeben, z. B. 0,5 oder 1/2. |
|
|
606
|
+
| `numeric.sent` | Sent: {value}. Send another to change. | Gesendet: {value}. Zum Ändern eine andere senden. |
|
|
607
|
+
| `numeric.other` | Other | Andere |
|
|
608
|
+
| `numeric.right` | {n} right | {n} richtig |
|
|
595
609
|
|
|
596
610
|
---
|
|
597
611
|
|
|
@@ -742,24 +756,33 @@ For `play="auto"` the video pauses and resets to the beginning when you navigate
|
|
|
742
756
|
| Attribute | Default | Description |
|
|
743
757
|
|---|---|---|
|
|
744
758
|
| `room` | `poll` | Name of the room that collects the answers; unique per poll in a deck. Letters, digits, `.`, `-`, `_` |
|
|
745
|
-
| `options` | — | Answers separated by
|
|
746
|
-
| `question` | — | Question text
|
|
759
|
+
| `options` | — | Answers separated by `\|`. Markdown and `$…$` maths, as on a slide (see below) |
|
|
760
|
+
| `question` | — | Question text, Markdown and maths like `options`; phones fall back to the slide heading |
|
|
747
761
|
| `qr` | `true` | `false` leaves out the QR code (shown earlier with `<qrcode join />`) |
|
|
748
|
-
| `answer` | — | The right answer (several separated by `\|`): a button with a tick outlines its label, bar and count in green, also in the audience window and on paired devices. `--poll-correct` sets the colour |
|
|
762
|
+
| `answer` | — | The right answer (several separated by `\|`): a button with a tick outlines its label, bar and count in green, also in the audience window and on paired devices, and each phone says whether its vote was right. `--poll-correct` sets the colour |
|
|
763
|
+
| `multiple` | — | Each phone may pick several options; the slide and the phones say so. A vote is then the list of options picked, and is right when it is exactly the `answer` options |
|
|
764
|
+
| `buttons` | — | `letters` puts A, B, C … before the options, and the phones show only those; `numbers` does the same with 1, 2, 3 …. For options better read on the slide, such as pictures (`options="\|"`) |
|
|
749
765
|
| `results` | — | `hidden` starts with the results hidden, e.g. for peer instruction. A button with an eye shows or hides them (bars and counts) at any time, also in the audience window and on paired devices. Hidden, the audience window shows no bars; the presenter's own screens show them faint |
|
|
750
766
|
|
|
751
|
-
|
|
767
|
+
Four more activities take the same `room`, `question` and `qr`:
|
|
752
768
|
|
|
753
769
|
| Tag | Attributes | Phones | Slide |
|
|
754
770
|
|---|---|---|---|
|
|
755
771
|
| `<scale>` | `min` (1), `max` (5), `low`, `high` | One button per number, `low`/`high` as labels | Count per number and the average; latest answer per device |
|
|
772
|
+
| `<numeric>` | `answer`, `tolerance` (0), `limit` (6), `placeholder`, `results` | A number field: `0.5`, `0,5`, `1/2`, `1e-3`; latest answer per device | The most frequent answers as bars, the rest as Other; with `answer`, the tick marks the right ones, counts them and tells each phone |
|
|
756
773
|
| `<wordcloud>` | `placeholder`, `limit` (60), `height` (520) | Text field, up to 40 characters, repeatable | A packed word cloud (d3-cloud): size by frequency, some words upright, case-insensitive |
|
|
757
774
|
| `<question>` | `placeholder`, `limit` (8) | Text field, up to 200 characters, repeatable | The newest answers as cards |
|
|
758
775
|
|
|
759
776
|
`<qrcode follow />` shows the link for following the slides on phones and laptops (`?view=follow`), with `mdeck run --network` or a hosted deck with a `server` setting; the phones' answer page links to it as well. `<qrcode join />` shows the deck's join code large with its link (`size`, default 420), for a slide that invites everyone once; activities after it can use `qr="false"`. QR codes (`<qrcode>`, activities) are SVG in the slide's `--ink` on a transparent background; `--qr-ink` and `--qr-bg` override the colours.
|
|
760
777
|
|
|
778
|
+
Options and the question may use Markdown and maths, e.g. `options="$2x$|$x^2$|$\frac{x^3}{3}$"`. A `|` inside a formula belongs to it (`$|x|$` is one option), and `\|` is a bar in plain text. The slide draws maths with KaTeX, the phones as MathML; votes and `answer` use the text as written. The other activities' `question`, and the scale's `low` and `high`, take Markdown and maths too.
|
|
779
|
+
|
|
761
780
|
The slide shows live bars, the number of answers, a QR code and a short link. Phones never load the deck: they open the server's own answer page at `<server>/<code>`, where the six-digit session code is the same for every poll in the deck. The presenter's screen (the presenter view or a full deck window, never an embedded preview) announces the poll on the current slide with what the phones should show and the deck's look; the server only accepts that from the presenter. Between polls the phones wait. Each device's latest vote counts. The presenter can reset the room (hover over the results).
|
|
762
781
|
|
|
782
|
+
Every activity has a lock button that closes it: answers after that do not count, and the phones say it is closed until it opens again. Polls and `<numeric>` also have the eye that hides or shows the results. Below the results, polls, scales and `<numeric>` count “12 of 31 answered” while phones are connected (people following the slides count as phones). The lock, the eye and the tick reach every screen of the talk, also after a reload: they are kept in the activity room's state on the server, which only the presenter may set, and phones read it there. Reset clears the answers and opens the activity again.
|
|
783
|
+
|
|
784
|
+
While `mdeck run` runs, the answers are kept beside the deck in `<slides>.results.json`: per activity room, each answer with its time, the phones numbered per room (their ids are not kept), and, once an activity was closed, only the answers that counted. Reset takes a room out of the file; a new, empty server leaves the file alone. Builds, `mdeck send` and `mdeck pdf` show the kept answers where no room server answers (`--no-results` leaves them out), and `mdeck results [slides.md] [-o file.csv]` prints them as CSV: `room,phone,at,answer`, several picks joined by `|`.
|
|
785
|
+
|
|
763
786
|
`--no-polls` leaves slides with an activity or a join code (`<qrcode join />`) out of `mdeck send`, `mdeck build --reader` and `mdeck pdf`; without it they show as a record of the talk. Saved drawings follow the remaining slides.
|
|
764
787
|
|
|
765
788
|
Rooms run inside `mdeck run` (add `--network` so phones can reach it) or on a server started with `mdeck server`, set in the deck:
|
|
@@ -28,19 +28,47 @@ Every poll in a presentation shows the same code, so people scan once. Their pho
|
|
|
28
28
|
| Setting | Meaning |
|
|
29
29
|
|---|---|
|
|
30
30
|
| `room` | A short name for this question, different for each poll in the deck. Letters, digits, hyphens and underscores. |
|
|
31
|
-
| `answer` | Optional: the right answer, written exactly like the option. A button with a tick then appears under the bars; tapping it outlines the right answer, its bar and its number in green, and
|
|
31
|
+
| `answer` | Optional: the right answer, written exactly like the option (several separated by `\|`). A button with a tick then appears under the bars; tapping it outlines the right answer, its bar and its number in green, and each phone says whether its vote was right. Tapping again hides it. |
|
|
32
32
|
| `options` | The answers, separated by `\|` |
|
|
33
33
|
| `question` | Optional question text. Without it, phones show the slide's heading. |
|
|
34
|
+
| `multiple` | Each phone may pick several options. The slide and the phones say “Several answers possible”. Without it, one option per phone. |
|
|
35
|
+
| `buttons` | `letters` puts A, B, C … before the options on the slide, and the phones show only those letters; `numbers` does the same with 1, 2, 3 … For options that are pictures or long formulas, better read on the big screen. |
|
|
36
|
+
| `results` | `hidden` starts with the bars hidden, e.g. to let people vote before they see what others chose. |
|
|
34
37
|
| `qr` | `false` leaves out the QR code, when an earlier slide showed it with `<qrcode join />`. |
|
|
35
38
|
|
|
39
|
+
The question and the options may contain formatting and formulas, written as on a slide, so a maths lecture can ask:
|
|
40
|
+
|
|
41
|
+
```markdown
|
|
42
|
+
<poll room="derivative" question="What is $\frac{d}{dx}\, x^2$?" options="$2x$|$x^2$|$\frac{x^3}{3}$" answer="$2x$" />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Both the slide and the phones show the formulas. A `|` inside a formula stays in it, so `$|x|$` is one option; to put a `|` in plain text, write `\|`.
|
|
46
|
+
|
|
47
|
+
Options can also be pictures. Show only letters on the phones then:
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
<poll room="graph" question="Which graph shows $f(x) = e^{-x}$?" options="||" answer="" buttons="letters" />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### During the poll
|
|
54
|
+
|
|
55
|
+
Below the bars, the slide counts the answers, “12 of 31 answered” while phones are connected. The presenter has three buttons there (the audience window does not show them):
|
|
56
|
+
|
|
57
|
+
- **The lock** closes the poll. Answers after that do not count, and the phones say the poll is closed until you open it again.
|
|
58
|
+
- **The eye** hides or shows the bars. Hidden, the audience sees no bars; your own screens show them faint.
|
|
59
|
+
- **The tick** (with `answer`) shows the right answer: on the slide, and on each phone, which says whether its answer was right.
|
|
60
|
+
|
|
61
|
+
Together they make a peer-instruction round: start with `results="hidden"`, let people vote, close, show the bars, let them discuss, and show the answer. **Reset** clears the answers and opens the poll again.
|
|
62
|
+
|
|
36
63
|
With `lang: de` in the settings, the slide and the phones show German words, such as “Scannen und abstimmen”. The phones use the presentation's colours and fonts; if you pick another theme or palette in the presenter view, the phones change with it.
|
|
37
64
|
|
|
38
65
|
## Other kinds of questions
|
|
39
66
|
|
|
40
|
-
Besides a poll,
|
|
67
|
+
Besides a poll, four more kinds work the same way: one line on a slide, answered on the phones.
|
|
41
68
|
|
|
42
69
|
```markdown
|
|
43
70
|
<scale room="pace" min="1" max="5" low="Too slow" high="Too fast" />
|
|
71
|
+
<numeric room="limit" question="What is $\lim_{x \to 0} \frac{\sin x}{x}$?" answer="1" />
|
|
44
72
|
<wordcloud room="mood" placeholder="One word" />
|
|
45
73
|
<question room="ask" placeholder="Your question" />
|
|
46
74
|
```
|
|
@@ -48,10 +76,11 @@ Besides a poll, three more kinds work the same way: one line on a slide, answere
|
|
|
48
76
|
| Tag | Phones show | The slide shows |
|
|
49
77
|
|---|---|---|
|
|
50
78
|
| `<scale>` | The numbers from `min` to `max` (1 to 5 unless set, at most 11 steps), with `low` and `high` as labels at the ends | How many chose each number, and the average. Each phone's latest answer counts |
|
|
79
|
+
| `<numeric>` | A field for a number: `0.5`, `0,5`, `1/2` or `1e-3` | The most frequent answers as bars, six unless `limit` says otherwise, and the rest as Other. With `answer`, the tick marks the right ones and counts them, and each phone hears whether it was right; `tolerance` (0 unless set) says how far off still counts. Each phone's latest answer counts |
|
|
51
80
|
| `<wordcloud>` | A short text field | A cloud of every answer, packed around the middle, larger the more often it came in and some upright. Capitals and extra spaces do not matter. People may send several. `height` sets its height (520). Colours come from the palette; `--cloud-1` to `--cloud-6` override them |
|
|
52
81
|
| `<question>` | A text field | The newest answers as cards, eight unless `limit` says otherwise. People may send several |
|
|
53
82
|
|
|
54
|
-
All of them take `room`, `question` and `qr` like a poll, and `placeholder` for the text fields.
|
|
83
|
+
All of them take `room`, `question` and `qr` like a poll, and `placeholder` for the text fields. Their `question`, and the scale's `low` and `high`, may contain formatting and formulas like a poll's options. All of them have the lock that closes them; `<numeric>` also has the eye and the tick, and takes `results="hidden"`.
|
|
55
84
|
|
|
56
85
|
## Show the code once
|
|
57
86
|
|
|
@@ -175,9 +204,11 @@ Two presentations with the same title share a session code. Give one of them `se
|
|
|
175
204
|
|
|
176
205
|
Answers are anonymous. Each phone gets a random number so it can change its vote; nothing else identifies it. The server keeps answers in memory only. It forgets a room after twelve hours without visitors and everything when it restarts. Each phone can send a limited number of answers in a short time.
|
|
177
206
|
|
|
207
|
+
While you present with `mdeck run`, the answers are also kept beside the presentation, in `my-talk.results.json`. It holds each answer with its time, and the phones numbered 1, 2, 3 … per question instead of their random numbers. Once a question was closed, only the answers that counted. **Reset** takes a question's answers out of the file; a new server that starts empty leaves the file as it is. `mdeck results my-talk.md` prints the answers as CSV (question, phone, time, answer), to open in a spreadsheet; `-o answers.csv` saves them to a file. A deck hosted with its own `server` keeps nothing beside it.
|
|
208
|
+
|
|
178
209
|
## In a PDF or a shared file
|
|
179
210
|
|
|
180
|
-
A PDF, a file made with `mdeck send` and a folder hosted without a poll server keep your questions as a record of the talk
|
|
211
|
+
A PDF, a file made with `mdeck send` and a folder hosted without a poll server keep your questions as a record of the talk, without a join code. With answers kept in `my-talk.results.json`, each shows its results as they were at the end of the talk; without, its question and options with a note that it was answered live. `--no-results` leaves the kept answers out. If the deck's `server` still answers when the file is opened, the results it holds appear instead. Printed slides never show the join code or the presenter's buttons.
|
|
181
212
|
|
|
182
213
|
## Make your own activity
|
|
183
214
|
|
|
@@ -97,6 +97,8 @@ The design page shows themes and palettes on a sample deck with every kind of sl
|
|
|
97
97
|
| `mdeck send my-talk.md --no-pdf` | The same, without rendering the PDF |
|
|
98
98
|
| `mdeck pdf my-talk.md -o talk.pdf` | Render the slides to a PDF file (`--no-polls` leaves out the slides with polls) |
|
|
99
99
|
| `--no-drawings` | Leave out the drawings from `my-talk.drawings.json`, for `build`, `send` and `pdf` |
|
|
100
|
+
| `--no-results` | Leave out the answers kept in `my-talk.results.json`, for `build`, `send` and `pdf`; the polls then show their questions only |
|
|
101
|
+
| `mdeck results my-talk.md` | The answers kept during the talk, as CSV; `-o answers.csv` saves them to a file |
|
|
100
102
|
|
|
101
103
|
See [Send, host or print your slides](sharing.html) for help choosing between them. Unknown options, missing option values and extra filenames stop the command before it changes any output. `--output=path` and `-o path` are both accepted. `mdeck preview --no-open --port 4200` previews without opening a browser and chooses a port.
|
|
102
104
|
|
package/package.json
CHANGED
|
@@ -94,7 +94,7 @@ Right column
|
|
|
94
94
|
- Named areas use `:::slot name … :::`; the layout's regions and settings come from `mdeck list layouts --json`.
|
|
95
95
|
- `:::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.
|
|
96
96
|
- Code fences get syntax highlighting; `$…$` and `$$…$$` render math.
|
|
97
|
-
- `<poll room="lunch" options="Mensa|Thai|Pizza" />` lets the audience vote on their phones through a QR code on the slide; each poll needs its own `room`. `<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. With several of them, put `<qrcode join />` (the deck's join code) on an early slide to show the QR code once, and add `qr="false"` to the questions. It works when the deck is presented with `mdeck run <deck>.md --network` or with a server set as `server:` in the deck settings.
|
|
97
|
+
- `<poll room="lunch" options="Mensa|Thai|Pizza" />` lets the audience vote on their phones through a QR code on the slide; each poll needs its own `room`. `question` and `options` may use Markdown and `$…$` maths (`options="$2x$|$x^2$"`); `answer` repeats the right option's text. `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. With several of them, put `<qrcode join />` (the deck's join code) on an early slide to show the QR code once, and add `qr="false"` to the questions. It works when the deck is presented with `mdeck run <deck>.md --network` or with a server set as `server:` in the deck settings.
|
|
98
98
|
|
|
99
99
|
## 3. Write the deck
|
|
100
100
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readFileSync, statSync } from 'fs'
|
|
2
|
-
import { resolve, dirname, extname } from 'path'
|
|
1
|
+
import { readFileSync, statSync, existsSync } from 'fs'
|
|
2
|
+
import { resolve, dirname, extname, basename } from 'path'
|
|
3
3
|
import yaml from 'js-yaml'
|
|
4
4
|
import { parseSlides } from '../core/parseSlides.js'
|
|
5
5
|
import { scanCitations, citedKeys } from '../core/citations.js'
|
|
6
|
+
import { sampleDeck, SAMPLE_REFERENCES, SAMPLE_REFERENCES_FILE } from '../editor/sampleDeck.js'
|
|
6
7
|
|
|
7
8
|
// The deck's references, formatted here in Node with citeproc so that none of
|
|
8
9
|
// it travels in the deck: the page receives finished HTML for every citation
|
|
@@ -26,8 +27,14 @@ async function readItems(file) {
|
|
|
26
27
|
const { mtimeMs } = statSync(file)
|
|
27
28
|
const cached = parsed.get(file)
|
|
28
29
|
if (cached?.mtimeMs === mtimeMs) return cached.items
|
|
29
|
-
const
|
|
30
|
-
|
|
30
|
+
const items = await parseReferences(readFileSync(file, 'utf-8'), extname(file))
|
|
31
|
+
parsed.set(file, { mtimeMs, items })
|
|
32
|
+
return items
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Reference file text as CSL-JSON items; `ext` names the format.
|
|
36
|
+
export async function parseReferences(text, ext) {
|
|
37
|
+
ext = ext.toLowerCase()
|
|
31
38
|
let items
|
|
32
39
|
if (ext === '.json') items = JSON.parse(text)
|
|
33
40
|
else if (ext === '.yml' || ext === '.yaml') {
|
|
@@ -40,18 +47,20 @@ async function readItems(file) {
|
|
|
40
47
|
items = new Cite(text, { forceType: '@biblatex/text' }).data
|
|
41
48
|
} else throw new Error(`unknown reference format "${ext}" (use .bib, .json or .yml)`)
|
|
42
49
|
if (!Array.isArray(items)) throw new Error('expected a list of references')
|
|
43
|
-
|
|
44
|
-
parsed.set(file, { mtimeMs, items })
|
|
45
|
-
return items
|
|
50
|
+
return items.filter(item => item && item.id != null).map(item => ({ ...item, id: String(item.id) }))
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
// `sample`: the sample deck's references stand in for a missing file of
|
|
54
|
+
// theirs (the dev and edit servers show the sample deck without a folder).
|
|
55
|
+
export async function loadBibliography(files, { sample = false } = {}) {
|
|
49
56
|
const items = []
|
|
50
57
|
const seen = new Set()
|
|
51
58
|
const diagnostics = []
|
|
52
59
|
for (const file of files) {
|
|
53
60
|
let found
|
|
54
|
-
try {
|
|
61
|
+
try {
|
|
62
|
+
found = sample && basename(file) === SAMPLE_REFERENCES_FILE && !existsSync(file) ? await parseReferences(SAMPLE_REFERENCES, '.bib') : await readItems(file)
|
|
63
|
+
} catch (error) {
|
|
55
64
|
diagnostics.push({ severity: 'warning', code: 'bibliography-file', message: error.code === 'ENOENT' ? `Bibliography ${file} not found` : `Bibliography ${file}: ${error.message}` })
|
|
56
65
|
continue
|
|
57
66
|
}
|
|
@@ -275,15 +284,24 @@ export async function formatBibliography({ items, texts, csl, lang, nocite = [],
|
|
|
275
284
|
|
|
276
285
|
// Everything the deck's `bibliography`, `csl`, `nocite` and `lang` ask for,
|
|
277
286
|
// or null when the deck has no bibliography.
|
|
278
|
-
export async function resolveBibliography(deckPath, source) {
|
|
287
|
+
export async function resolveBibliography(deckPath, source, { sample = false } = {}) {
|
|
279
288
|
let deckConfig
|
|
280
289
|
try { deckConfig = parseSlides(source).deckConfig ?? {} } catch { return null }
|
|
281
290
|
const files = bibliographyFiles(deckPath, deckConfig)
|
|
282
291
|
if (!files.length) return null
|
|
283
|
-
const loaded = await loadBibliography(files)
|
|
292
|
+
const loaded = await loadBibliography(files, { sample })
|
|
284
293
|
const formatted = await formatBibliography({
|
|
285
294
|
items: loaded.items, texts: deckTexts(source), csl: deckConfig.csl, lang: deckConfig.lang,
|
|
286
295
|
nocite: deckConfig.nocite ?? [], deckDir: dirname(resolve(deckPath)),
|
|
287
296
|
})
|
|
288
297
|
return { ...formatted, diagnostics: [...loaded.diagnostics, ...formatted.diagnostics], files, style: styleFile(deckPath, deckConfig) }
|
|
289
298
|
}
|
|
299
|
+
|
|
300
|
+
// The sample deck's citations, formatted once: the design page previews it
|
|
301
|
+
// in the editor of any deck, whose own references are another matter.
|
|
302
|
+
let sampleFormatted = null
|
|
303
|
+
export function sampleBibliography() {
|
|
304
|
+
sampleFormatted ??= parseReferences(SAMPLE_REFERENCES, '.bib')
|
|
305
|
+
.then(items => formatBibliography({ items, texts: deckTexts(sampleDeck()) }))
|
|
306
|
+
return sampleFormatted
|
|
307
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Keeping the answers while `mdeck run` runs: the presenter's screens send
|
|
2
|
+
// each activity's answers as they come in (src/live/client.js), and they go
|
|
3
|
+
// into <deck>.results.json beside the deck (src/core/results.js).
|
|
4
|
+
//
|
|
5
|
+
// POST /__mdeck/results { room, closed, answers } → { ok }
|
|
6
|
+
//
|
|
7
|
+
// Answers of none take the room out of the file, as Reset does. Writes wait
|
|
8
|
+
// a moment, so a burst of votes is one write, and do not reload the open
|
|
9
|
+
// windows (ownWrites.js).
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
|
|
11
|
+
import { resolve } from 'node:path'
|
|
12
|
+
import { resultsFileFor, normalizeResults, withRoom, serializeResults } from '../core/results.js'
|
|
13
|
+
import { isAllowedRequest, send, readJson } from './editorPlugin.js'
|
|
14
|
+
import { markOwnWrite } from './ownWrites.js'
|
|
15
|
+
|
|
16
|
+
export function createResultsFile(deckPath, { debounceMs = 500 } = {}) {
|
|
17
|
+
const path = resultsFileFor(resolve(deckPath))
|
|
18
|
+
let results = null, timer = null
|
|
19
|
+
|
|
20
|
+
const current = () => {
|
|
21
|
+
if (results) return results
|
|
22
|
+
try { results = existsSync(path) ? normalizeResults(JSON.parse(readFileSync(path, 'utf8'))) : normalizeResults({}) }
|
|
23
|
+
catch { results = normalizeResults({}) }
|
|
24
|
+
return results
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function flush() {
|
|
28
|
+
clearTimeout(timer)
|
|
29
|
+
timer = null
|
|
30
|
+
if (!results) return
|
|
31
|
+
// A deck that never kept answers gets no empty file.
|
|
32
|
+
if (!Object.keys(results.rooms).length && !existsSync(path)) return
|
|
33
|
+
const text = serializeResults(results)
|
|
34
|
+
markOwnWrite(path, text)
|
|
35
|
+
const temp = `${path}.${process.pid}.tmp`
|
|
36
|
+
writeFileSync(temp, text, 'utf8')
|
|
37
|
+
renameSync(temp, path)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
path,
|
|
42
|
+
read: current,
|
|
43
|
+
save(room, entry) {
|
|
44
|
+
results = withRoom(current(), room, entry)
|
|
45
|
+
clearTimeout(timer)
|
|
46
|
+
timer = setTimeout(flush, debounceMs)
|
|
47
|
+
},
|
|
48
|
+
flush,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resultsMiddleware(file, { authorize = isAllowedRequest } = {}) {
|
|
53
|
+
return async (request, response, next) => {
|
|
54
|
+
const { pathname } = new URL(request.url, 'http://localhost')
|
|
55
|
+
if (pathname !== '/') return next()
|
|
56
|
+
if (request.method !== 'POST') { response.setHeader('Allow', 'POST'); return send(response, 405, { error: 'Method not allowed' }) }
|
|
57
|
+
if (!authorize(request)) return send(response, 403, { error: 'Only the presenter keeps the results' })
|
|
58
|
+
try {
|
|
59
|
+
const { room, closed, answers } = await readJson(request)
|
|
60
|
+
file.save(String(room ?? ''), { closed, answers })
|
|
61
|
+
return send(response, 200, { ok: true })
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return send(response, error.status ?? 500, { error: error.message })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resultsPlugin(slidesPath, { authorize } = {}) {
|
|
69
|
+
return {
|
|
70
|
+
name: 'vite-plugin-mdeck-results',
|
|
71
|
+
configureServer(server) {
|
|
72
|
+
const file = createResultsFile(slidesPath)
|
|
73
|
+
server.middlewares.use('/__mdeck/results', resultsMiddleware(file, { authorize }))
|
|
74
|
+
server.httpServer?.once('close', () => file.flush())
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|