mdeck 3.3.0 → 3.5.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/assets/base.css +23 -2
  4. package/assets/extensions/palettes/cobalt/extension.toml +30 -0
  5. package/assets/extensions/palettes/greyscale/extension.toml +57 -0
  6. package/assets/extensions/themes/academic/styles.css +18 -5
  7. package/assets/extensions/themes/glass/extension.toml +59 -0
  8. package/assets/extensions/themes/glass/styles.css +754 -0
  9. package/assets/extensions/themes/minimal/styles.css +16 -4
  10. package/assets/extensions/themes/neue/styles.css +17 -4
  11. package/assets/extensions/themes/plain/extension.toml +58 -0
  12. package/assets/extensions/themes/plain/styles.css +471 -0
  13. package/assets/extensions/themes/work/extension.toml +57 -0
  14. package/assets/extensions/themes/work/styles.css +512 -0
  15. package/docs/reference/authoring.md +50 -10
  16. package/docs/reference/palettes.md +4 -3
  17. package/docs/reference/themes.md +5 -3
  18. package/docs/site/content/appearance.md +24 -10
  19. package/docs/site/content/audience.md +49 -4
  20. package/docs/site/content/commands.md +2 -0
  21. package/docs/site/content/theme-repository.md +7 -7
  22. package/docs/site/images/themes/academic-dark.webp +0 -0
  23. package/docs/site/images/themes/academic-light.webp +0 -0
  24. package/docs/site/images/themes/academic.webp +0 -0
  25. package/docs/site/images/themes/glass-dark.webp +0 -0
  26. package/docs/site/images/themes/glass-light.webp +0 -0
  27. package/docs/site/images/themes/glass.webp +0 -0
  28. package/docs/site/images/themes/minimal-dark.webp +0 -0
  29. package/docs/site/images/themes/minimal-light.webp +0 -0
  30. package/docs/site/images/themes/minimal.webp +0 -0
  31. package/docs/site/images/themes/neue-dark.webp +0 -0
  32. package/docs/site/images/themes/neue-light.webp +0 -0
  33. package/docs/site/images/themes/neue.webp +0 -0
  34. package/docs/site/images/themes/plain-dark.webp +0 -0
  35. package/docs/site/images/themes/plain-light.webp +0 -0
  36. package/docs/site/images/themes/plain.webp +0 -0
  37. package/docs/site/images/themes/work-dark.webp +0 -0
  38. package/docs/site/images/themes/work-light.webp +0 -0
  39. package/docs/site/images/themes/work.webp +0 -0
  40. package/examples/showcase/slides.md +2 -2
  41. package/package.json +4 -1
  42. package/skills/write-slides/SKILL.md +2 -2
  43. package/src/build/bibliography.js +29 -11
  44. package/src/build/resultsPlugin.js +77 -0
  45. package/src/build/slidesPlugin.js +42 -18
  46. package/src/build/themeCheck.js +2 -1
  47. package/src/cli/args.js +2 -1
  48. package/src/cli/index.js +17 -4
  49. package/src/components/Numeric.jsx +59 -0
  50. package/src/components/Poll.jsx +72 -57
  51. package/src/components/Question.jsx +10 -6
  52. package/src/components/Scale.jsx +13 -8
  53. package/src/components/WordCloud.jsx +10 -6
  54. package/src/components/activity.jsx +88 -7
  55. package/src/components/inlineText.js +63 -0
  56. package/src/components/numbers.js +39 -0
  57. package/src/components/poll.css +26 -13
  58. package/src/core/editDeck.js +5 -6
  59. package/src/core/icons.js +2 -0
  60. package/src/core/labels.js +32 -0
  61. package/src/core/pollList.js +66 -0
  62. package/src/core/results.js +64 -0
  63. package/src/core/tags.js +195 -0
  64. package/src/editor/sampleDeck.js +34 -8
  65. package/src/extensions/discover.js +2 -2
  66. package/src/extensions/moved.js +4 -4
  67. package/src/layouts/layoutApi.jsx +24 -13
  68. package/src/live/answer/answer.css +19 -0
  69. package/src/live/answer/answer.js +147 -24
  70. package/src/live/client.js +51 -6
  71. package/src/live/roomTag.js +11 -15
  72. package/src/live/rooms.js +6 -1
  73. package/src/runtime/citations.js +15 -5
  74. package/src/runtime/ink/bus.js +7 -13
  75. package/src/runtime/main.jsx +5 -2
  76. package/src/runtime/markedSetup.js +3 -0
  77. package/src/runtime/registry.jsx +2 -0
  78. package/assets/extensions/palettes/neon/extension.toml +0 -30
  79. package/assets/extensions/themes/aurora/extension.toml +0 -40
  80. package/assets/extensions/themes/aurora/styles.css +0 -644
  81. package/docs/site/images/themes/aurora-dark.webp +0 -0
  82. package/docs/site/images/themes/aurora-light.webp +0 -0
  83. package/docs/site/images/themes/aurora.webp +0 -0
@@ -5,7 +5,7 @@ A **theme** chooses the fonts and the style of the slides. A **palette** chooses
5
5
  Ask your assistant by name or by description:
6
6
 
7
7
  ```prompt
8
- Use the aurora theme, dark. Or suggest a look that suits a talk for
8
+ Use the glass theme, light. Or suggest a look that suits a talk for
9
9
  a design conference.
10
10
  ```
11
11
 
@@ -26,9 +26,11 @@ If you already have a settings block there, change the existing lines rather tha
26
26
  | Name to use | What it looks like |
27
27
  |---|---|
28
28
  | `neue` | Swiss style: large type, clear lines, red on white |
29
- | `aurora` | Northern lights: curtains of light, night-sky chapters and glass |
29
+ | `plain` | Black on white and lots of room, the heading beside the content |
30
30
  | `minimal` | Nothing but the content: system fonts, calm type, no decoration; works offline |
31
31
  | `academic` | A lecture: compact slides, theorem and definition boxes, tables like in a paper |
32
+ | `work` | An everyday company template: a coloured band, your logo, numbered sections, a footer |
33
+ | `glass` | Liquid glass: soft light behind frosted panes; it starts dark |
32
34
 
33
35
  Each theme comes with colours that suit it, so you do not have to choose any. More themes are in the theme repository (see “Themes that moved to the repository” below).
34
36
 
@@ -42,11 +44,11 @@ Each theme in its own colours, on the title slide of the tour example. Click a p
42
44
 
43
45
  Default colours: `swiss`.
44
46
 
45
- ### Aurora
47
+ ### Plain
46
48
 
47
- [![The tour's title slide in the Aurora theme](images/themes/aurora.webp)](images/themes/aurora.webp)
49
+ [![The tour's title slide in the Plain theme](images/themes/plain.webp)](images/themes/plain.webp)
48
50
 
49
- Default colours: `neon`.
51
+ Default colours: `greyscale`. In the dark it is the same, inverted.
50
52
 
51
53
  ### Academic
52
54
 
@@ -60,6 +62,18 @@ Default colours: `nordic`.
60
62
 
61
63
  Default colours: `paper`. It uses the fonts already on the computer, so it needs no internet connection and looks plain on purpose.
62
64
 
65
+ ### Work
66
+
67
+ [![The tour's title slide in the Work theme](images/themes/work.webp)](images/themes/work.webp)
68
+
69
+ Default colours: `cobalt`. Set `meta.logo` and it shows top left; any palette makes it your company's colours.
70
+
71
+ ### Glass
72
+
73
+ [![The tour's title slide in the Glass theme](images/themes/glass.webp)](images/themes/glass.webp)
74
+
75
+ Default colours: `nordic`, dark.
76
+
63
77
  ## Choose colours
64
78
 
65
79
  To use other colours, set `palette` to one of these:
@@ -67,8 +81,9 @@ To use other colours, set `palette` to one of these:
67
81
  | Name to use | Colours |
68
82
  |---|---|
69
83
  | `swiss` | White, black and signal red (neue's own) |
70
- | `nordic` | Ice grey, deep navy and a cool steel blue (academic's own) |
71
- | `neon` | Violet and magenta, loud (aurora's own) |
84
+ | `nordic` | Ice grey, deep navy and a cool steel blue (academic's and glass's own) |
85
+ | `cobalt` | Strong ultramarine on white with vivid orange (work's own) |
86
+ | `greyscale` | Black, white and greys only, good for printing (plain's own) |
72
87
  | `lagoon` | Teal on warm stone |
73
88
  | `paper` | Off-white and near-black with one orange accent (minimal's own) |
74
89
 
@@ -97,8 +112,6 @@ Some slides use the other version on purpose: neue's statement slides are dark i
97
112
 
98
113
  The first chapter slide of the tour in each palette a theme offers. Choose a theme above the pictures; click a sheet to see it larger. A theme that offers only its own colours is not shown here.
99
114
 
100
- Some themes turn their chapter slides around on purpose: aurora's are a night sky in a light talk, and light in a dark one.
101
-
102
115
  ### Light
103
116
 
104
117
  <!-- theme-sheets light -->
@@ -127,7 +140,8 @@ Earlier versions of mdeck came with more themes and palettes. They are now in th
127
140
 
128
141
  | Theme | What it looks like (its palette) |
129
142
  |---|---|
130
- | `duet` | Two accent colours that take turns, slab headings (`cobalt`) |
143
+ | `aurora` | Northern lights: curtains of light, night-sky chapters (`neon`) |
144
+ | `duet` | Two accent colours that take turns, slab headings (`cobalt`, which comes with mdeck) |
131
145
  | `editorial` | A magazine spread with serif headlines and pull quotes (`terra`) |
132
146
  | `fhnw` | The FHNW visual identity, with its own `brand` palette only |
133
147
  | `terminal` | Headings typed at a prompt, a status bar; it starts dark (`phosphor`) |
@@ -28,19 +28,61 @@ 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 tapping again hides it. The audience window shows it too. |
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
+ When the options get long, write them as a list inside the tag instead, one option per line, and tick the right one with `[x]`:
48
+
49
+ ```markdown
50
+ <poll room="derivative" question="What is $\frac{d}{dx}\, x^2$?">
51
+
52
+ - [x] $2x$
53
+ - [ ] $x^2$
54
+ - [ ] $\frac{x^3}{3}$
55
+
56
+ </poll>
57
+ ```
58
+
59
+ Nothing needs escaping there, and the right answer is marked where it stands.
60
+
61
+ Options can also be pictures. Show only letters on the phones then:
62
+
63
+ ```markdown
64
+ <poll room="graph" question="Which graph shows $f(x) = e^{-x}$?" options="![](a.svg)|![](b.svg)|![](c.svg)" answer="![](b.svg)" buttons="letters" />
65
+ ```
66
+
67
+ ### During the poll
68
+
69
+ 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):
70
+
71
+ - **The lock** closes the poll. Answers after that do not count, and the phones say the poll is closed until you open it again.
72
+ - **The eye** hides or shows the bars. Hidden, the audience sees no bars; your own screens show them faint.
73
+ - **The tick** (with `answer`) shows the right answer: on the slide, and on each phone, which says whether its answer was right.
74
+
75
+ 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.
76
+
36
77
  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
78
 
38
79
  ## Other kinds of questions
39
80
 
40
- Besides a poll, three more kinds work the same way: one line on a slide, answered on the phones.
81
+ Besides a poll, four more kinds work the same way: one line on a slide, answered on the phones.
41
82
 
42
83
  ```markdown
43
84
  <scale room="pace" min="1" max="5" low="Too slow" high="Too fast" />
85
+ <numeric room="limit" question="What is $\lim_{x \to 0} \frac{\sin x}{x}$?" answer="1" />
44
86
  <wordcloud room="mood" placeholder="One word" />
45
87
  <question room="ask" placeholder="Your question" />
46
88
  ```
@@ -48,10 +90,11 @@ Besides a poll, three more kinds work the same way: one line on a slide, answere
48
90
  | Tag | Phones show | The slide shows |
49
91
  |---|---|---|
50
92
  | `<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 |
93
+ | `<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
94
  | `<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
95
  | `<question>` | A text field | The newest answers as cards, eight unless `limit` says otherwise. People may send several |
53
96
 
54
- All of them take `room`, `question` and `qr` like a poll, and `placeholder` for the text fields.
97
+ 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
98
 
56
99
  ## Show the code once
57
100
 
@@ -175,9 +218,11 @@ Two presentations with the same title share a session code. Give one of them `se
175
218
 
176
219
  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
220
 
221
+ 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.
222
+
178
223
  ## In a PDF or a shared file
179
224
 
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: each shows its question and options, with a note that it was answered live, and no join code. If the deck's `server` still answers when the file is made or opened, the results it holds appear instead. Printed slides never show the join code or the presenter's buttons.
225
+ 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
226
 
182
227
  ## Make your own activity
183
228
 
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  # Install, remove or share a theme
2
2
 
3
- mdeck comes with four themes, `neue`, `academic`, `aurora` and `minimal`, and five palettes, `swiss`, `nordic`, `neon`, `paper` and `lagoon`. They work right away, also without an internet connection. More themes and palettes are in the **theme repository**; try every one on a sample deck, light and dark, at <https://gh.tschieber.de/mdeck-themes/>.
3
+ mdeck comes with six themes, `neue`, `plain`, `academic`, `minimal`, `work` and `glass`, and six palettes, `swiss`, `nordic`, `cobalt`, `greyscale`, `paper` and `lagoon`. They work right away, also without an internet connection. More themes and palettes are in the **theme repository**; try every one on a sample deck, light and dark, at <https://gh.tschieber.de/mdeck-themes/>.
4
4
 
5
5
  ## Find and install one
6
6
 
@@ -15,16 +15,16 @@ mdeck palettes search
15
15
  Install a theme:
16
16
 
17
17
  ```sh
18
- mdeck themes install duet
18
+ mdeck themes install terminal
19
19
  ```
20
20
 
21
- A theme comes with its default palette, so this installs the palette `cobalt` too. A palette installs on its own:
21
+ A theme comes with its default palette, so this installs the palette `phosphor` too. A palette installs on its own:
22
22
 
23
23
  ```sh
24
24
  mdeck palettes install solarized
25
25
  ```
26
26
 
27
- They are installed for every deck on this computer, in a folder in your home folder (`~/.mdeck/extensions`). Then choose one at the top of a slide file (`theme: duet`, `palette: solarized`) or in the editor's **Deck** tab.
27
+ They are installed for every deck on this computer, in a folder in your home folder (`~/.mdeck/extensions`). Then choose one at the top of a slide file (`theme: terminal`, `palette: solarized`) or in the editor's **Deck** tab.
28
28
 
29
29
  The design page has the same: open it with `mdeck design`, choose **Show what you can install**, and press **Install** beside a theme or palette.
30
30
 
@@ -44,15 +44,15 @@ The slide folder then carries the theme and its palette, so it works on a comput
44
44
  mdeck themes list # what comes with mdeck, and what is installed
45
45
  mdeck themes update # newer versions of the installed themes
46
46
  mdeck palettes update
47
- mdeck themes remove duet
48
- mdeck palettes remove cobalt
47
+ mdeck themes remove terminal
48
+ mdeck palettes remove phosphor
49
49
  ```
50
50
 
51
51
  Add a slide file, or `--local`, to work beside that deck. If you changed an installed file by hand, `update` and `remove` stop and tell you which one, so your change is not lost; add `--force` to go ahead anyway.
52
52
 
53
53
  A palette that an installed theme uses by default stays until that theme is removed. A palette made for one theme only (like FHNW's `brand`) comes and goes with that theme.
54
54
 
55
- The themes and palettes that come with mdeck can be removed too: `mdeck themes remove aurora` hides it from every deck and from the lists, and `mdeck themes install aurora` brings it back, without the internet. The last theme always stays. A deck without a `theme:` setting uses `neue`; if you removed it, `mdeck check` tells you.
55
+ The themes and palettes that come with mdeck can be removed too: `mdeck themes remove glass` hides it from every deck and from the lists, and `mdeck themes install glass` brings it back, without the internet. The last theme always stays. A deck without a `theme:` setting uses `neue`; if you removed it, `mdeck check` tells you.
56
56
 
57
57
  ## What may be in one
58
58
 
Binary file
@@ -1,5 +1,5 @@
1
1
  ---
2
- theme: aurora
2
+ theme: glass
3
3
  meta:
4
4
  title: "A tour of mdeck"
5
5
  author: "Tilman Schieber"
@@ -380,7 +380,7 @@ layout: split
380
380
  :::slot left
381
381
  ```markdown
382
382
  ---
383
- theme: aurora
383
+ theme: work
384
384
  palette: lagoon
385
385
  ---
386
386
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdeck",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "description": "Write slides as a plain text file, present them in the browser, share them as one HTML file or PDF.",
6
6
  "license": "MIT",
@@ -75,5 +75,8 @@
75
75
  "vite": "^8.3.1",
76
76
  "vite-plugin-singlefile": "^2.3.3",
77
77
  "ws": "^8.22.0"
78
+ },
79
+ "devDependencies": {
80
+ "parse5": "^8.0.1"
78
81
  }
79
82
  }
@@ -34,7 +34,7 @@ If `mdeck` is not installed, rely on the quick reference below.
34
34
  ```markdown
35
35
  ---
36
36
  theme: neue # theme id from `mdeck list themes`
37
- palette: nordic # optional: swiss, nordic, neon, lagoon, paper, or one installed from the theme repository (`mdeck themes search`)
37
+ palette: nordic # optional: swiss, nordic, cobalt, greyscale, lagoon, paper, or one installed from the theme repository (`mdeck themes search`)
38
38
  appearance: light # optional: light or dark
39
39
  meta:
40
40
  title: "Talk title"
@@ -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. 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. 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 text = readFileSync(file, 'utf-8')
30
- const ext = extname(file).toLowerCase()
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
- items = items.filter(item => item && item.id != null).map(item => ({ ...item, id: String(item.id) }))
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
- export async function loadBibliography(files) {
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 { found = await readItems(file) } catch (error) {
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
+ }