mdeck 3.2.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 +27 -0
- package/assets/base.css +23 -2
- package/assets/extensions/themes/academic/styles.css +33 -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 +89 -6
- package/docs/site/content/audience.md +41 -4
- package/docs/site/content/commands.md +30 -4
- package/docs/site/content/drawing.md +1 -1
- package/docs/site/content/more-content.md +22 -0
- package/docs/site/content/sharing.md +1 -0
- package/docs/site/content/troubleshooting.md +1 -1
- package/docs/site/content/views.md +3 -1
- package/examples/citations/refs.bib +30 -0
- package/examples/citations/refs.yml +21 -0
- package/examples/citations/slides.md +43 -0
- package/examples/citations/slides.pdf +0 -0
- package/package.json +5 -1
- package/skills/write-slides/SKILL.md +7 -3
- package/src/build/bibliography.js +307 -0
- package/src/build/check.js +12 -0
- package/src/build/renderCheck.js +98 -0
- package/src/build/resultsPlugin.js +77 -0
- package/src/build/slidesPlugin.js +70 -7
- package/src/build/themeCheck.js +8 -5
- package/src/cli/args.js +22 -5
- package/src/cli/index.js +54 -12
- package/src/components/Bibliography.jsx +23 -0
- package/src/components/Numeric.jsx +59 -0
- package/src/components/Poll.jsx +59 -33
- package/src/components/QrCodeTag.jsx +11 -2
- package/src/components/Question.jsx +10 -6
- package/src/components/Scale.jsx +13 -8
- package/src/components/VideoPlayer.jsx +16 -0
- package/src/components/WordCloud.jsx +10 -6
- package/src/components/activity.jsx +90 -9
- package/src/components/citation.css +28 -0
- package/src/components/inlineText.js +40 -0
- package/src/components/numbers.js +39 -0
- package/src/components/poll.css +27 -7
- package/src/core/citations.js +108 -0
- package/src/core/editDeck.js +43 -0
- package/src/core/icons.js +2 -0
- package/src/core/ink.js +16 -0
- package/src/core/labels.js +48 -0
- package/src/core/parseSlides.js +1 -1
- package/src/core/results.js +64 -0
- package/src/core/urls.js +2 -2
- package/src/core/validateDeck.js +7 -1
- package/src/editor/sampleDeck.js +34 -8
- package/src/layouts/layoutApi.jsx +14 -3
- package/src/live/answer/answer.css +20 -0
- package/src/live/answer/answer.js +149 -24
- package/src/live/client.js +75 -7
- package/src/live/roomTag.js +6 -2
- package/src/live/rooms.js +6 -1
- package/src/runtime/ReaderView.jsx +2 -1
- package/src/runtime/citations.js +70 -0
- package/src/runtime/deck-stage.js +5 -2
- package/src/runtime/editorBridge.js +7 -1
- package/src/runtime/follow.js +89 -0
- package/src/runtime/ink/bus.js +7 -6
- package/src/runtime/ink/room.js +8 -0
- package/src/runtime/ink/select.js +4 -2
- package/src/runtime/main.jsx +22 -8
- package/src/runtime/registry.jsx +4 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// A deck as it shows, in a real browser: `mdeck check --render` finds slides
|
|
2
|
+
// whose content does not fit, text cut off at the slide's edge or hard to
|
|
3
|
+
// read on what is behind it, and errors the page reports; `mdeck snapshot`
|
|
4
|
+
// saves pictures of slides, so a person or an assistant can look at them.
|
|
5
|
+
// Both build the deck as it is (with the extensions installed here), show
|
|
6
|
+
// every slide with all its steps revealed, as in a PDF, and need Chrome
|
|
7
|
+
// (MDECK_CHROME). The measuring is the theme check's (themeCheck.js).
|
|
8
|
+
import { execFileSync } from 'node:child_process'
|
|
9
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { basename, dirname, resolve } from 'node:path'
|
|
12
|
+
import { launchChrome } from './chrome.js'
|
|
13
|
+
import { frameworkRoot } from '../paths.js'
|
|
14
|
+
import { parseSlides } from '../core/parseSlides.js'
|
|
15
|
+
import { MEASURE, RECORDER, settle } from './themeCheck.js'
|
|
16
|
+
|
|
17
|
+
// Each slide's number, line and heading, for messages.
|
|
18
|
+
export function slideLabels(source) {
|
|
19
|
+
return parseSlides(source).slides.map((slide, index) => {
|
|
20
|
+
// The slide's first written line, past its settings and blank lines.
|
|
21
|
+
const body = slide.bodySource ?? slide.source ?? { start: 0, end: 0 }
|
|
22
|
+
let start = body.start
|
|
23
|
+
while (/\s/.test(source[start] ?? '') && start < body.end) start++
|
|
24
|
+
const line = source.slice(0, start).split('\n').length
|
|
25
|
+
const heading = /^#{1,2}\s+(.+)$/m.exec(slide.content ?? '')?.[1]?.replace(/[*_`]/g, '').trim() ?? null
|
|
26
|
+
return { number: index + 1, id: slide.id, line, heading }
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Builds the deck into a temporary folder and opens it in Chrome for
|
|
31
|
+
// `use({ page, count })`, at `appearance` ('' for the deck's own), with every
|
|
32
|
+
// step revealed.
|
|
33
|
+
async function withDeckPage(slidesPath, { appearance = '', width = 1600, height = 900 } = {}, use) {
|
|
34
|
+
const abs = resolve(slidesPath)
|
|
35
|
+
const temp = mkdtempSync(resolve(tmpdir(), 'mdeck-render-'))
|
|
36
|
+
try {
|
|
37
|
+
try {
|
|
38
|
+
execFileSync(process.execPath, [resolve(frameworkRoot, 'bin/mdeck.js'), 'build', abs, '-o', resolve(temp, 'out/index.html')], { cwd: dirname(abs), stdio: 'pipe' })
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`The deck could not be built: ${String(error.stderr ?? error.message).replace(/\x1b\[[0-9;]*m/g, '').trim().split('\n').slice(-3).join(' ')}`)
|
|
41
|
+
}
|
|
42
|
+
const browser = await launchChrome({ dir: resolve(temp, 'out') })
|
|
43
|
+
try {
|
|
44
|
+
const page = await browser.open('about:blank')
|
|
45
|
+
await page.send('Page.enable')
|
|
46
|
+
await page.send('Page.addScriptToEvaluateOnNewDocument', { source: RECORDER })
|
|
47
|
+
await page.send('Page.navigate', { url: `${browser.origin}/index.html?embedded=1${appearance ? `&appearance=${appearance}` : ''}#1` })
|
|
48
|
+
await page.waitFor("location.href.includes('index.html')")
|
|
49
|
+
await page.waitForDeck()
|
|
50
|
+
await page.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false })
|
|
51
|
+
await page.evaluate("document.querySelector('deck-stage').printing = true")
|
|
52
|
+
const count = await page.evaluate("document.querySelector('deck-stage').length")
|
|
53
|
+
return await use({ page, count, show: async index => { await page.evaluate(`window.__index = ${index}; document.querySelector('deck-stage').goTo(${index})`); await settle(page) } })
|
|
54
|
+
} finally { await browser.close() }
|
|
55
|
+
} finally { rmSync(temp, { recursive: true, force: true }) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The problems of each slide, in the deck's own look, as diagnostics
|
|
59
|
+
// ({ severity, code, message, line }) at the line the slide starts.
|
|
60
|
+
export function renderCheck(slidesPath) {
|
|
61
|
+
const labels = slideLabels(readFileSync(slidesPath, 'utf8'))
|
|
62
|
+
return withDeckPage(slidesPath, {}, async ({ page, count, show }) => {
|
|
63
|
+
const diagnostics = []
|
|
64
|
+
for (let index = 0; index < count; index++) {
|
|
65
|
+
await show(index)
|
|
66
|
+
const label = labels[index] ?? { number: index + 1, line: 1, heading: null }
|
|
67
|
+
const where = `slide ${label.number}${label.heading ? ` (“${label.heading}”)` : ''}`
|
|
68
|
+
// Content that does not fit cuts off line after line; the first says which.
|
|
69
|
+
const problems = await page.evaluate(MEASURE)
|
|
70
|
+
const cut = problems.filter(problem => problem.endsWith(' is cut off at the edge'))
|
|
71
|
+
const shown = problems.filter(problem => !cut.includes(problem))
|
|
72
|
+
if (cut.length) shown.push(cut.length === 1 ? cut[0] : `${cut[0].replace(/ is cut off at the edge$/, '')} and ${cut.length - 1} more ${cut.length === 2 ? 'line is' : 'lines are'} cut off at the edge`)
|
|
73
|
+
for (const problem of shown) diagnostics.push({ severity: 'warning', code: 'render', message: `${where}: ${problem}`, line: label.line })
|
|
74
|
+
}
|
|
75
|
+
// The deck's own warning about content that does not fit is said above, per slide.
|
|
76
|
+
for (const line of (await page.evaluate('window.__report')).filter(line => !/has more content than fits/.test(line))) diagnostics.push({ severity: line.startsWith('error') ? 'error' : 'warning', code: 'render', message: `the page reported ${line}`, line: 1 })
|
|
77
|
+
return diagnostics
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Pictures of slides (numbers from 1; all when none are given) as PNG files
|
|
82
|
+
// in `outDir`, named <deck>-<number>.png. Returns the files written.
|
|
83
|
+
export async function snapshot(slidesPath, { slides = [], outDir, width = 1280, height = 720, appearance = '' } = {}) {
|
|
84
|
+
const name = basename(slidesPath).replace(/\.md$/i, '')
|
|
85
|
+
mkdirSync(outDir, { recursive: true })
|
|
86
|
+
return withDeckPage(slidesPath, { appearance, width, height }, async ({ page, count, show }) => {
|
|
87
|
+
const wanted = slides.length ? slides : Array.from({ length: count }, (_, i) => i + 1)
|
|
88
|
+
const files = []
|
|
89
|
+
for (const number of wanted) {
|
|
90
|
+
if (number < 1 || number > count) throw new Error(`There is no slide ${number}; the deck has ${count}`)
|
|
91
|
+
await show(number - 1)
|
|
92
|
+
const file = resolve(outDir, `${name}-${String(number).padStart(2, '0')}.png`)
|
|
93
|
+
writeFileSync(file, await page.screenshot({ width, height }))
|
|
94
|
+
files.push(file)
|
|
95
|
+
}
|
|
96
|
+
return files
|
|
97
|
+
})
|
|
98
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -5,13 +5,15 @@ 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 } from '../core/editDeck.js'
|
|
8
|
+
import { stripNotes as stripNotesFrom, stripActivities as stripActivitiesFrom } from '../core/editDeck.js'
|
|
9
9
|
import { componentFolders, componentFiles } from './components.js'
|
|
10
10
|
import { inkFileFor, normalizeInk, emptyInk } from '../core/ink.js'
|
|
11
|
+
import { resultsFileFor, normalizeResults } from '../core/results.js'
|
|
11
12
|
import { isOwnWrite } from './ownWrites.js'
|
|
12
13
|
import { fileURLToPath } from 'node:url'
|
|
13
14
|
import { assertDrawings } from './drawings.js'
|
|
14
15
|
import { marked } from 'marked'
|
|
16
|
+
import { resolveBibliography, sampleBibliography } from './bibliography.js'
|
|
15
17
|
|
|
16
18
|
const VIRTUAL_ID = 'virtual:slides'
|
|
17
19
|
const RESOLVED_ID = '\0virtual:slides'
|
|
@@ -24,6 +26,10 @@ const INK_ID = 'virtual:deck-ink'
|
|
|
24
26
|
const RESOLVED_INK_ID = '\0virtual:deck-ink'
|
|
25
27
|
const SVGS_ID = 'virtual:deck-svgs'
|
|
26
28
|
const RESOLVED_SVGS_ID = '\0virtual:deck-svgs'
|
|
29
|
+
const BIB_ID = 'virtual:bibliography'
|
|
30
|
+
const RESOLVED_BIB_ID = '\0virtual:bibliography'
|
|
31
|
+
const RESULTS_ID = 'virtual:deck-results'
|
|
32
|
+
const RESOLVED_RESULTS_ID = '\0virtual:deck-results'
|
|
27
33
|
|
|
28
34
|
const MIME_BY_EXT = {
|
|
29
35
|
'.png': 'image/png',
|
|
@@ -136,6 +142,8 @@ export function collectLocalAssetRefs(markdown) {
|
|
|
136
142
|
if (token.type === 'image') add(token.href)
|
|
137
143
|
if (token.type === 'html') {
|
|
138
144
|
for (const match of token.text.matchAll(/<(?:img|video|audio|source|videoplayer)\b[^>]*\bsrc=(["'])([^"']+)\1/gi)) add(match[2])
|
|
145
|
+
// Markdown pictures in a tag's attributes, such as a poll's options.
|
|
146
|
+
for (const match of token.text.matchAll(/!\[[^\]]*\]\(([^)\s]+)/g)) add(match[1])
|
|
139
147
|
}
|
|
140
148
|
})
|
|
141
149
|
}
|
|
@@ -213,12 +221,16 @@ export function deckLookOnly(registry, deckConfig = {}) {
|
|
|
213
221
|
// extension changes are announced instead of forcing a reload. `source` is
|
|
214
222
|
// the deck's text where there is no file (`mdeck design`, on the sample deck);
|
|
215
223
|
// `slidesPath` then only says where its folder is.
|
|
216
|
-
export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = false, editor = false, stripNotes = false, ink = true, embedFonts: embedThemeFonts = false, deckLook = false, source: given = null } = {}) {
|
|
224
|
+
export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = false, editor = false, stripNotes = false, stripActivities = false, ink = true, results = true, embedFonts: embedThemeFonts = false, deckLook = false, source: given = null } = {}) {
|
|
217
225
|
const abs = resolve(slidesPath)
|
|
218
226
|
const readDeck = () => given ?? readFileSync(abs, 'utf-8')
|
|
219
227
|
const watchDeck = context => { if (given === null) context.addWatchFile(abs) }
|
|
220
|
-
const sourceFor = raw =>
|
|
228
|
+
const sourceFor = raw => {
|
|
229
|
+
const shared = stripNotes ? stripNotesFrom(raw) : raw
|
|
230
|
+
return stripActivities ? stripActivitiesFrom(shared).source : shared
|
|
231
|
+
}
|
|
221
232
|
const inkPath = inkFileFor(abs)
|
|
233
|
+
const resultsPath = resultsFileFor(abs)
|
|
222
234
|
const extensionDirs = extensionRoots(abs).map(root => root.dir)
|
|
223
235
|
let componentDirs = componentFolders(abs, readDeck()).map(folder => folder.dir)
|
|
224
236
|
const watchDirs = () => [...extensionDirs, ...componentDirs]
|
|
@@ -233,8 +245,18 @@ export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = f
|
|
|
233
245
|
for (const dir of next) if (!allow.includes(dir)) allow.push(dir)
|
|
234
246
|
return true
|
|
235
247
|
}
|
|
236
|
-
const ALL_IDS = [RESOLVED_ID, RESOLVED_COMPONENTS_ID, RESOLVED_EXTENSIONS_ID, RESOLVED_INK_ID, RESOLVED_SVGS_ID]
|
|
237
|
-
const SOURCE_IDS = [RESOLVED_ID, RESOLVED_INK_ID, RESOLVED_SVGS_ID]
|
|
248
|
+
const ALL_IDS = [RESOLVED_ID, RESOLVED_COMPONENTS_ID, RESOLVED_EXTENSIONS_ID, RESOLVED_INK_ID, RESOLVED_SVGS_ID, RESOLVED_BIB_ID]
|
|
249
|
+
const SOURCE_IDS = [RESOLVED_ID, RESOLVED_INK_ID, RESOLVED_SVGS_ID, RESOLVED_BIB_ID]
|
|
250
|
+
// The reference files (and .csl style) the deck names, from the last load.
|
|
251
|
+
let bibFiles = []
|
|
252
|
+
// The dev and edit servers also show the sample deck (?sample=1, the
|
|
253
|
+
// design page), with its references from memory; builds never do.
|
|
254
|
+
let serving = false
|
|
255
|
+
// The editor keeps its page: the formatted references arrive as a hot update.
|
|
256
|
+
const bibUpdate = server => {
|
|
257
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_BIB_ID)
|
|
258
|
+
return editor && mod ? [mod] : []
|
|
259
|
+
}
|
|
238
260
|
const invalidate = (server, ids) => {
|
|
239
261
|
for (const id of ids) {
|
|
240
262
|
const mod = server.moduleGraph.getModuleById(id)
|
|
@@ -249,6 +271,7 @@ export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = f
|
|
|
249
271
|
|
|
250
272
|
return {
|
|
251
273
|
name: 'vite-plugin-slides',
|
|
274
|
+
configResolved(config) { serving = config.command === 'serve' },
|
|
252
275
|
configureServer(server) {
|
|
253
276
|
server.watcher.add([abs, inkPath, ...watchDirs()])
|
|
254
277
|
const refresh = file => {
|
|
@@ -266,6 +289,8 @@ export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = f
|
|
|
266
289
|
if (id === EXTENSIONS_ID) return RESOLVED_EXTENSIONS_ID
|
|
267
290
|
if (id === INK_ID) return RESOLVED_INK_ID
|
|
268
291
|
if (id === SVGS_ID) return RESOLVED_SVGS_ID
|
|
292
|
+
if (id === BIB_ID) return RESOLVED_BIB_ID
|
|
293
|
+
if (id === RESULTS_ID) return RESOLVED_RESULTS_ID
|
|
269
294
|
},
|
|
270
295
|
load(id) {
|
|
271
296
|
if (id === RESOLVED_ID) {
|
|
@@ -295,14 +320,44 @@ export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = f
|
|
|
295
320
|
if (ink && existsSync(inkPath)) {
|
|
296
321
|
try { data = normalizeInk(JSON.parse(readFileSync(inkPath, 'utf-8')), size) } catch (error) { this.warn(`${inkPath}: ${error.message}; showing no ink`) }
|
|
297
322
|
}
|
|
323
|
+
// Without the activity slides, drawings follow their slides to their new ids.
|
|
324
|
+
if (stripActivities) {
|
|
325
|
+
try {
|
|
326
|
+
const { ids, removed } = stripActivitiesFrom(readDeck())
|
|
327
|
+
const gone = new Set(removed)
|
|
328
|
+
data = { ...data, slides: Object.fromEntries(Object.entries(data.slides).filter(([id]) => !gone.has(id)).map(([id, strokes]) => [ids[id] ?? id, strokes])) }
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
298
331
|
return `export default ${JSON.stringify(data)}\nexport const inkFileName = ${JSON.stringify(basename(inkPath))}`
|
|
299
332
|
}
|
|
333
|
+
// The answers kept in the talk (<deck>.results.json): builds show them
|
|
334
|
+
// where no room server is (src/live/client.js). `mdeck run` shows the
|
|
335
|
+
// live answers, and writes the file, so it does not read it here.
|
|
336
|
+
if (id === RESOLVED_RESULTS_ID) {
|
|
337
|
+
let data = normalizeResults({})
|
|
338
|
+
if (results && !serving && existsSync(resultsPath)) {
|
|
339
|
+
try { data = normalizeResults(JSON.parse(readFileSync(resultsPath, 'utf-8'))) } catch (error) { this.warn(`${resultsPath}: ${error.message}; showing no results`) }
|
|
340
|
+
}
|
|
341
|
+
return `export default ${JSON.stringify(data)}`
|
|
342
|
+
}
|
|
300
343
|
if (id === RESOLVED_SVGS_ID) {
|
|
301
344
|
watchDeck(this)
|
|
302
345
|
const svgs = themedSvgs(abs, sourceFor(readDeck()))
|
|
303
346
|
for (const { file } of Object.values(svgs)) this.addWatchFile(file)
|
|
304
347
|
return `export default ${JSON.stringify(Object.fromEntries(Object.entries(svgs).map(([ref, { markup }]) => [ref, markup])))}`
|
|
305
348
|
}
|
|
349
|
+
// The deck's citations and references, formatted with citeproc here so
|
|
350
|
+
// that none of it travels in the deck (bibliography.js).
|
|
351
|
+
if (id === RESOLVED_BIB_ID) {
|
|
352
|
+
watchDeck(this)
|
|
353
|
+
const data = bibliography => bibliography && { ids: bibliography.ids, clusters: bibliography.clusters, singles: bibliography.singles, entries: bibliography.entries, order: bibliography.order }
|
|
354
|
+
return Promise.all([resolveBibliography(abs, sourceFor(readDeck()), { sample: serving }), serving ? sampleBibliography() : null]).then(([bibliography, sample]) => {
|
|
355
|
+
bibFiles = bibliography ? [...bibliography.files, bibliography.style].filter(Boolean) : []
|
|
356
|
+
for (const file of bibFiles) if (existsSync(file)) this.addWatchFile(file)
|
|
357
|
+
if (bibliography?.diagnostics.length) this.warn(bibliography.diagnostics.map(d => d.message).join('\n'))
|
|
358
|
+
return `export default ${JSON.stringify(data(bibliography))}\nexport const sample = ${JSON.stringify(data(sample))}`
|
|
359
|
+
})
|
|
360
|
+
}
|
|
306
361
|
if (id === RESOLVED_EXTENSIONS_ID) {
|
|
307
362
|
const registry = loadRegistry(abs)
|
|
308
363
|
for (const record of registry.records) this.addWatchFile(record.file)
|
|
@@ -347,15 +402,23 @@ export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = f
|
|
|
347
402
|
}
|
|
348
403
|
// mdeck's own writes (saved ink, a new slide id) must not reload the
|
|
349
404
|
// windows mid-talk: the pages refresh their ink on `mdeck:ink` instead.
|
|
405
|
+
// The results file is written during the talk and read only by builds.
|
|
406
|
+
if (changed === resultsPath) return []
|
|
350
407
|
const own = [inkPath, abs].includes(changed) && (() => { try { return isOwnWrite(changed, readFileSync(changed, 'utf-8')) } catch { return false } })()
|
|
351
408
|
if (own) {
|
|
352
409
|
invalidate(server, changed === abs ? SOURCE_IDS : [RESOLVED_INK_ID])
|
|
353
|
-
return []
|
|
410
|
+
return changed === abs ? bibUpdate(server) : []
|
|
354
411
|
}
|
|
355
412
|
if (changed === inkPath) {
|
|
356
413
|
reload(server, [RESOLVED_INK_ID], changed)
|
|
357
414
|
return []
|
|
358
415
|
}
|
|
416
|
+
if (bibFiles.includes(changed)) {
|
|
417
|
+
invalidate(server, [RESOLVED_BIB_ID])
|
|
418
|
+
if (editor) return bibUpdate(server)
|
|
419
|
+
server.ws.send({ type: 'full-reload' })
|
|
420
|
+
return []
|
|
421
|
+
}
|
|
359
422
|
if (changed === abs) {
|
|
360
423
|
if (followComponentFolders(server)) {
|
|
361
424
|
reload(server, ALL_IDS, changed)
|
|
@@ -363,7 +426,7 @@ export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = f
|
|
|
363
426
|
}
|
|
364
427
|
// The deck sets the ink's design size and which SVGs are inlined.
|
|
365
428
|
invalidate(server, SOURCE_IDS)
|
|
366
|
-
if (editor) return
|
|
429
|
+
if (editor) return bibUpdate(server)
|
|
367
430
|
server.ws.send({ type: 'full-reload' })
|
|
368
431
|
}
|
|
369
432
|
},
|
package/src/build/themeCheck.js
CHANGED
|
@@ -10,17 +10,17 @@ import { tmpdir } from 'node:os'
|
|
|
10
10
|
import { resolve } from 'node:path'
|
|
11
11
|
import { launchChrome } from './chrome.js'
|
|
12
12
|
import { frameworkRoot } from '../paths.js'
|
|
13
|
-
import { sampleDeck, SAMPLE_PICTURE } from '../editor/sampleDeck.js'
|
|
13
|
+
import { sampleDeck, SAMPLE_PICTURE, SAMPLE_REFERENCES, SAMPLE_REFERENCES_FILE } from '../editor/sampleDeck.js'
|
|
14
14
|
import { loadRegistry } from '../extensions/discover.js'
|
|
15
15
|
|
|
16
16
|
// Runs in the page before the deck: keeps what the page reports.
|
|
17
|
-
const RECORDER = `window.__report = [];
|
|
17
|
+
export const RECORDER = `window.__report = [];
|
|
18
18
|
for (const level of ['warn', 'error']) { const original = console[level]; console[level] = (...args) => { window.__report.push(level + ': ' + args.map(String).join(' ')); original.apply(console, args) } }
|
|
19
19
|
addEventListener('error', event => window.__report.push('error: ' + event.message))
|
|
20
20
|
addEventListener('unhandledrejection', event => window.__report.push('error: ' + (event.reason?.message ?? event.reason)))`
|
|
21
21
|
|
|
22
22
|
// Runs in the page for the slide on screen: what is cut off or hard to read.
|
|
23
|
-
const MEASURE = `(() => {
|
|
23
|
+
export const MEASURE = `(() => {
|
|
24
24
|
const slide = document.querySelector('deck-stage').querySelectorAll(':scope > section')[window.__index]
|
|
25
25
|
const problems = []
|
|
26
26
|
const frame = slide.getBoundingClientRect()
|
|
@@ -71,7 +71,9 @@ const MEASURE = `(() => {
|
|
|
71
71
|
const box = range.getBoundingClientRect()
|
|
72
72
|
if (!box.width || !box.height) continue
|
|
73
73
|
const words = text.textContent.trim().slice(0, 40)
|
|
74
|
-
|
|
74
|
+
// Text in a box that scrolls (a component's long list) is meant to be out of view.
|
|
75
|
+
const scrolls = node => { for (; node && node !== slide; node = node.parentElement) if (/auto|scroll/.test(getComputedStyle(node).overflowY + getComputedStyle(node).overflowX)) return true; return false }
|
|
76
|
+
if (!scrolls(element) && (box.left < frame.left - 2 * scale || box.right > frame.right + 2 * scale || box.top < frame.top - 2 * scale || box.bottom > frame.bottom + 2 * scale)) problems.push('"' + words + '" is cut off at the edge')
|
|
75
77
|
const colour = parse(style.color), back = behind(element, box)
|
|
76
78
|
if (!colour || !back || colour.a < 0.5 || element.closest('.code-block, .katex')) continue
|
|
77
79
|
const large = parseFloat(style.fontSize) / scale >= 40 || (parseFloat(style.fontSize) / scale >= 30 && Number(style.fontWeight) >= 600)
|
|
@@ -89,6 +91,7 @@ export function buildSampleDeck({ extensions = {}, outDir }) {
|
|
|
89
91
|
try {
|
|
90
92
|
writeFileSync(resolve(temp, 'slides.md'), sampleDeck())
|
|
91
93
|
writeFileSync(resolve(temp, 'picture.svg'), SAMPLE_PICTURE)
|
|
94
|
+
writeFileSync(resolve(temp, SAMPLE_REFERENCES_FILE), SAMPLE_REFERENCES)
|
|
92
95
|
// Keyed by id, or by "kind:id" to keep a theme and a palette of one name apart.
|
|
93
96
|
for (const [key, dir] of Object.entries(extensions)) cpSync(dir, resolve(temp, 'extensions', ...key.split(':').map((part, i, all) => all.length > 1 && i === 0 ? `${part}s` : part)), { recursive: true })
|
|
94
97
|
mkdirSync(resolve(temp, 'home'))
|
|
@@ -120,7 +123,7 @@ export async function withSampleDeck({ extensions = {} } = {}, use) {
|
|
|
120
123
|
} finally { rmSync(temp, { recursive: true, force: true }) }
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
const settle = page => page.evaluate('new Promise(done => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(done, 60))))')
|
|
126
|
+
export const settle = page => page.evaluate('new Promise(done => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(done, 60))))')
|
|
124
127
|
|
|
125
128
|
// Every slide of each look ({ theme, palette }), light and dark; returns the
|
|
126
129
|
// problems found, one line each.
|
package/src/cli/args.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Parse the entire command before it can start a server or change any files.
|
|
2
2
|
const value = 'value', flag = 'flag', optional = 'optional'
|
|
3
3
|
const common = { '--port': value, '--no-open': flag }
|
|
4
|
-
const output = { '--output': value, '-o': '--output', '--no-drawings': flag }
|
|
4
|
+
const output = { '--output': value, '-o': '--output', '--no-drawings': flag, '--no-results': flag }
|
|
5
5
|
const commands = {
|
|
6
6
|
new: { max: 0 },
|
|
7
7
|
run: { ...common, '--network': flag, '--server': optional, max: 1 },
|
|
@@ -9,17 +9,19 @@ const commands = {
|
|
|
9
9
|
design: { ...common, max: 1 },
|
|
10
10
|
themes: { '--local': flag, '--force': flag, '--check': flag, '--output': value, '-o': '--output', max: 4 },
|
|
11
11
|
palettes: { '--local': flag, '--force': flag, max: 4 },
|
|
12
|
-
build: { ...output, '--single-file': flag, '--launchers': flag, '--reader': flag, '--notes': flag, max: 1 },
|
|
13
|
-
send: { ...output, '--notes': flag, '--no-pdf': flag, max: 1 },
|
|
14
|
-
pdf: { ...output, max: 1 },
|
|
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 },
|
|
15
15
|
preview: { ...common, max: 1 },
|
|
16
|
-
check: { '--strict': flag, max: 1 },
|
|
16
|
+
check: { '--strict': flag, '--render': flag, max: 1 },
|
|
17
|
+
snapshot: { '--output': value, '-o': '--output', '--slide': value, '--dark': flag, '--light': flag, max: 1 },
|
|
17
18
|
list: { '--json': flag, max: 2 },
|
|
18
19
|
starter: { max: 2 },
|
|
19
20
|
docs: { ...common, '--build': flag, max: 1 },
|
|
20
21
|
server: { '--port': value, '--host': optional, max: 0 },
|
|
21
22
|
skill: { '--print': flag, '--install': 'list', '--project': flag, max: 0 },
|
|
22
23
|
migrate: { '--dry-run': flag, max: 1 },
|
|
24
|
+
results: { '--output': value, '-o': '--output', max: 1 },
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function validateServerOrigin(address) {
|
|
@@ -28,6 +30,19 @@ export function validateServerOrigin(address) {
|
|
|
28
30
|
if (!url || !['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/') throw new Error('--server needs an http:// or https:// server origin, for example https://slides.example.org')
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
// Slide numbers from "3", "3,5" or "2-4": in order, each once.
|
|
34
|
+
export function parseSlideNumbers(text) {
|
|
35
|
+
const numbers = []
|
|
36
|
+
for (const part of String(text).split(',').map(part => part.trim()).filter(Boolean)) {
|
|
37
|
+
const range = /^(\d+)(?:-(\d+))?$/.exec(part)
|
|
38
|
+
if (!range) throw new Error(`--slide takes slide numbers such as 3, 3,5 or 2-4, not "${part}"`)
|
|
39
|
+
const [from, to] = [Number(range[1]), Number(range[2] ?? range[1])]
|
|
40
|
+
if (!from || to < from) throw new Error(`--slide takes slide numbers from 1, with the smaller one first: not "${part}"`)
|
|
41
|
+
for (let n = from; n <= to; n++) if (!numbers.includes(n)) numbers.push(n)
|
|
42
|
+
}
|
|
43
|
+
return numbers
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
export function parseArgs(command, argv) {
|
|
32
47
|
const schema = commands[command]
|
|
33
48
|
if (!schema) throw new Error(`Unknown command: "${command}"`)
|
|
@@ -70,8 +85,10 @@ export function parseArgs(command, argv) {
|
|
|
70
85
|
validateServerOrigin(options['--server'])
|
|
71
86
|
}
|
|
72
87
|
if (command === 'build' && options['--notes'] && !options['--reader']) throw new Error('--notes requires --reader for mdeck build')
|
|
88
|
+
if (command === 'build' && options['--no-polls'] && !options['--reader']) throw new Error('--no-polls requires --reader for mdeck build')
|
|
73
89
|
if (command === 'build' && options['--launchers'] && options['--single-file']) throw new Error('--launchers is only available for folders, not with --single-file.')
|
|
74
90
|
if (command === 'list' && positionals.length === 2 && !['layouts', 'themes', 'palettes'].includes(positionals[0])) throw new Error('Use mdeck list [layouts|themes|palettes] [slides.md]')
|
|
91
|
+
if (command === 'snapshot' && options['--dark'] && options['--light']) throw new Error('Choose --dark or --light')
|
|
75
92
|
if (command === 'skill' && options['--print'] && options['--install']) throw new Error('Choose --print or --install')
|
|
76
93
|
return { options, positionals }
|
|
77
94
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer, build, preview } from 'vite'
|
|
2
|
-
import { existsSync, copyFileSync, mkdtempSync, readFileSync, statSync } from 'fs'
|
|
2
|
+
import { existsSync, copyFileSync, mkdtempSync, readFileSync, statSync, writeFileSync } from 'fs'
|
|
3
3
|
import { chmod, rm, mkdir, writeFile } from 'fs/promises'
|
|
4
4
|
import { resolve, dirname, basename, relative, isAbsolute } from 'path'
|
|
5
5
|
import { tmpdir } from 'os'
|
|
@@ -12,9 +12,10 @@ import { loadRegistry, manifestsOf, serializeRegistry } from '../extensions/disc
|
|
|
12
12
|
import { BACKUP_DIR } from '../build/editorPlugin.js'
|
|
13
13
|
import { createEditorServer, createDesignServer } from '../build/editorServer.js'
|
|
14
14
|
import { homePlugin } from '../build/homePlugin.js'
|
|
15
|
-
import { checkDeck } from '../build/check.js'
|
|
15
|
+
import { checkDeck, bibliographyDiagnostics } from '../build/check.js'
|
|
16
16
|
import { livePlugin, startLiveServer } from '../live/server.js'
|
|
17
17
|
import { inkPlugin } from '../build/inkPlugin.js'
|
|
18
|
+
import { resultsPlugin } from '../build/resultsPlugin.js'
|
|
18
19
|
import { createPairing, pairingPlugin } from '../build/pairing.js'
|
|
19
20
|
import { isAllowedRequest } from '../build/editorPlugin.js'
|
|
20
21
|
import { renderPdf, attachPdf, findChrome } from '../build/pdf.js'
|
|
@@ -24,9 +25,10 @@ import { ManifestError, KINDS } from '../extensions/manifest.js'
|
|
|
24
25
|
|
|
25
26
|
import { frameworkRoot } from '../paths.js'
|
|
26
27
|
import { parseSlides } from '../core/parseSlides.js'
|
|
27
|
-
import { stripNotes as stripNotesFrom } from '../core/editDeck.js'
|
|
28
|
-
import { parseArgs, validateServerOrigin } from './args.js'
|
|
28
|
+
import { stripNotes as stripNotesFrom, stripActivities as stripActivitiesFrom } from '../core/editDeck.js'
|
|
29
|
+
import { parseArgs, parseSlideNumbers, validateServerOrigin } from './args.js'
|
|
29
30
|
import { assertDrawings } from '../build/drawings.js'
|
|
31
|
+
import { resultsFileFor, normalizeResults, resultsCsv } from '../core/results.js'
|
|
30
32
|
import { createRelayAccess } from '../build/relayAccess.js'
|
|
31
33
|
import { sessionCode } from '../live/code.js'
|
|
32
34
|
import { migrateDeck } from './migrate.js'
|
|
@@ -178,12 +180,13 @@ async function runNewWizard() {
|
|
|
178
180
|
}
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
async function copyLocalAssets(slidesPath, outDir, { stripNotes = false } = {}) {
|
|
183
|
+
async function copyLocalAssets(slidesPath, outDir, { stripNotes = false, stripActivities = false } = {}) {
|
|
182
184
|
const absSlides = resolve(slidesPath)
|
|
183
185
|
const deckDir = dirname(absSlides)
|
|
184
186
|
const absOutDir = resolve(outDir)
|
|
185
187
|
const raw = readFileSync(absSlides, 'utf-8')
|
|
186
|
-
|
|
188
|
+
let markdown = stripNotes ? stripNotesFrom(raw) : raw
|
|
189
|
+
if (stripActivities) markdown = stripActivitiesFrom(markdown).source
|
|
187
190
|
|
|
188
191
|
for (const ref of collectLocalAssetRefs(markdown)) {
|
|
189
192
|
const source = resolve(deckDir, ref)
|
|
@@ -320,20 +323,28 @@ const HELP = `
|
|
|
320
323
|
|
|
321
324
|
${c.dim}Make something to hand out${c.reset}
|
|
322
325
|
${c.green}mdeck build${c.reset} [slides.md] [-o dir/index.html] A folder to host
|
|
323
|
-
--reader reader view, notes removed unless --notes
|
|
326
|
+
--reader reader view, notes removed unless --notes, polls kept as a record unless --no-polls
|
|
324
327
|
--single-file everything inlined into one file instead
|
|
325
328
|
--launchers add macOS/Linux and Windows launchers (folders only)
|
|
326
329
|
${c.green}mdeck send${c.reset} [slides.md] [-o talk.html] One file to send: the reader view, speaker notes removed,
|
|
327
330
|
with a PDF inside
|
|
328
331
|
--notes keep the speaker notes in the file
|
|
332
|
+
--no-polls for send, build --reader and pdf: leave out the slides with polls and join codes
|
|
329
333
|
--no-pdf do not render the PDF inside
|
|
330
334
|
${c.green}mdeck pdf${c.reset} [slides.md] [-o talk.pdf] A PDF, one page per slide
|
|
331
335
|
--no-drawings for build, send and pdf: leave out <slides>.drawings.json
|
|
336
|
+
--no-results for build, send and pdf: leave out <slides>.results.json (the answers kept in the talk)
|
|
332
337
|
${c.green}mdeck preview${c.reset} [folder] Preview a built folder (default: dist)
|
|
333
338
|
|
|
334
339
|
${c.dim}Check and look things up${c.reset}
|
|
335
340
|
${c.green}mdeck migrate${c.reset} [slides.md] [--dry-run] Update settings, local layouts and drawings to API 2.0
|
|
336
341
|
${c.green}mdeck check${c.reset} [slides.md] [--strict] Validate source and local assets
|
|
342
|
+
--render also show every slide in Chrome, all steps revealed: content that
|
|
343
|
+
does not fit, text cut off or hard to read, errors on the page
|
|
344
|
+
${c.green}mdeck snapshot${c.reset} [slides.md] Pictures of slides as PNG files (default: .mdeck-snapshots/)
|
|
345
|
+
--slide <numbers> only these slides: 3, 3,5 or 2-4
|
|
346
|
+
--dark | --light in this appearance instead of the deck's own -o <folder>
|
|
347
|
+
${c.green}mdeck results${c.reset} [slides.md] [-o answers.csv] The answers kept in the talk, as CSV (room, phone, time, answer)
|
|
337
348
|
${c.green}mdeck list${c.reset} [layouts|themes|palettes] [slides.md] [--json]
|
|
338
349
|
List built-in and deck-local layouts, themes and palettes
|
|
339
350
|
${c.green}mdeck starter${c.reset} <layout> [slides.md] Print starter Markdown for a layout
|
|
@@ -459,15 +470,42 @@ if (command === 'new') {
|
|
|
459
470
|
else if (hasFlag('--dry-run')) tip('No files changed. Run without --dry-run to apply these changes.')
|
|
460
471
|
} catch (error) { err(error.message); process.exitCode = 1 }
|
|
461
472
|
|
|
473
|
+
} else if (command === 'results') {
|
|
474
|
+
const input = requireInput('results')
|
|
475
|
+
const file = resultsFileFor(resolve(input))
|
|
476
|
+
if (!existsSync(file)) { err(`No answers kept for ${basename(input)} yet: ${basename(file)} appears beside it once people answer during mdeck run.`); process.exit(1) }
|
|
477
|
+
let csv
|
|
478
|
+
try { csv = resultsCsv(normalizeResults(JSON.parse(readFileSync(file, 'utf8')))) } catch (error) { err(`${basename(file)}: ${error.message}`); process.exit(1) }
|
|
479
|
+
const out = outputOption()
|
|
480
|
+
if (out) { writeFileSync(out, csv); ok(`Saved ${relative(process.cwd(), resolve(out))}`) }
|
|
481
|
+
else process.stdout.write(csv)
|
|
462
482
|
} else if (command === 'check') {
|
|
463
483
|
const input = requireInput('check')
|
|
464
484
|
const registry = registryFor(input)
|
|
465
485
|
const { diagnostics } = checkDeck(input, registry)
|
|
486
|
+
diagnostics.push(...await bibliographyDiagnostics(input))
|
|
487
|
+
if (hasFlag('--render') && !diagnostics.some(d => d.severity === 'error')) {
|
|
488
|
+
tip('Building the deck and looking at every slide in Chrome…')
|
|
489
|
+
try {
|
|
490
|
+
const { renderCheck } = await import('../build/renderCheck.js')
|
|
491
|
+
diagnostics.push(...await renderCheck(input))
|
|
492
|
+
} catch (error) { diagnostics.push({ severity: 'error', code: 'render', message: error.message, line: 1 }) }
|
|
493
|
+
}
|
|
466
494
|
if (diagnostics.length) console.log(formatDiagnostics(diagnostics, input))
|
|
467
495
|
const warned = diagnostics.length || registry.warnings.length
|
|
468
496
|
if (diagnostics.some(d => d.severity === 'error') || (warned && hasFlag('--strict'))) process.exitCode = 1
|
|
469
497
|
else ok(`Checked ${input}${warned ? ' (with warnings)' : ''}`)
|
|
470
498
|
|
|
499
|
+
} else if (command === 'snapshot') {
|
|
500
|
+
const input = requireInput('snapshot')
|
|
501
|
+
try {
|
|
502
|
+
const slides = parseSlideNumbers(parsed.options['--slide'] ?? '')
|
|
503
|
+
const outDir = resolve(outputOption() ?? resolve(dirname(resolve(input)), '.mdeck-snapshots'))
|
|
504
|
+
const appearance = hasFlag('--dark') ? 'dark' : hasFlag('--light') ? 'light' : ''
|
|
505
|
+
const { snapshot } = await import('../build/renderCheck.js')
|
|
506
|
+
for (const file of await snapshot(input, { slides, outDir, appearance })) console.log(relative(process.cwd(), file))
|
|
507
|
+
} catch (error) { err(error.message); process.exitCode = 1 }
|
|
508
|
+
|
|
471
509
|
} else if (command === 'list') {
|
|
472
510
|
const KIND_WORDS = { layouts: 'layout', themes: 'theme', palettes: 'palette' }
|
|
473
511
|
const [first, ...others] = positionals()
|
|
@@ -537,7 +575,7 @@ if (command === 'new') {
|
|
|
537
575
|
...base,
|
|
538
576
|
// A paired iPad may save ink and steer the rooms, like this computer.
|
|
539
577
|
...(relay ? { base: relay.base } : {}),
|
|
540
|
-
plugins: [...(relay ? [stripRelayBase(relay.base)] : []), ...base.plugins, homePlugin(abs, { services, pairing, relay, server: relay?.server ?? null }), pairingPlugin(pairing), livePlugin({ pairing, upstream, key: process.env.MDECK_SERVER_KEY, session: () => sessionCode(parseSlides(readFileSync(abs, 'utf8')).deckConfig) }), inkPlugin(abs, { authorize: request => isAllowedRequest(request) || pairing.allows(request) })],
|
|
578
|
+
plugins: [...(relay ? [stripRelayBase(relay.base)] : []), ...base.plugins, homePlugin(abs, { services, pairing, relay, server: relay?.server ?? null }), pairingPlugin(pairing), livePlugin({ pairing, upstream, key: process.env.MDECK_SERVER_KEY, session: () => sessionCode(parseSlides(readFileSync(abs, 'utf8')).deckConfig) }), inkPlugin(abs, { authorize: request => isAllowedRequest(request) || pairing.allows(request) }), resultsPlugin(abs, { authorize: request => isAllowedRequest(request) || pairing.allows(request) })],
|
|
541
579
|
publicDir: dirname(abs),
|
|
542
580
|
server: {
|
|
543
581
|
...base.server,
|
|
@@ -578,7 +616,8 @@ if (command === 'new') {
|
|
|
578
616
|
ok(`Launch page: ${c.cyan}${new URL('home.html', local).href}${c.reset}`)
|
|
579
617
|
tip(`Presenter: ${new URL('?view=presenter', local).href}`)
|
|
580
618
|
tip(`Audience: ${new URL('?view=audience', local).href}`)
|
|
581
|
-
|
|
619
|
+
// Phones in the room follow the talk (the reader view would show slides ahead).
|
|
620
|
+
if (exposed) for (const url of server.resolvedUrls.network) tip(`Follow: ${new URL('?view=follow', url).href}`)
|
|
582
621
|
console.log()
|
|
583
622
|
if (relay) tip(`iPad: choose "Show pairing code" on the launch page; it opens ${relay.url}`)
|
|
584
623
|
ok(`Watching ${c.cyan}${abs}${c.reset}`)
|
|
@@ -645,6 +684,8 @@ if (command === 'new') {
|
|
|
645
684
|
const launchers = !sending && hasFlag('--launchers')
|
|
646
685
|
const reader = sending || hasFlag('--reader')
|
|
647
686
|
const stripNotes = reader && !hasFlag('--notes')
|
|
687
|
+
// Polls show as a record of the talk; --no-polls leaves their slides and join codes out.
|
|
688
|
+
const stripActivities = reader && hasFlag('--no-polls')
|
|
648
689
|
const wantPdf = sending && !hasFlag('--no-pdf')
|
|
649
690
|
|
|
650
691
|
if (!hasFlag('--no-drawings')) { try { assertDrawings(input) } catch (error) { err(error.message); process.exit(1) } }
|
|
@@ -659,7 +700,7 @@ if (command === 'new') {
|
|
|
659
700
|
try {
|
|
660
701
|
await build({
|
|
661
702
|
...baseConfig(input, { selfContained, defaultView: reader ? 'reader' : 'deck' }),
|
|
662
|
-
plugins: [preact(), slidesPlugin(resolve(input), { inlineImages: selfContained, inlineMedia: selfContained, embedFonts: selfContained, stripNotes, ink: !hasFlag('--no-drawings'), deckLook: reader }), viteSingleFile()],
|
|
703
|
+
plugins: [preact(), slidesPlugin(resolve(input), { inlineImages: selfContained, inlineMedia: selfContained, embedFonts: selfContained, stripNotes, stripActivities, ink: !hasFlag('--no-drawings'), results: !hasFlag('--no-results'), deckLook: reader }), viteSingleFile()],
|
|
663
704
|
build: {
|
|
664
705
|
outDir,
|
|
665
706
|
emptyOutDir: !tempDir,
|
|
@@ -677,7 +718,7 @@ if (command === 'new') {
|
|
|
677
718
|
await mkdir(finalOutDir, { recursive: true })
|
|
678
719
|
|
|
679
720
|
if (!selfContained) {
|
|
680
|
-
await copyLocalAssets(input, finalOutDir, { stripNotes })
|
|
721
|
+
await copyLocalAssets(input, finalOutDir, { stripNotes, stripActivities })
|
|
681
722
|
if (launchers) await writePresenterLaunchers(finalOutDir, htmlFilename)
|
|
682
723
|
}
|
|
683
724
|
|
|
@@ -701,6 +742,7 @@ if (command === 'new') {
|
|
|
701
742
|
} finally { await rm(pdfDir, { recursive: true, force: true }) }
|
|
702
743
|
}
|
|
703
744
|
if (stripNotes) tip('Speaker notes were removed from this file (use --notes to keep them).')
|
|
745
|
+
if (stripActivities && stripActivitiesFrom(readFileSync(resolve(input), 'utf-8')).removed.length) tip('Slides with polls were left out (--no-polls).')
|
|
704
746
|
ok(`${sending ? 'Made' : 'Built'}: ${c.cyan}${htmlFile}${c.reset}${reader ? ' — opens in the reader view' : ''}${selfContained ? ' (single file)' : ' + local assets'}\n`)
|
|
705
747
|
|
|
706
748
|
// ── skill ─────────────────────────────────────────────────────────────────────
|
|
@@ -730,7 +772,7 @@ if (command === 'new') {
|
|
|
730
772
|
try {
|
|
731
773
|
await build({
|
|
732
774
|
...baseConfig(input, { selfContained: true }),
|
|
733
|
-
plugins: [preact(), slidesPlugin(resolve(input), { inlineImages: true, inlineMedia: true, embedFonts: true, ink: !hasFlag('--no-drawings'), deckLook: true }), viteSingleFile()],
|
|
775
|
+
plugins: [preact(), slidesPlugin(resolve(input), { inlineImages: true, inlineMedia: true, embedFonts: true, stripActivities: hasFlag('--no-polls'), ink: !hasFlag('--no-drawings'), results: !hasFlag('--no-results'), deckLook: true }), viteSingleFile()],
|
|
734
776
|
build: { outDir: tempDir, emptyOutDir: true, target: 'esnext', assetsInlineLimit: 100 * 1024 * 1024 },
|
|
735
777
|
logLevel: 'warn',
|
|
736
778
|
})
|